##// END OF EJS Templates
subrepo: make update -C clean the working directory for git subrepos...
Erik Zielke -
r13324:e5617047 default
parent child Browse files
Show More
@@ -1,945 +1,961 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, urlparse, posixpath
9 9 import stat, subprocess, tarfile
10 10 from i18n import _
11 11 import config, util, node, error, cmdutil
12 12 hg = None
13 13
14 14 nullstate = ('', '', 'empty')
15 15
16 16 def state(ctx, ui):
17 17 """return a state dict, mapping subrepo paths configured in .hgsub
18 18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 19 (key in types dict))
20 20 """
21 21 p = config.config()
22 22 def read(f, sections=None, remap=None):
23 23 if f in ctx:
24 24 try:
25 25 data = ctx[f].data()
26 26 except IOError, err:
27 27 if err.errno != errno.ENOENT:
28 28 raise
29 29 # handle missing subrepo spec files as removed
30 30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 31 return
32 32 p.parse(f, data, sections, remap, read)
33 33 else:
34 34 raise util.Abort(_("subrepo spec file %s not found") % f)
35 35
36 36 if '.hgsub' in ctx:
37 37 read('.hgsub')
38 38
39 39 for path, src in ui.configitems('subpaths'):
40 40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41 41
42 42 rev = {}
43 43 if '.hgsubstate' in ctx:
44 44 try:
45 45 for l in ctx['.hgsubstate'].data().splitlines():
46 46 revision, path = l.split(" ", 1)
47 47 rev[path] = revision
48 48 except IOError, err:
49 49 if err.errno != errno.ENOENT:
50 50 raise
51 51
52 52 state = {}
53 53 for path, src in p[''].items():
54 54 kind = 'hg'
55 55 if src.startswith('['):
56 56 if ']' not in src:
57 57 raise util.Abort(_('missing ] in subrepo source'))
58 58 kind, src = src.split(']', 1)
59 59 kind = kind[1:]
60 60
61 61 for pattern, repl in p.items('subpaths'):
62 62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 63 # does a string decode.
64 64 repl = repl.encode('string-escape')
65 65 # However, we still want to allow back references to go
66 66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 67 # extra escapes are needed because re.sub string decodes.
68 68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 69 try:
70 70 src = re.sub(pattern, repl, src, 1)
71 71 except re.error, e:
72 72 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 73 % (p.source('subpaths', pattern), e))
74 74
75 75 state[path] = (src.strip(), rev.get(path, ''), kind)
76 76
77 77 return state
78 78
79 79 def writestate(repo, state):
80 80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
81 81 repo.wwrite('.hgsubstate',
82 82 ''.join(['%s %s\n' % (state[s][1], s)
83 83 for s in sorted(state)]), '')
84 84
85 85 def submerge(repo, wctx, mctx, actx, overwrite):
86 86 """delegated from merge.applyupdates: merging of .hgsubstate file
87 87 in working context, merging context and ancestor context"""
88 88 if mctx == actx: # backwards?
89 89 actx = wctx.p1()
90 90 s1 = wctx.substate
91 91 s2 = mctx.substate
92 92 sa = actx.substate
93 93 sm = {}
94 94
95 95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
96 96
97 97 def debug(s, msg, r=""):
98 98 if r:
99 99 r = "%s:%s:%s" % r
100 100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
101 101
102 102 for s, l in s1.items():
103 103 a = sa.get(s, nullstate)
104 104 ld = l # local state with possible dirty flag for compares
105 105 if wctx.sub(s).dirty():
106 106 ld = (l[0], l[1] + "+")
107 107 if wctx == actx: # overwrite
108 108 a = ld
109 109
110 110 if s in s2:
111 111 r = s2[s]
112 112 if ld == r or r == a: # no change or local is newer
113 113 sm[s] = l
114 114 continue
115 115 elif ld == a: # other side changed
116 116 debug(s, "other changed, get", r)
117 117 wctx.sub(s).get(r, overwrite)
118 118 sm[s] = r
119 119 elif ld[0] != r[0]: # sources differ
120 120 if repo.ui.promptchoice(
121 121 _(' subrepository sources for %s differ\n'
122 122 'use (l)ocal source (%s) or (r)emote source (%s)?')
123 123 % (s, l[0], r[0]),
124 124 (_('&Local'), _('&Remote')), 0):
125 125 debug(s, "prompt changed, get", r)
126 126 wctx.sub(s).get(r, overwrite)
127 127 sm[s] = r
128 128 elif ld[1] == a[1]: # local side is unchanged
129 129 debug(s, "other side changed, get", r)
130 130 wctx.sub(s).get(r, overwrite)
131 131 sm[s] = r
132 132 else:
133 133 debug(s, "both sides changed, merge with", r)
134 134 wctx.sub(s).merge(r)
135 135 sm[s] = l
136 136 elif ld == a: # remote removed, local unchanged
137 137 debug(s, "remote removed, remove")
138 138 wctx.sub(s).remove()
139 139 else:
140 140 if repo.ui.promptchoice(
141 141 _(' local changed subrepository %s which remote removed\n'
142 142 'use (c)hanged version or (d)elete?') % s,
143 143 (_('&Changed'), _('&Delete')), 0):
144 144 debug(s, "prompt remove")
145 145 wctx.sub(s).remove()
146 146
147 147 for s, r in s2.items():
148 148 if s in s1:
149 149 continue
150 150 elif s not in sa:
151 151 debug(s, "remote added, get", r)
152 152 mctx.sub(s).get(r)
153 153 sm[s] = r
154 154 elif r != sa[s]:
155 155 if repo.ui.promptchoice(
156 156 _(' remote changed subrepository %s which local removed\n'
157 157 'use (c)hanged version or (d)elete?') % s,
158 158 (_('&Changed'), _('&Delete')), 0) == 0:
159 159 debug(s, "prompt recreate", r)
160 160 wctx.sub(s).get(r)
161 161 sm[s] = r
162 162
163 163 # record merged .hgsubstate
164 164 writestate(repo, sm)
165 165
166 166 def reporelpath(repo):
167 167 """return path to this (sub)repo as seen from outermost repo"""
168 168 parent = repo
169 169 while hasattr(parent, '_subparent'):
170 170 parent = parent._subparent
171 171 return repo.root[len(parent.root)+1:]
172 172
173 173 def subrelpath(sub):
174 174 """return path to this subrepo as seen from outermost repo"""
175 175 if hasattr(sub, '_relpath'):
176 176 return sub._relpath
177 177 if not hasattr(sub, '_repo'):
178 178 return sub._path
179 179 return reporelpath(sub._repo)
180 180
181 181 def _abssource(repo, push=False, abort=True):
182 182 """return pull/push path of repo - either based on parent repo .hgsub info
183 183 or on the top repo config. Abort or return None if no source found."""
184 184 if hasattr(repo, '_subparent'):
185 185 source = repo._subsource
186 186 if source.startswith('/') or '://' in source:
187 187 return source
188 188 parent = _abssource(repo._subparent, push, abort=False)
189 189 if parent:
190 190 if '://' in parent:
191 191 if parent[-1] == '/':
192 192 parent = parent[:-1]
193 193 r = urlparse.urlparse(parent + '/' + source)
194 194 r = urlparse.urlunparse((r[0], r[1],
195 195 posixpath.normpath(r[2]),
196 196 r[3], r[4], r[5]))
197 197 return r
198 198 else: # plain file system path
199 199 return posixpath.normpath(os.path.join(parent, repo._subsource))
200 200 else: # recursion reached top repo
201 201 if hasattr(repo, '_subtoppath'):
202 202 return repo._subtoppath
203 203 if push and repo.ui.config('paths', 'default-push'):
204 204 return repo.ui.config('paths', 'default-push')
205 205 if repo.ui.config('paths', 'default'):
206 206 return repo.ui.config('paths', 'default')
207 207 if abort:
208 208 raise util.Abort(_("default path for subrepository %s not found") %
209 209 reporelpath(repo))
210 210
211 211 def itersubrepos(ctx1, ctx2):
212 212 """find subrepos in ctx1 or ctx2"""
213 213 # Create a (subpath, ctx) mapping where we prefer subpaths from
214 214 # ctx1. The subpaths from ctx2 are important when the .hgsub file
215 215 # has been modified (in ctx2) but not yet committed (in ctx1).
216 216 subpaths = dict.fromkeys(ctx2.substate, ctx2)
217 217 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
218 218 for subpath, ctx in sorted(subpaths.iteritems()):
219 219 yield subpath, ctx.sub(subpath)
220 220
221 221 def subrepo(ctx, path):
222 222 """return instance of the right subrepo class for subrepo in path"""
223 223 # subrepo inherently violates our import layering rules
224 224 # because it wants to make repo objects from deep inside the stack
225 225 # so we manually delay the circular imports to not break
226 226 # scripts that don't use our demand-loading
227 227 global hg
228 228 import hg as h
229 229 hg = h
230 230
231 231 util.path_auditor(ctx._repo.root)(path)
232 232 state = ctx.substate.get(path, nullstate)
233 233 if state[2] not in types:
234 234 raise util.Abort(_('unknown subrepo type %s') % state[2])
235 235 return types[state[2]](ctx, path, state[:2])
236 236
237 237 # subrepo classes need to implement the following abstract class:
238 238
239 239 class abstractsubrepo(object):
240 240
241 241 def dirty(self, ignoreupdate=False):
242 242 """returns true if the dirstate of the subrepo is dirty or does not
243 243 match current stored state. If ignoreupdate is true, only check
244 244 whether the subrepo has uncommitted changes in its dirstate.
245 245 """
246 246 raise NotImplementedError
247 247
248 248 def checknested(self, path):
249 249 """check if path is a subrepository within this repository"""
250 250 return False
251 251
252 252 def commit(self, text, user, date):
253 253 """commit the current changes to the subrepo with the given
254 254 log message. Use given user and date if possible. Return the
255 255 new state of the subrepo.
256 256 """
257 257 raise NotImplementedError
258 258
259 259 def remove(self):
260 260 """remove the subrepo
261 261
262 262 (should verify the dirstate is not dirty first)
263 263 """
264 264 raise NotImplementedError
265 265
266 266 def get(self, state, overwrite=False):
267 267 """run whatever commands are needed to put the subrepo into
268 268 this state
269 269 """
270 270 raise NotImplementedError
271 271
272 272 def merge(self, state, overwrite=False):
273 273 """merge currently-saved state with the new state."""
274 274 raise NotImplementedError
275 275
276 276 def push(self, force):
277 277 """perform whatever action is analogous to 'hg push'
278 278
279 279 This may be a no-op on some systems.
280 280 """
281 281 raise NotImplementedError
282 282
283 283 def add(self, ui, match, dryrun, prefix):
284 284 return []
285 285
286 286 def status(self, rev2, **opts):
287 287 return [], [], [], [], [], [], []
288 288
289 289 def diff(self, diffopts, node2, match, prefix, **opts):
290 290 pass
291 291
292 292 def outgoing(self, ui, dest, opts):
293 293 return 1
294 294
295 295 def incoming(self, ui, source, opts):
296 296 return 1
297 297
298 298 def files(self):
299 299 """return filename iterator"""
300 300 raise NotImplementedError
301 301
302 302 def filedata(self, name):
303 303 """return file data"""
304 304 raise NotImplementedError
305 305
306 306 def fileflags(self, name):
307 307 """return file flags"""
308 308 return ''
309 309
310 310 def archive(self, ui, archiver, prefix):
311 311 files = self.files()
312 312 total = len(files)
313 313 relpath = subrelpath(self)
314 314 ui.progress(_('archiving (%s)') % relpath, 0,
315 315 unit=_('files'), total=total)
316 316 for i, name in enumerate(files):
317 317 flags = self.fileflags(name)
318 318 mode = 'x' in flags and 0755 or 0644
319 319 symlink = 'l' in flags
320 320 archiver.addfile(os.path.join(prefix, self._path, name),
321 321 mode, symlink, self.filedata(name))
322 322 ui.progress(_('archiving (%s)') % relpath, i + 1,
323 323 unit=_('files'), total=total)
324 324 ui.progress(_('archiving (%s)') % relpath, None)
325 325
326 326
327 327 class hgsubrepo(abstractsubrepo):
328 328 def __init__(self, ctx, path, state):
329 329 self._path = path
330 330 self._state = state
331 331 r = ctx._repo
332 332 root = r.wjoin(path)
333 333 create = False
334 334 if not os.path.exists(os.path.join(root, '.hg')):
335 335 create = True
336 336 util.makedirs(root)
337 337 self._repo = hg.repository(r.ui, root, create=create)
338 338 self._repo._subparent = r
339 339 self._repo._subsource = state[0]
340 340
341 341 if create:
342 342 fp = self._repo.opener("hgrc", "w", text=True)
343 343 fp.write('[paths]\n')
344 344
345 345 def addpathconfig(key, value):
346 346 if value:
347 347 fp.write('%s = %s\n' % (key, value))
348 348 self._repo.ui.setconfig('paths', key, value)
349 349
350 350 defpath = _abssource(self._repo, abort=False)
351 351 defpushpath = _abssource(self._repo, True, abort=False)
352 352 addpathconfig('default', defpath)
353 353 if defpath != defpushpath:
354 354 addpathconfig('default-push', defpushpath)
355 355 fp.close()
356 356
357 357 def add(self, ui, match, dryrun, prefix):
358 358 return cmdutil.add(ui, self._repo, match, dryrun, True,
359 359 os.path.join(prefix, self._path))
360 360
361 361 def status(self, rev2, **opts):
362 362 try:
363 363 rev1 = self._state[1]
364 364 ctx1 = self._repo[rev1]
365 365 ctx2 = self._repo[rev2]
366 366 return self._repo.status(ctx1, ctx2, **opts)
367 367 except error.RepoLookupError, inst:
368 368 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
369 369 % (inst, subrelpath(self)))
370 370 return [], [], [], [], [], [], []
371 371
372 372 def diff(self, diffopts, node2, match, prefix, **opts):
373 373 try:
374 374 node1 = node.bin(self._state[1])
375 375 # We currently expect node2 to come from substate and be
376 376 # in hex format
377 377 if node2 is not None:
378 378 node2 = node.bin(node2)
379 379 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
380 380 node1, node2, match,
381 381 prefix=os.path.join(prefix, self._path),
382 382 listsubrepos=True, **opts)
383 383 except error.RepoLookupError, inst:
384 384 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
385 385 % (inst, subrelpath(self)))
386 386
387 387 def archive(self, ui, archiver, prefix):
388 388 abstractsubrepo.archive(self, ui, archiver, prefix)
389 389
390 390 rev = self._state[1]
391 391 ctx = self._repo[rev]
392 392 for subpath in ctx.substate:
393 393 s = subrepo(ctx, subpath)
394 394 s.archive(ui, archiver, os.path.join(prefix, self._path))
395 395
396 396 def dirty(self, ignoreupdate=False):
397 397 r = self._state[1]
398 398 if r == '' and not ignoreupdate: # no state recorded
399 399 return True
400 400 w = self._repo[None]
401 401 # version checked out changed?
402 402 if w.p1() != self._repo[r] and not ignoreupdate:
403 403 return True
404 404 return w.dirty() # working directory changed
405 405
406 406 def checknested(self, path):
407 407 return self._repo._checknested(self._repo.wjoin(path))
408 408
409 409 def commit(self, text, user, date):
410 410 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
411 411 n = self._repo.commit(text, user, date)
412 412 if not n:
413 413 return self._repo['.'].hex() # different version checked out
414 414 return node.hex(n)
415 415
416 416 def remove(self):
417 417 # we can't fully delete the repository as it may contain
418 418 # local-only history
419 419 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
420 420 hg.clean(self._repo, node.nullid, False)
421 421
422 422 def _get(self, state):
423 423 source, revision, kind = state
424 424 try:
425 425 self._repo.lookup(revision)
426 426 except error.RepoError:
427 427 self._repo._subsource = source
428 428 srcurl = _abssource(self._repo)
429 429 self._repo.ui.status(_('pulling subrepo %s from %s\n')
430 430 % (subrelpath(self), srcurl))
431 431 other = hg.repository(self._repo.ui, srcurl)
432 432 self._repo.pull(other)
433 433
434 434 def get(self, state, overwrite=False):
435 435 self._get(state)
436 436 source, revision, kind = state
437 437 self._repo.ui.debug("getting subrepo %s\n" % self._path)
438 438 hg.clean(self._repo, revision, False)
439 439
440 440 def merge(self, state):
441 441 self._get(state)
442 442 cur = self._repo['.']
443 443 dst = self._repo[state[1]]
444 444 anc = dst.ancestor(cur)
445 445 if anc == cur:
446 446 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
447 447 hg.update(self._repo, state[1])
448 448 elif anc == dst:
449 449 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
450 450 else:
451 451 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
452 452 hg.merge(self._repo, state[1], remind=False)
453 453
454 454 def push(self, force):
455 455 # push subrepos depth-first for coherent ordering
456 456 c = self._repo['']
457 457 subs = c.substate # only repos that are committed
458 458 for s in sorted(subs):
459 459 if not c.sub(s).push(force):
460 460 return False
461 461
462 462 dsturl = _abssource(self._repo, True)
463 463 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
464 464 (subrelpath(self), dsturl))
465 465 other = hg.repository(self._repo.ui, dsturl)
466 466 return self._repo.push(other, force)
467 467
468 468 def outgoing(self, ui, dest, opts):
469 469 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
470 470
471 471 def incoming(self, ui, source, opts):
472 472 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
473 473
474 474 def files(self):
475 475 rev = self._state[1]
476 476 ctx = self._repo[rev]
477 477 return ctx.manifest()
478 478
479 479 def filedata(self, name):
480 480 rev = self._state[1]
481 481 return self._repo[rev][name].data()
482 482
483 483 def fileflags(self, name):
484 484 rev = self._state[1]
485 485 ctx = self._repo[rev]
486 486 return ctx.flags(name)
487 487
488 488
489 489 class svnsubrepo(abstractsubrepo):
490 490 def __init__(self, ctx, path, state):
491 491 self._path = path
492 492 self._state = state
493 493 self._ctx = ctx
494 494 self._ui = ctx._repo.ui
495 495
496 496 def _svncommand(self, commands, filename=''):
497 497 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
498 498 cmd = ['svn'] + commands + [path]
499 499 env = dict(os.environ)
500 500 # Avoid localized output, preserve current locale for everything else.
501 501 env['LC_MESSAGES'] = 'C'
502 502 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
503 503 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
504 504 universal_newlines=True, env=env)
505 505 stdout, stderr = p.communicate()
506 506 stderr = stderr.strip()
507 507 if stderr:
508 508 raise util.Abort(stderr)
509 509 return stdout
510 510
511 511 def _wcrevs(self):
512 512 # Get the working directory revision as well as the last
513 513 # commit revision so we can compare the subrepo state with
514 514 # both. We used to store the working directory one.
515 515 output = self._svncommand(['info', '--xml'])
516 516 doc = xml.dom.minidom.parseString(output)
517 517 entries = doc.getElementsByTagName('entry')
518 518 lastrev, rev = '0', '0'
519 519 if entries:
520 520 rev = str(entries[0].getAttribute('revision')) or '0'
521 521 commits = entries[0].getElementsByTagName('commit')
522 522 if commits:
523 523 lastrev = str(commits[0].getAttribute('revision')) or '0'
524 524 return (lastrev, rev)
525 525
526 526 def _wcrev(self):
527 527 return self._wcrevs()[0]
528 528
529 529 def _wcchanged(self):
530 530 """Return (changes, extchanges) where changes is True
531 531 if the working directory was changed, and extchanges is
532 532 True if any of these changes concern an external entry.
533 533 """
534 534 output = self._svncommand(['status', '--xml'])
535 535 externals, changes = [], []
536 536 doc = xml.dom.minidom.parseString(output)
537 537 for e in doc.getElementsByTagName('entry'):
538 538 s = e.getElementsByTagName('wc-status')
539 539 if not s:
540 540 continue
541 541 item = s[0].getAttribute('item')
542 542 props = s[0].getAttribute('props')
543 543 path = e.getAttribute('path')
544 544 if item == 'external':
545 545 externals.append(path)
546 546 if (item not in ('', 'normal', 'unversioned', 'external')
547 547 or props not in ('', 'none')):
548 548 changes.append(path)
549 549 for path in changes:
550 550 for ext in externals:
551 551 if path == ext or path.startswith(ext + os.sep):
552 552 return True, True
553 553 return bool(changes), False
554 554
555 555 def dirty(self, ignoreupdate=False):
556 556 if not self._wcchanged()[0]:
557 557 if self._state[1] in self._wcrevs() or ignoreupdate:
558 558 return False
559 559 return True
560 560
561 561 def commit(self, text, user, date):
562 562 # user and date are out of our hands since svn is centralized
563 563 changed, extchanged = self._wcchanged()
564 564 if not changed:
565 565 return self._wcrev()
566 566 if extchanged:
567 567 # Do not try to commit externals
568 568 raise util.Abort(_('cannot commit svn externals'))
569 569 commitinfo = self._svncommand(['commit', '-m', text])
570 570 self._ui.status(commitinfo)
571 571 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
572 572 if not newrev:
573 573 raise util.Abort(commitinfo.splitlines()[-1])
574 574 newrev = newrev.groups()[0]
575 575 self._ui.status(self._svncommand(['update', '-r', newrev]))
576 576 return newrev
577 577
578 578 def remove(self):
579 579 if self.dirty():
580 580 self._ui.warn(_('not removing repo %s because '
581 581 'it has changes.\n' % self._path))
582 582 return
583 583 self._ui.note(_('removing subrepo %s\n') % self._path)
584 584
585 585 def onerror(function, path, excinfo):
586 586 if function is not os.remove:
587 587 raise
588 588 # read-only files cannot be unlinked under Windows
589 589 s = os.stat(path)
590 590 if (s.st_mode & stat.S_IWRITE) != 0:
591 591 raise
592 592 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
593 593 os.remove(path)
594 594
595 595 path = self._ctx._repo.wjoin(self._path)
596 596 shutil.rmtree(path, onerror=onerror)
597 597 try:
598 598 os.removedirs(os.path.dirname(path))
599 599 except OSError:
600 600 pass
601 601
602 602 def get(self, state, overwrite=False):
603 603 if overwrite:
604 604 self._svncommand(['revert', '--recursive', self._path])
605 605 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
606 606 if not re.search('Checked out revision [0-9]+.', status):
607 607 raise util.Abort(status.splitlines()[-1])
608 608 self._ui.status(status)
609 609
610 610 def merge(self, state):
611 611 old = int(self._state[1])
612 612 new = int(state[1])
613 613 if new > old:
614 614 self.get(state)
615 615
616 616 def push(self, force):
617 617 # push is a no-op for SVN
618 618 return True
619 619
620 620 def files(self):
621 621 output = self._svncommand(['list'])
622 622 # This works because svn forbids \n in filenames.
623 623 return output.splitlines()
624 624
625 625 def filedata(self, name):
626 626 return self._svncommand(['cat'], name)
627 627
628 628
629 629 class gitsubrepo(abstractsubrepo):
630 630 def __init__(self, ctx, path, state):
631 631 # TODO add git version check.
632 632 self._state = state
633 633 self._ctx = ctx
634 634 self._path = path
635 635 self._relpath = os.path.join(reporelpath(ctx._repo), path)
636 636 self._abspath = ctx._repo.wjoin(path)
637 637 self._ui = ctx._repo.ui
638 638
639 639 def _gitcommand(self, commands, env=None, stream=False):
640 640 return self._gitdir(commands, env=env, stream=stream)[0]
641 641
642 642 def _gitdir(self, commands, env=None, stream=False):
643 643 return self._gitnodir(commands, env=env, stream=stream,
644 644 cwd=self._abspath)
645 645
646 646 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
647 647 """Calls the git command
648 648
649 649 The methods tries to call the git command. versions previor to 1.6.0
650 650 are not supported and very probably fail.
651 651 """
652 652 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
653 653 # unless ui.quiet is set, print git's stderr,
654 654 # which is mostly progress and useful info
655 655 errpipe = None
656 656 if self._ui.quiet:
657 657 errpipe = open(os.devnull, 'w')
658 658 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
659 659 close_fds=util.closefds,
660 660 stdout=subprocess.PIPE, stderr=errpipe)
661 661 if stream:
662 662 return p.stdout, None
663 663
664 664 retdata = p.stdout.read().strip()
665 665 # wait for the child to exit to avoid race condition.
666 666 p.wait()
667 667
668 668 if p.returncode != 0 and p.returncode != 1:
669 669 # there are certain error codes that are ok
670 670 command = commands[0]
671 671 if command in ('cat-file', 'symbolic-ref'):
672 672 return retdata, p.returncode
673 673 # for all others, abort
674 674 raise util.Abort('git %s error %d in %s' %
675 675 (command, p.returncode, self._relpath))
676 676
677 677 return retdata, p.returncode
678 678
679 679 def _gitstate(self):
680 680 return self._gitcommand(['rev-parse', 'HEAD'])
681 681
682 682 def _gitcurrentbranch(self):
683 683 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
684 684 if err:
685 685 current = None
686 686 return current
687 687
688 688 def _githavelocally(self, revision):
689 689 out, code = self._gitdir(['cat-file', '-e', revision])
690 690 return code == 0
691 691
692 692 def _gitisancestor(self, r1, r2):
693 693 base = self._gitcommand(['merge-base', r1, r2])
694 694 return base == r1
695 695
696 696 def _gitbranchmap(self):
697 697 '''returns 2 things:
698 698 a map from git branch to revision
699 699 a map from revision to branches'''
700 700 branch2rev = {}
701 701 rev2branch = {}
702 702
703 703 out = self._gitcommand(['for-each-ref', '--format',
704 704 '%(objectname) %(refname)'])
705 705 for line in out.split('\n'):
706 706 revision, ref = line.split(' ')
707 707 if ref.startswith('refs/tags/'):
708 708 continue
709 709 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
710 710 continue # ignore remote/HEAD redirects
711 711 branch2rev[ref] = revision
712 712 rev2branch.setdefault(revision, []).append(ref)
713 713 return branch2rev, rev2branch
714 714
715 715 def _gittracking(self, branches):
716 716 'return map of remote branch to local tracking branch'
717 717 # assumes no more than one local tracking branch for each remote
718 718 tracking = {}
719 719 for b in branches:
720 720 if b.startswith('refs/remotes/'):
721 721 continue
722 722 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
723 723 if remote:
724 724 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
725 725 tracking['refs/remotes/%s/%s' %
726 726 (remote, ref.split('/', 2)[2])] = b
727 727 return tracking
728 728
729 729 def _fetch(self, source, revision):
730 730 if not os.path.exists(os.path.join(self._abspath, '.git')):
731 731 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
732 732 self._gitnodir(['clone', source, self._abspath])
733 733 if self._githavelocally(revision):
734 734 return
735 735 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
736 736 # first try from origin
737 737 self._gitcommand(['fetch'])
738 738 if self._githavelocally(revision):
739 739 return
740 740 # then try from known subrepo source
741 741 self._gitcommand(['fetch', source])
742 742 if not self._githavelocally(revision):
743 743 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
744 744 (revision, self._relpath))
745 745
746 746 def dirty(self, ignoreupdate=False):
747 747 # version checked out changed?
748 748 if not ignoreupdate and self._state[1] != self._gitstate():
749 749 return True
750 750 # check for staged changes or modified files; ignore untracked files
751 751 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
752 752 return code == 1
753 753
754 754 def get(self, state, overwrite=False):
755 755 source, revision, kind = state
756 756 self._fetch(source, revision)
757 757 # if the repo was set to be bare, unbare it
758 758 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
759 759 self._gitcommand(['config', 'core.bare', 'false'])
760 760 if self._gitstate() == revision:
761 761 self._gitcommand(['reset', '--hard', 'HEAD'])
762 762 return
763 763 elif self._gitstate() == revision:
764 if overwrite:
765 # first reset the index to unmark new files for commit, because
766 # reset --hard will otherwise throw away files added for commit,
767 # not just unmark them.
768 self._gitcommand(['reset', 'HEAD'])
769 self._gitcommand(['reset', '--hard', 'HEAD'])
764 770 return
765 771 branch2rev, rev2branch = self._gitbranchmap()
766 772
773 def checkout(args):
774 cmd = ['checkout']
775 if overwrite:
776 # first reset the index to unmark new files for commit, because
777 # the -f option will otherwise throw away files added for
778 # commit, not just unmark them.
779 self._gitcommand(['reset', 'HEAD'])
780 cmd.append('-f')
781 self._gitcommand(cmd + args)
782
767 783 def rawcheckout():
768 784 # no branch to checkout, check it out with no branch
769 785 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
770 786 self._relpath)
771 787 self._ui.warn(_('check out a git branch if you intend '
772 788 'to make changes\n'))
773 self._gitcommand(['checkout', '-q', revision])
789 checkout(['-q', revision])
774 790
775 791 if revision not in rev2branch:
776 792 rawcheckout()
777 793 return
778 794 branches = rev2branch[revision]
779 795 firstlocalbranch = None
780 796 for b in branches:
781 797 if b == 'refs/heads/master':
782 798 # master trumps all other branches
783 self._gitcommand(['checkout', 'refs/heads/master'])
799 checkout(['refs/heads/master'])
784 800 return
785 801 if not firstlocalbranch and not b.startswith('refs/remotes/'):
786 802 firstlocalbranch = b
787 803 if firstlocalbranch:
788 self._gitcommand(['checkout', firstlocalbranch])
804 checkout([firstlocalbranch])
789 805 return
790 806
791 807 tracking = self._gittracking(branch2rev.keys())
792 808 # choose a remote branch already tracked if possible
793 809 remote = branches[0]
794 810 if remote not in tracking:
795 811 for b in branches:
796 812 if b in tracking:
797 813 remote = b
798 814 break
799 815
800 816 if remote not in tracking:
801 817 # create a new local tracking branch
802 818 local = remote.split('/', 2)[2]
803 self._gitcommand(['checkout', '-b', local, remote])
819 checkout(['-b', local, remote])
804 820 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
805 821 # When updating to a tracked remote branch,
806 822 # if the local tracking branch is downstream of it,
807 823 # a normal `git pull` would have performed a "fast-forward merge"
808 824 # which is equivalent to updating the local branch to the remote.
809 825 # Since we are only looking at branching at update, we need to
810 826 # detect this situation and perform this action lazily.
811 827 if tracking[remote] != self._gitcurrentbranch():
812 self._gitcommand(['checkout', tracking[remote]])
828 checkout([tracking[remote]])
813 829 self._gitcommand(['merge', '--ff', remote])
814 830 else:
815 831 # a real merge would be required, just checkout the revision
816 832 rawcheckout()
817 833
818 834 def commit(self, text, user, date):
819 835 cmd = ['commit', '-a', '-m', text]
820 836 env = os.environ.copy()
821 837 if user:
822 838 cmd += ['--author', user]
823 839 if date:
824 840 # git's date parser silently ignores when seconds < 1e9
825 841 # convert to ISO8601
826 842 env['GIT_AUTHOR_DATE'] = util.datestr(date,
827 843 '%Y-%m-%dT%H:%M:%S %1%2')
828 844 self._gitcommand(cmd, env=env)
829 845 # make sure commit works otherwise HEAD might not exist under certain
830 846 # circumstances
831 847 return self._gitstate()
832 848
833 849 def merge(self, state):
834 850 source, revision, kind = state
835 851 self._fetch(source, revision)
836 852 base = self._gitcommand(['merge-base', revision, self._state[1]])
837 853 if base == revision:
838 854 self.get(state) # fast forward merge
839 855 elif base != self._state[1]:
840 856 self._gitcommand(['merge', '--no-commit', revision])
841 857
842 858 def push(self, force):
843 859 # if a branch in origin contains the revision, nothing to do
844 860 branch2rev, rev2branch = self._gitbranchmap()
845 861 if self._state[1] in rev2branch:
846 862 for b in rev2branch[self._state[1]]:
847 863 if b.startswith('refs/remotes/origin/'):
848 864 return True
849 865 for b, revision in branch2rev.iteritems():
850 866 if b.startswith('refs/remotes/origin/'):
851 867 if self._gitisancestor(self._state[1], revision):
852 868 return True
853 869 # otherwise, try to push the currently checked out branch
854 870 cmd = ['push']
855 871 if force:
856 872 cmd.append('--force')
857 873
858 874 current = self._gitcurrentbranch()
859 875 if current:
860 876 # determine if the current branch is even useful
861 877 if not self._gitisancestor(self._state[1], current):
862 878 self._ui.warn(_('unrelated git branch checked out '
863 879 'in subrepo %s\n') % self._relpath)
864 880 return False
865 881 self._ui.status(_('pushing branch %s of subrepo %s\n') %
866 882 (current.split('/', 2)[2], self._relpath))
867 883 self._gitcommand(cmd + ['origin', current])
868 884 return True
869 885 else:
870 886 self._ui.warn(_('no branch checked out in subrepo %s\n'
871 887 'cannot push revision %s') %
872 888 (self._relpath, self._state[1]))
873 889 return False
874 890
875 891 def remove(self):
876 892 if self.dirty():
877 893 self._ui.warn(_('not removing repo %s because '
878 894 'it has changes.\n') % self._relpath)
879 895 return
880 896 # we can't fully delete the repository as it may contain
881 897 # local-only history
882 898 self._ui.note(_('removing subrepo %s\n') % self._relpath)
883 899 self._gitcommand(['config', 'core.bare', 'true'])
884 900 for f in os.listdir(self._abspath):
885 901 if f == '.git':
886 902 continue
887 903 path = os.path.join(self._abspath, f)
888 904 if os.path.isdir(path) and not os.path.islink(path):
889 905 shutil.rmtree(path)
890 906 else:
891 907 os.remove(path)
892 908
893 909 def archive(self, ui, archiver, prefix):
894 910 source, revision = self._state
895 911 self._fetch(source, revision)
896 912
897 913 # Parse git's native archive command.
898 914 # This should be much faster than manually traversing the trees
899 915 # and objects with many subprocess calls.
900 916 tarstream = self._gitcommand(['archive', revision], stream=True)
901 917 tar = tarfile.open(fileobj=tarstream, mode='r|')
902 918 relpath = subrelpath(self)
903 919 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
904 920 for i, info in enumerate(tar):
905 921 if info.isdir():
906 922 continue
907 923 if info.issym():
908 924 data = info.linkname
909 925 else:
910 926 data = tar.extractfile(info).read()
911 927 archiver.addfile(os.path.join(prefix, self._path, info.name),
912 928 info.mode, info.issym(), data)
913 929 ui.progress(_('archiving (%s)') % relpath, i + 1,
914 930 unit=_('files'))
915 931 ui.progress(_('archiving (%s)') % relpath, None)
916 932
917 933
918 934 def status(self, rev2, **opts):
919 935 rev1 = self._state[1]
920 936 modified, added, removed = [], [], []
921 937 if rev2:
922 938 command = ['diff-tree', rev1, rev2]
923 939 else:
924 940 command = ['diff-index', rev1]
925 941 out = self._gitcommand(command)
926 942 for line in out.split('\n'):
927 943 tab = line.find('\t')
928 944 if tab == -1:
929 945 continue
930 946 status, f = line[tab - 1], line[tab + 1:]
931 947 if status == 'M':
932 948 modified.append(f)
933 949 elif status == 'A':
934 950 added.append(f)
935 951 elif status == 'D':
936 952 removed.append(f)
937 953
938 954 deleted = unknown = ignored = clean = []
939 955 return modified, added, removed, deleted, unknown, ignored, clean
940 956
941 957 types = {
942 958 'hg': hgsubrepo,
943 959 'svn': svnsubrepo,
944 960 'git': gitsubrepo,
945 961 }
@@ -1,306 +1,324 b''
1 1 $ "$TESTDIR/hghave" git || exit 80
2 2
3 3 make git commits repeatable
4 4
5 5 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
6 6 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
7 7 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
8 8 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
9 9 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
10 10 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
11 11
12 12 root hg repo
13 13
14 14 $ hg init t
15 15 $ cd t
16 16 $ echo a > a
17 17 $ hg add a
18 18 $ hg commit -m a
19 19 $ cd ..
20 20
21 21 new external git repo
22 22
23 23 $ mkdir gitroot
24 24 $ cd gitroot
25 25 $ git init -q
26 26 $ echo g > g
27 27 $ git add g
28 28 $ git commit -q -m g
29 29
30 30 add subrepo clone
31 31
32 32 $ cd ../t
33 33 $ echo 's = [git]../gitroot' > .hgsub
34 34 $ git clone -q ../gitroot s
35 35 $ hg add .hgsub
36 36 $ hg commit -m 'new git subrepo'
37 37 committing subrepository s
38 38 $ hg debugsub
39 39 path s
40 40 source ../gitroot
41 41 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
42 42
43 43 record a new commit from upstream from a different branch
44 44
45 45 $ cd ../gitroot
46 46 $ git checkout -q -b testing
47 47 $ echo gg >> g
48 48 $ git commit -q -a -m gg
49 49
50 50 $ cd ../t/s
51 51 $ git pull -q >/dev/null 2>/dev/null
52 52 $ git checkout -q -b testing origin/testing >/dev/null
53 53
54 54 $ cd ..
55 55 $ hg status --subrepos
56 56 M s/g
57 57 $ hg commit -m 'update git subrepo'
58 58 committing subrepository s
59 59 $ hg debugsub
60 60 path s
61 61 source ../gitroot
62 62 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
63 63
64 64 make $GITROOT pushable, by replacing it with a clone with nothing checked out
65 65
66 66 $ cd ..
67 67 $ git clone gitroot gitrootbare --bare -q
68 68 $ rm -rf gitroot
69 69 $ mv gitrootbare gitroot
70 70
71 71 clone root
72 72
73 73 $ cd t
74 74 $ hg clone . ../tc
75 75 updating to branch default
76 76 cloning subrepo s
77 77 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 78 $ cd ../tc
79 79 $ hg debugsub
80 80 path s
81 81 source ../gitroot
82 82 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
83 83
84 84 update to previous substate
85 85
86 86 $ hg update 1 -q
87 87 $ cat s/g
88 88 g
89 89 $ hg debugsub
90 90 path s
91 91 source ../gitroot
92 92 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
93 93
94 94 clone root, make local change
95 95
96 96 $ cd ../t
97 97 $ hg clone . ../ta
98 98 updating to branch default
99 99 cloning subrepo s
100 100 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
101 101
102 102 $ cd ../ta
103 103 $ echo ggg >> s/g
104 104 $ hg status --subrepos
105 105 M s/g
106 106 $ hg commit -m ggg
107 107 committing subrepository s
108 108 $ hg debugsub
109 109 path s
110 110 source ../gitroot
111 111 revision 79695940086840c99328513acbe35f90fcd55e57
112 112
113 113 clone root separately, make different local change
114 114
115 115 $ cd ../t
116 116 $ hg clone . ../tb
117 117 updating to branch default
118 118 cloning subrepo s
119 119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
120 120
121 121 $ cd ../tb/s
122 122 $ echo f > f
123 123 $ git add f
124 124 $ cd ..
125 125
126 126 $ hg status --subrepos
127 127 A s/f
128 128 $ hg commit -m f
129 129 committing subrepository s
130 130 $ hg debugsub
131 131 path s
132 132 source ../gitroot
133 133 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
134 134
135 135 user b push changes
136 136
137 137 $ hg push 2>/dev/null
138 138 pushing to $TESTTMP/t
139 139 pushing branch testing of subrepo s
140 140 searching for changes
141 141 adding changesets
142 142 adding manifests
143 143 adding file changes
144 144 added 1 changesets with 1 changes to 1 files
145 145
146 146 user a pulls, merges, commits
147 147
148 148 $ cd ../ta
149 149 $ hg pull
150 150 pulling from $TESTTMP/t
151 151 searching for changes
152 152 adding changesets
153 153 adding manifests
154 154 adding file changes
155 155 added 1 changesets with 1 changes to 1 files (+1 heads)
156 156 (run 'hg heads' to see heads, 'hg merge' to merge)
157 157 $ hg merge 2>/dev/null
158 158 pulling subrepo s
159 159 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
160 160 (branch merge, don't forget to commit)
161 161 $ cat s/f
162 162 f
163 163 $ cat s/g
164 164 g
165 165 gg
166 166 ggg
167 167 $ hg commit -m 'merge'
168 168 committing subrepository s
169 169 $ hg status --subrepos --rev 1:5
170 170 M .hgsubstate
171 171 M s/g
172 172 A s/f
173 173 $ hg debugsub
174 174 path s
175 175 source ../gitroot
176 176 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
177 177 $ hg push 2>/dev/null
178 178 pushing to $TESTTMP/t
179 179 pushing branch testing of subrepo s
180 180 searching for changes
181 181 adding changesets
182 182 adding manifests
183 183 adding file changes
184 184 added 2 changesets with 2 changes to 1 files
185 185
186 186 make upstream git changes
187 187
188 188 $ cd ..
189 189 $ git clone -q gitroot gitclone
190 190 $ cd gitclone
191 191 $ echo ff >> f
192 192 $ git commit -q -a -m ff
193 193 $ echo fff >> f
194 194 $ git commit -q -a -m fff
195 195 $ git push origin testing 2>/dev/null
196 196
197 197 make and push changes to hg without updating the subrepo
198 198
199 199 $ cd ../t
200 200 $ hg clone . ../td
201 201 updating to branch default
202 202 cloning subrepo s
203 203 checking out detached HEAD in subrepo s
204 204 check out a git branch if you intend to make changes
205 205 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
206 206 $ cd ../td
207 207 $ echo aa >> a
208 208 $ hg commit -m aa
209 209 $ hg push
210 210 pushing to $TESTTMP/t
211 211 searching for changes
212 212 adding changesets
213 213 adding manifests
214 214 adding file changes
215 215 added 1 changesets with 1 changes to 1 files
216 216
217 217 sync to upstream git, distribute changes
218 218
219 219 $ cd ../ta
220 220 $ hg pull -u -q
221 221 $ cd s
222 222 $ git pull -q >/dev/null 2>/dev/null
223 223 $ cd ..
224 224 $ hg commit -m 'git upstream sync'
225 225 committing subrepository s
226 226 $ hg debugsub
227 227 path s
228 228 source ../gitroot
229 229 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
230 230 $ hg push -q
231 231
232 232 $ cd ../tb
233 233 $ hg pull -q
234 234 $ hg update 2>/dev/null
235 235 pulling subrepo s
236 236 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 237 $ hg debugsub
238 238 path s
239 239 source ../gitroot
240 240 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
241 241
242 242 update to a revision without the subrepo, keeping the local git repository
243 243
244 244 $ cd ../t
245 245 $ hg up 0
246 246 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
247 247 $ ls -a s
248 248 .
249 249 ..
250 250 .git
251 251
252 252 $ hg up 2
253 253 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 254 $ ls -a s
255 255 .
256 256 ..
257 257 .git
258 258 g
259 259
260 260 archive subrepos
261 261
262 262 $ cd ../tc
263 263 $ hg pull -q
264 264 $ hg archive --subrepos -r 5 ../archive 2>/dev/null
265 265 pulling subrepo s
266 266 $ cd ../archive
267 267 $ cat s/f
268 268 f
269 269 $ cat s/g
270 270 g
271 271 gg
272 272 ggg
273 273
274 274 create nested repo
275 275
276 276 $ cd ..
277 277 $ hg init outer
278 278 $ cd outer
279 279 $ echo b>b
280 280 $ hg add b
281 281 $ hg commit -m b
282 282
283 283 $ hg clone ../t inner
284 284 updating to branch default
285 285 cloning subrepo s
286 286 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 287 $ echo inner = inner > .hgsub
288 288 $ hg add .hgsub
289 289 $ hg commit -m 'nested sub'
290 290 committing subrepository inner
291 291
292 292 nested commit
293 293
294 294 $ echo ffff >> inner/s/f
295 295 $ hg status --subrepos
296 296 M inner/s/f
297 297 $ hg commit -m nested
298 298 committing subrepository inner
299 299 committing subrepository inner/s
300 300
301 301 nested archive
302 302
303 303 $ hg archive --subrepos ../narchive
304 304 $ ls ../narchive/inner/s | grep -v pax_global_header
305 305 f
306 306 g
307
308 Check hg update --clean
309 $ cd $TESTTMP/t
310 $ echo > s/g
311 $ cd s
312 $ echo c1 > f1
313 $ echo c1 > f2
314 $ git add f1
315 $ git status --short
316 A f1
317 M g
318 ?? f2
319 $ cd ..
320 $ hg update -C > /dev/null 2>/dev/null
321 $ cd s
322 $ git status --short
323 ?? f1
324 ?? f2
General Comments 0
You need to be logged in to leave comments. Login now