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