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