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