##// END OF EJS Templates
subrepo: make stdin for svn a pipe for non-interactive use (issue2759)...
Augie Fackler -
r14506:733af5d9 1.8.4 stable
parent child Browse files
Show More
@@ -1,1056 +1,1060 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 # Starting in svn 1.5 --non-interactive is a global flag
528 # instead of being per-command, but we need to support 1.4 so
529 # we have to be intelligent about what commands take
530 # --non-interactive.
531 if (not self._ui.interactive() and
532 commands[0] in ('update', 'checkout', 'commit')):
533 cmd.append('--non-interactive')
527 extrakw = {}
528 if not self._ui.interactive():
529 # Making stdin be a pipe should prevent svn from behaving
530 # interactively even if we can't pass --non-interactive.
531 extrakw['stdin'] = subprocess.PIPE
532 # Starting in svn 1.5 --non-interactive is a global flag
533 # instead of being per-command, but we need to support 1.4 so
534 # we have to be intelligent about what commands take
535 # --non-interactive.
536 if commands[0] in ('update', 'checkout', 'commit'):
537 cmd.append('--non-interactive')
534 538 cmd.extend(commands)
535 539 if filename is not None:
536 540 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
537 541 cmd.append(path)
538 542 env = dict(os.environ)
539 543 # Avoid localized output, preserve current locale for everything else.
540 544 env['LC_MESSAGES'] = 'C'
541 545 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
542 546 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
543 universal_newlines=True, env=env)
547 universal_newlines=True, env=env, **extrakw)
544 548 stdout, stderr = p.communicate()
545 549 stderr = stderr.strip()
546 550 if p.returncode:
547 551 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
548 552 if stderr:
549 553 self._ui.warn(stderr + '\n')
550 554 return stdout
551 555
552 556 @propertycache
553 557 def _svnversion(self):
554 558 output = self._svncommand(['--version'], filename=None)
555 559 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
556 560 if not m:
557 561 raise util.Abort(_('cannot retrieve svn tool version'))
558 562 return (int(m.group(1)), int(m.group(2)))
559 563
560 564 def _wcrevs(self):
561 565 # Get the working directory revision as well as the last
562 566 # commit revision so we can compare the subrepo state with
563 567 # both. We used to store the working directory one.
564 568 output = self._svncommand(['info', '--xml'])
565 569 doc = xml.dom.minidom.parseString(output)
566 570 entries = doc.getElementsByTagName('entry')
567 571 lastrev, rev = '0', '0'
568 572 if entries:
569 573 rev = str(entries[0].getAttribute('revision')) or '0'
570 574 commits = entries[0].getElementsByTagName('commit')
571 575 if commits:
572 576 lastrev = str(commits[0].getAttribute('revision')) or '0'
573 577 return (lastrev, rev)
574 578
575 579 def _wcrev(self):
576 580 return self._wcrevs()[0]
577 581
578 582 def _wcchanged(self):
579 583 """Return (changes, extchanges) where changes is True
580 584 if the working directory was changed, and extchanges is
581 585 True if any of these changes concern an external entry.
582 586 """
583 587 output = self._svncommand(['status', '--xml'])
584 588 externals, changes = [], []
585 589 doc = xml.dom.minidom.parseString(output)
586 590 for e in doc.getElementsByTagName('entry'):
587 591 s = e.getElementsByTagName('wc-status')
588 592 if not s:
589 593 continue
590 594 item = s[0].getAttribute('item')
591 595 props = s[0].getAttribute('props')
592 596 path = e.getAttribute('path')
593 597 if item == 'external':
594 598 externals.append(path)
595 599 if (item not in ('', 'normal', 'unversioned', 'external')
596 600 or props not in ('', 'none')):
597 601 changes.append(path)
598 602 for path in changes:
599 603 for ext in externals:
600 604 if path == ext or path.startswith(ext + os.sep):
601 605 return True, True
602 606 return bool(changes), False
603 607
604 608 def dirty(self, ignoreupdate=False):
605 609 if not self._wcchanged()[0]:
606 610 if self._state[1] in self._wcrevs() or ignoreupdate:
607 611 return False
608 612 return True
609 613
610 614 def commit(self, text, user, date):
611 615 # user and date are out of our hands since svn is centralized
612 616 changed, extchanged = self._wcchanged()
613 617 if not changed:
614 618 return self._wcrev()
615 619 if extchanged:
616 620 # Do not try to commit externals
617 621 raise util.Abort(_('cannot commit svn externals'))
618 622 commitinfo = self._svncommand(['commit', '-m', text])
619 623 self._ui.status(commitinfo)
620 624 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
621 625 if not newrev:
622 626 raise util.Abort(commitinfo.splitlines()[-1])
623 627 newrev = newrev.groups()[0]
624 628 self._ui.status(self._svncommand(['update', '-r', newrev]))
625 629 return newrev
626 630
627 631 def remove(self):
628 632 if self.dirty():
629 633 self._ui.warn(_('not removing repo %s because '
630 634 'it has changes.\n' % self._path))
631 635 return
632 636 self._ui.note(_('removing subrepo %s\n') % self._path)
633 637
634 638 def onerror(function, path, excinfo):
635 639 if function is not os.remove:
636 640 raise
637 641 # read-only files cannot be unlinked under Windows
638 642 s = os.stat(path)
639 643 if (s.st_mode & stat.S_IWRITE) != 0:
640 644 raise
641 645 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
642 646 os.remove(path)
643 647
644 648 path = self._ctx._repo.wjoin(self._path)
645 649 shutil.rmtree(path, onerror=onerror)
646 650 try:
647 651 os.removedirs(os.path.dirname(path))
648 652 except OSError:
649 653 pass
650 654
651 655 def get(self, state, overwrite=False):
652 656 if overwrite:
653 657 self._svncommand(['revert', '--recursive'])
654 658 args = ['checkout']
655 659 if self._svnversion >= (1, 5):
656 660 args.append('--force')
657 661 args.extend([state[0], '--revision', state[1]])
658 662 status = self._svncommand(args)
659 663 if not re.search('Checked out revision [0-9]+.', status):
660 664 raise util.Abort(status.splitlines()[-1])
661 665 self._ui.status(status)
662 666
663 667 def merge(self, state):
664 668 old = self._state[1]
665 669 new = state[1]
666 670 if new != self._wcrev():
667 671 dirty = old == self._wcrev() or self._wcchanged()[0]
668 672 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
669 673 self.get(state, False)
670 674
671 675 def push(self, force):
672 676 # push is a no-op for SVN
673 677 return True
674 678
675 679 def files(self):
676 680 output = self._svncommand(['list'])
677 681 # This works because svn forbids \n in filenames.
678 682 return output.splitlines()
679 683
680 684 def filedata(self, name):
681 685 return self._svncommand(['cat'], name)
682 686
683 687
684 688 class gitsubrepo(abstractsubrepo):
685 689 def __init__(self, ctx, path, state):
686 690 # TODO add git version check.
687 691 self._state = state
688 692 self._ctx = ctx
689 693 self._path = path
690 694 self._relpath = os.path.join(reporelpath(ctx._repo), path)
691 695 self._abspath = ctx._repo.wjoin(path)
692 696 self._subparent = ctx._repo
693 697 self._ui = ctx._repo.ui
694 698
695 699 def _gitcommand(self, commands, env=None, stream=False):
696 700 return self._gitdir(commands, env=env, stream=stream)[0]
697 701
698 702 def _gitdir(self, commands, env=None, stream=False):
699 703 return self._gitnodir(commands, env=env, stream=stream,
700 704 cwd=self._abspath)
701 705
702 706 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
703 707 """Calls the git command
704 708
705 709 The methods tries to call the git command. versions previor to 1.6.0
706 710 are not supported and very probably fail.
707 711 """
708 712 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
709 713 # unless ui.quiet is set, print git's stderr,
710 714 # which is mostly progress and useful info
711 715 errpipe = None
712 716 if self._ui.quiet:
713 717 errpipe = open(os.devnull, 'w')
714 718 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
715 719 close_fds=util.closefds,
716 720 stdout=subprocess.PIPE, stderr=errpipe)
717 721 if stream:
718 722 return p.stdout, None
719 723
720 724 retdata = p.stdout.read().strip()
721 725 # wait for the child to exit to avoid race condition.
722 726 p.wait()
723 727
724 728 if p.returncode != 0 and p.returncode != 1:
725 729 # there are certain error codes that are ok
726 730 command = commands[0]
727 731 if command in ('cat-file', 'symbolic-ref'):
728 732 return retdata, p.returncode
729 733 # for all others, abort
730 734 raise util.Abort('git %s error %d in %s' %
731 735 (command, p.returncode, self._relpath))
732 736
733 737 return retdata, p.returncode
734 738
735 739 def _gitmissing(self):
736 740 return not os.path.exists(os.path.join(self._abspath, '.git'))
737 741
738 742 def _gitstate(self):
739 743 return self._gitcommand(['rev-parse', 'HEAD'])
740 744
741 745 def _gitcurrentbranch(self):
742 746 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
743 747 if err:
744 748 current = None
745 749 return current
746 750
747 751 def _githavelocally(self, revision):
748 752 out, code = self._gitdir(['cat-file', '-e', revision])
749 753 return code == 0
750 754
751 755 def _gitisancestor(self, r1, r2):
752 756 base = self._gitcommand(['merge-base', r1, r2])
753 757 return base == r1
754 758
755 759 def _gitbranchmap(self):
756 760 '''returns 2 things:
757 761 a map from git branch to revision
758 762 a map from revision to branches'''
759 763 branch2rev = {}
760 764 rev2branch = {}
761 765
762 766 out = self._gitcommand(['for-each-ref', '--format',
763 767 '%(objectname) %(refname)'])
764 768 for line in out.split('\n'):
765 769 revision, ref = line.split(' ')
766 770 if (not ref.startswith('refs/heads/') and
767 771 not ref.startswith('refs/remotes/')):
768 772 continue
769 773 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
770 774 continue # ignore remote/HEAD redirects
771 775 branch2rev[ref] = revision
772 776 rev2branch.setdefault(revision, []).append(ref)
773 777 return branch2rev, rev2branch
774 778
775 779 def _gittracking(self, branches):
776 780 'return map of remote branch to local tracking branch'
777 781 # assumes no more than one local tracking branch for each remote
778 782 tracking = {}
779 783 for b in branches:
780 784 if b.startswith('refs/remotes/'):
781 785 continue
782 786 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
783 787 if remote:
784 788 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
785 789 tracking['refs/remotes/%s/%s' %
786 790 (remote, ref.split('/', 2)[2])] = b
787 791 return tracking
788 792
789 793 def _abssource(self, source):
790 794 if '://' not in source:
791 795 # recognize the scp syntax as an absolute source
792 796 colon = source.find(':')
793 797 if colon != -1 and '/' not in source[:colon]:
794 798 return source
795 799 self._subsource = source
796 800 return _abssource(self)
797 801
798 802 def _fetch(self, source, revision):
799 803 if self._gitmissing():
800 804 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
801 805 self._gitnodir(['clone', self._abssource(source), self._abspath])
802 806 if self._githavelocally(revision):
803 807 return
804 808 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
805 809 # try only origin: the originally cloned repo
806 810 self._gitcommand(['fetch'])
807 811 if not self._githavelocally(revision):
808 812 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
809 813 (revision, self._relpath))
810 814
811 815 def dirty(self, ignoreupdate=False):
812 816 if self._gitmissing():
813 817 return self._state[1] != ''
814 818 if not ignoreupdate and self._state[1] != self._gitstate():
815 819 # different version checked out
816 820 return True
817 821 # check for staged changes or modified files; ignore untracked files
818 822 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
819 823 return code == 1
820 824
821 825 def get(self, state, overwrite=False):
822 826 source, revision, kind = state
823 827 if not revision:
824 828 self.remove()
825 829 return
826 830 self._fetch(source, revision)
827 831 # if the repo was set to be bare, unbare it
828 832 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
829 833 self._gitcommand(['config', 'core.bare', 'false'])
830 834 if self._gitstate() == revision:
831 835 self._gitcommand(['reset', '--hard', 'HEAD'])
832 836 return
833 837 elif self._gitstate() == revision:
834 838 if overwrite:
835 839 # first reset the index to unmark new files for commit, because
836 840 # reset --hard will otherwise throw away files added for commit,
837 841 # not just unmark them.
838 842 self._gitcommand(['reset', 'HEAD'])
839 843 self._gitcommand(['reset', '--hard', 'HEAD'])
840 844 return
841 845 branch2rev, rev2branch = self._gitbranchmap()
842 846
843 847 def checkout(args):
844 848 cmd = ['checkout']
845 849 if overwrite:
846 850 # first reset the index to unmark new files for commit, because
847 851 # the -f option will otherwise throw away files added for
848 852 # commit, not just unmark them.
849 853 self._gitcommand(['reset', 'HEAD'])
850 854 cmd.append('-f')
851 855 self._gitcommand(cmd + args)
852 856
853 857 def rawcheckout():
854 858 # no branch to checkout, check it out with no branch
855 859 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
856 860 self._relpath)
857 861 self._ui.warn(_('check out a git branch if you intend '
858 862 'to make changes\n'))
859 863 checkout(['-q', revision])
860 864
861 865 if revision not in rev2branch:
862 866 rawcheckout()
863 867 return
864 868 branches = rev2branch[revision]
865 869 firstlocalbranch = None
866 870 for b in branches:
867 871 if b == 'refs/heads/master':
868 872 # master trumps all other branches
869 873 checkout(['refs/heads/master'])
870 874 return
871 875 if not firstlocalbranch and not b.startswith('refs/remotes/'):
872 876 firstlocalbranch = b
873 877 if firstlocalbranch:
874 878 checkout([firstlocalbranch])
875 879 return
876 880
877 881 tracking = self._gittracking(branch2rev.keys())
878 882 # choose a remote branch already tracked if possible
879 883 remote = branches[0]
880 884 if remote not in tracking:
881 885 for b in branches:
882 886 if b in tracking:
883 887 remote = b
884 888 break
885 889
886 890 if remote not in tracking:
887 891 # create a new local tracking branch
888 892 local = remote.split('/', 2)[2]
889 893 checkout(['-b', local, remote])
890 894 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
891 895 # When updating to a tracked remote branch,
892 896 # if the local tracking branch is downstream of it,
893 897 # a normal `git pull` would have performed a "fast-forward merge"
894 898 # which is equivalent to updating the local branch to the remote.
895 899 # Since we are only looking at branching at update, we need to
896 900 # detect this situation and perform this action lazily.
897 901 if tracking[remote] != self._gitcurrentbranch():
898 902 checkout([tracking[remote]])
899 903 self._gitcommand(['merge', '--ff', remote])
900 904 else:
901 905 # a real merge would be required, just checkout the revision
902 906 rawcheckout()
903 907
904 908 def commit(self, text, user, date):
905 909 if self._gitmissing():
906 910 raise util.Abort(_("subrepo %s is missing") % self._relpath)
907 911 cmd = ['commit', '-a', '-m', text]
908 912 env = os.environ.copy()
909 913 if user:
910 914 cmd += ['--author', user]
911 915 if date:
912 916 # git's date parser silently ignores when seconds < 1e9
913 917 # convert to ISO8601
914 918 env['GIT_AUTHOR_DATE'] = util.datestr(date,
915 919 '%Y-%m-%dT%H:%M:%S %1%2')
916 920 self._gitcommand(cmd, env=env)
917 921 # make sure commit works otherwise HEAD might not exist under certain
918 922 # circumstances
919 923 return self._gitstate()
920 924
921 925 def merge(self, state):
922 926 source, revision, kind = state
923 927 self._fetch(source, revision)
924 928 base = self._gitcommand(['merge-base', revision, self._state[1]])
925 929 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
926 930
927 931 def mergefunc():
928 932 if base == revision:
929 933 self.get(state) # fast forward merge
930 934 elif base != self._state[1]:
931 935 self._gitcommand(['merge', '--no-commit', revision])
932 936
933 937 if self.dirty():
934 938 if self._gitstate() != revision:
935 939 dirty = self._gitstate() == self._state[1] or code != 0
936 940 if _updateprompt(self._ui, self, dirty,
937 941 self._state[1][:7], revision[:7]):
938 942 mergefunc()
939 943 else:
940 944 mergefunc()
941 945
942 946 def push(self, force):
943 947 if not self._state[1]:
944 948 return True
945 949 if self._gitmissing():
946 950 raise util.Abort(_("subrepo %s is missing") % self._relpath)
947 951 # if a branch in origin contains the revision, nothing to do
948 952 branch2rev, rev2branch = self._gitbranchmap()
949 953 if self._state[1] in rev2branch:
950 954 for b in rev2branch[self._state[1]]:
951 955 if b.startswith('refs/remotes/origin/'):
952 956 return True
953 957 for b, revision in branch2rev.iteritems():
954 958 if b.startswith('refs/remotes/origin/'):
955 959 if self._gitisancestor(self._state[1], revision):
956 960 return True
957 961 # otherwise, try to push the currently checked out branch
958 962 cmd = ['push']
959 963 if force:
960 964 cmd.append('--force')
961 965
962 966 current = self._gitcurrentbranch()
963 967 if current:
964 968 # determine if the current branch is even useful
965 969 if not self._gitisancestor(self._state[1], current):
966 970 self._ui.warn(_('unrelated git branch checked out '
967 971 'in subrepo %s\n') % self._relpath)
968 972 return False
969 973 self._ui.status(_('pushing branch %s of subrepo %s\n') %
970 974 (current.split('/', 2)[2], self._relpath))
971 975 self._gitcommand(cmd + ['origin', current])
972 976 return True
973 977 else:
974 978 self._ui.warn(_('no branch checked out in subrepo %s\n'
975 979 'cannot push revision %s') %
976 980 (self._relpath, self._state[1]))
977 981 return False
978 982
979 983 def remove(self):
980 984 if self._gitmissing():
981 985 return
982 986 if self.dirty():
983 987 self._ui.warn(_('not removing repo %s because '
984 988 'it has changes.\n') % self._relpath)
985 989 return
986 990 # we can't fully delete the repository as it may contain
987 991 # local-only history
988 992 self._ui.note(_('removing subrepo %s\n') % self._relpath)
989 993 self._gitcommand(['config', 'core.bare', 'true'])
990 994 for f in os.listdir(self._abspath):
991 995 if f == '.git':
992 996 continue
993 997 path = os.path.join(self._abspath, f)
994 998 if os.path.isdir(path) and not os.path.islink(path):
995 999 shutil.rmtree(path)
996 1000 else:
997 1001 os.remove(path)
998 1002
999 1003 def archive(self, ui, archiver, prefix):
1000 1004 source, revision = self._state
1001 1005 if not revision:
1002 1006 return
1003 1007 self._fetch(source, revision)
1004 1008
1005 1009 # Parse git's native archive command.
1006 1010 # This should be much faster than manually traversing the trees
1007 1011 # and objects with many subprocess calls.
1008 1012 tarstream = self._gitcommand(['archive', revision], stream=True)
1009 1013 tar = tarfile.open(fileobj=tarstream, mode='r|')
1010 1014 relpath = subrelpath(self)
1011 1015 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1012 1016 for i, info in enumerate(tar):
1013 1017 if info.isdir():
1014 1018 continue
1015 1019 if info.issym():
1016 1020 data = info.linkname
1017 1021 else:
1018 1022 data = tar.extractfile(info).read()
1019 1023 archiver.addfile(os.path.join(prefix, self._path, info.name),
1020 1024 info.mode, info.issym(), data)
1021 1025 ui.progress(_('archiving (%s)') % relpath, i + 1,
1022 1026 unit=_('files'))
1023 1027 ui.progress(_('archiving (%s)') % relpath, None)
1024 1028
1025 1029
1026 1030 def status(self, rev2, **opts):
1027 1031 rev1 = self._state[1]
1028 1032 if self._gitmissing() or not rev1:
1029 1033 # if the repo is missing, return no results
1030 1034 return [], [], [], [], [], [], []
1031 1035 modified, added, removed = [], [], []
1032 1036 if rev2:
1033 1037 command = ['diff-tree', rev1, rev2]
1034 1038 else:
1035 1039 command = ['diff-index', rev1]
1036 1040 out = self._gitcommand(command)
1037 1041 for line in out.split('\n'):
1038 1042 tab = line.find('\t')
1039 1043 if tab == -1:
1040 1044 continue
1041 1045 status, f = line[tab - 1], line[tab + 1:]
1042 1046 if status == 'M':
1043 1047 modified.append(f)
1044 1048 elif status == 'A':
1045 1049 added.append(f)
1046 1050 elif status == 'D':
1047 1051 removed.append(f)
1048 1052
1049 1053 deleted = unknown = ignored = clean = []
1050 1054 return modified, added, removed, deleted, unknown, ignored, clean
1051 1055
1052 1056 types = {
1053 1057 'hg': hgsubrepo,
1054 1058 'svn': svnsubrepo,
1055 1059 'git': gitsubrepo,
1056 1060 }
General Comments 0
You need to be logged in to leave comments. Login now