##// END OF EJS Templates
subrepo: add basestate method...
Matt Mackall -
r16072:bcb973ab default
parent child Browse files
Show More
@@ -1,1149 +1,1163
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 def basestate(self):
279 """current working directory base state, disregarding .hgsubstate
280 state and working directory modifications"""
281 raise NotImplementedError
282
278 283 def checknested(self, path):
279 284 """check if path is a subrepository within this repository"""
280 285 return False
281 286
282 287 def commit(self, text, user, date):
283 288 """commit the current changes to the subrepo with the given
284 289 log message. Use given user and date if possible. Return the
285 290 new state of the subrepo.
286 291 """
287 292 raise NotImplementedError
288 293
289 294 def remove(self):
290 295 """remove the subrepo
291 296
292 297 (should verify the dirstate is not dirty first)
293 298 """
294 299 raise NotImplementedError
295 300
296 301 def get(self, state, overwrite=False):
297 302 """run whatever commands are needed to put the subrepo into
298 303 this state
299 304 """
300 305 raise NotImplementedError
301 306
302 307 def merge(self, state):
303 308 """merge currently-saved state with the new state."""
304 309 raise NotImplementedError
305 310
306 311 def push(self, opts):
307 312 """perform whatever action is analogous to 'hg push'
308 313
309 314 This may be a no-op on some systems.
310 315 """
311 316 raise NotImplementedError
312 317
313 318 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
314 319 return []
315 320
316 321 def status(self, rev2, **opts):
317 322 return [], [], [], [], [], [], []
318 323
319 324 def diff(self, diffopts, node2, match, prefix, **opts):
320 325 pass
321 326
322 327 def outgoing(self, ui, dest, opts):
323 328 return 1
324 329
325 330 def incoming(self, ui, source, opts):
326 331 return 1
327 332
328 333 def files(self):
329 334 """return filename iterator"""
330 335 raise NotImplementedError
331 336
332 337 def filedata(self, name):
333 338 """return file data"""
334 339 raise NotImplementedError
335 340
336 341 def fileflags(self, name):
337 342 """return file flags"""
338 343 return ''
339 344
340 345 def archive(self, ui, archiver, prefix):
341 346 files = self.files()
342 347 total = len(files)
343 348 relpath = subrelpath(self)
344 349 ui.progress(_('archiving (%s)') % relpath, 0,
345 350 unit=_('files'), total=total)
346 351 for i, name in enumerate(files):
347 352 flags = self.fileflags(name)
348 353 mode = 'x' in flags and 0755 or 0644
349 354 symlink = 'l' in flags
350 355 archiver.addfile(os.path.join(prefix, self._path, name),
351 356 mode, symlink, self.filedata(name))
352 357 ui.progress(_('archiving (%s)') % relpath, i + 1,
353 358 unit=_('files'), total=total)
354 359 ui.progress(_('archiving (%s)') % relpath, None)
355 360
356 361 def walk(self, match):
357 362 '''
358 363 walk recursively through the directory tree, finding all files
359 364 matched by the match function
360 365 '''
361 366 pass
362 367
363 368 def forget(self, ui, match, prefix):
364 369 return []
365 370
366 371 class hgsubrepo(abstractsubrepo):
367 372 def __init__(self, ctx, path, state):
368 373 self._path = path
369 374 self._state = state
370 375 r = ctx._repo
371 376 root = r.wjoin(path)
372 377 create = False
373 378 if not os.path.exists(os.path.join(root, '.hg')):
374 379 create = True
375 380 util.makedirs(root)
376 381 self._repo = hg.repository(r.ui, root, create=create)
377 382 self._initrepo(r, state[0], create)
378 383
379 384 def _initrepo(self, parentrepo, source, create):
380 385 self._repo._subparent = parentrepo
381 386 self._repo._subsource = source
382 387
383 388 if create:
384 389 fp = self._repo.opener("hgrc", "w", text=True)
385 390 fp.write('[paths]\n')
386 391
387 392 def addpathconfig(key, value):
388 393 if value:
389 394 fp.write('%s = %s\n' % (key, value))
390 395 self._repo.ui.setconfig('paths', key, value)
391 396
392 397 defpath = _abssource(self._repo, abort=False)
393 398 defpushpath = _abssource(self._repo, True, abort=False)
394 399 addpathconfig('default', defpath)
395 400 if defpath != defpushpath:
396 401 addpathconfig('default-push', defpushpath)
397 402 fp.close()
398 403
399 404 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
400 405 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
401 406 os.path.join(prefix, self._path), explicitonly)
402 407
403 408 def status(self, rev2, **opts):
404 409 try:
405 410 rev1 = self._state[1]
406 411 ctx1 = self._repo[rev1]
407 412 ctx2 = self._repo[rev2]
408 413 return self._repo.status(ctx1, ctx2, **opts)
409 414 except error.RepoLookupError, inst:
410 415 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
411 416 % (inst, subrelpath(self)))
412 417 return [], [], [], [], [], [], []
413 418
414 419 def diff(self, diffopts, node2, match, prefix, **opts):
415 420 try:
416 421 node1 = node.bin(self._state[1])
417 422 # We currently expect node2 to come from substate and be
418 423 # in hex format
419 424 if node2 is not None:
420 425 node2 = node.bin(node2)
421 426 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
422 427 node1, node2, match,
423 428 prefix=os.path.join(prefix, self._path),
424 429 listsubrepos=True, **opts)
425 430 except error.RepoLookupError, inst:
426 431 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
427 432 % (inst, subrelpath(self)))
428 433
429 434 def archive(self, ui, archiver, prefix):
430 435 self._get(self._state + ('hg',))
431 436 abstractsubrepo.archive(self, ui, archiver, prefix)
432 437
433 438 rev = self._state[1]
434 439 ctx = self._repo[rev]
435 440 for subpath in ctx.substate:
436 441 s = subrepo(ctx, subpath)
437 442 s.archive(ui, archiver, os.path.join(prefix, self._path))
438 443
439 444 def dirty(self, ignoreupdate=False):
440 445 r = self._state[1]
441 446 if r == '' and not ignoreupdate: # no state recorded
442 447 return True
443 448 w = self._repo[None]
444 449 if r != w.p1().hex() and not ignoreupdate:
445 450 # different version checked out
446 451 return True
447 452 return w.dirty() # working directory changed
448 453
454 def basestate(self):
455 return self._repo['.'].hex()
456
449 457 def checknested(self, path):
450 458 return self._repo._checknested(self._repo.wjoin(path))
451 459
452 460 def commit(self, text, user, date):
453 461 # don't bother committing in the subrepo if it's only been
454 462 # updated
455 463 if not self.dirty(True):
456 464 return self._repo['.'].hex()
457 465 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
458 466 n = self._repo.commit(text, user, date)
459 467 if not n:
460 468 return self._repo['.'].hex() # different version checked out
461 469 return node.hex(n)
462 470
463 471 def remove(self):
464 472 # we can't fully delete the repository as it may contain
465 473 # local-only history
466 474 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
467 475 hg.clean(self._repo, node.nullid, False)
468 476
469 477 def _get(self, state):
470 478 source, revision, kind = state
471 479 if revision not in self._repo:
472 480 self._repo._subsource = source
473 481 srcurl = _abssource(self._repo)
474 482 other = hg.peer(self._repo.ui, {}, srcurl)
475 483 if len(self._repo) == 0:
476 484 self._repo.ui.status(_('cloning subrepo %s from %s\n')
477 485 % (subrelpath(self), srcurl))
478 486 parentrepo = self._repo._subparent
479 487 shutil.rmtree(self._repo.path)
480 488 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
481 489 self._repo.root, update=False)
482 490 self._initrepo(parentrepo, source, create=True)
483 491 else:
484 492 self._repo.ui.status(_('pulling subrepo %s from %s\n')
485 493 % (subrelpath(self), srcurl))
486 494 self._repo.pull(other)
487 495 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
488 496 srcurl)
489 497
490 498 def get(self, state, overwrite=False):
491 499 self._get(state)
492 500 source, revision, kind = state
493 501 self._repo.ui.debug("getting subrepo %s\n" % self._path)
494 502 hg.clean(self._repo, revision, False)
495 503
496 504 def merge(self, state):
497 505 self._get(state)
498 506 cur = self._repo['.']
499 507 dst = self._repo[state[1]]
500 508 anc = dst.ancestor(cur)
501 509
502 510 def mergefunc():
503 511 if anc == cur:
504 512 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
505 513 hg.update(self._repo, state[1])
506 514 elif anc == dst:
507 515 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
508 516 else:
509 517 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
510 518 hg.merge(self._repo, state[1], remind=False)
511 519
512 520 wctx = self._repo[None]
513 521 if self.dirty():
514 522 if anc != dst:
515 523 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
516 524 mergefunc()
517 525 else:
518 526 mergefunc()
519 527 else:
520 528 mergefunc()
521 529
522 530 def push(self, opts):
523 531 force = opts.get('force')
524 532 newbranch = opts.get('new_branch')
525 533 ssh = opts.get('ssh')
526 534
527 535 # push subrepos depth-first for coherent ordering
528 536 c = self._repo['']
529 537 subs = c.substate # only repos that are committed
530 538 for s in sorted(subs):
531 539 if c.sub(s).push(opts) == 0:
532 540 return False
533 541
534 542 dsturl = _abssource(self._repo, True)
535 543 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
536 544 (subrelpath(self), dsturl))
537 545 other = hg.peer(self._repo.ui, {'ssh': ssh}, dsturl)
538 546 return self._repo.push(other, force, newbranch=newbranch)
539 547
540 548 def outgoing(self, ui, dest, opts):
541 549 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
542 550
543 551 def incoming(self, ui, source, opts):
544 552 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
545 553
546 554 def files(self):
547 555 rev = self._state[1]
548 556 ctx = self._repo[rev]
549 557 return ctx.manifest()
550 558
551 559 def filedata(self, name):
552 560 rev = self._state[1]
553 561 return self._repo[rev][name].data()
554 562
555 563 def fileflags(self, name):
556 564 rev = self._state[1]
557 565 ctx = self._repo[rev]
558 566 return ctx.flags(name)
559 567
560 568 def walk(self, match):
561 569 ctx = self._repo[None]
562 570 return ctx.walk(match)
563 571
564 572 def forget(self, ui, match, prefix):
565 573 return cmdutil.forget(ui, self._repo, match,
566 574 os.path.join(prefix, self._path), True)
567 575
568 576 class svnsubrepo(abstractsubrepo):
569 577 def __init__(self, ctx, path, state):
570 578 self._path = path
571 579 self._state = state
572 580 self._ctx = ctx
573 581 self._ui = ctx._repo.ui
574 582 self._exe = util.findexe('svn')
575 583 if not self._exe:
576 584 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
577 585 % self._path)
578 586
579 587 def _svncommand(self, commands, filename='', failok=False):
580 588 cmd = [self._exe]
581 589 extrakw = {}
582 590 if not self._ui.interactive():
583 591 # Making stdin be a pipe should prevent svn from behaving
584 592 # interactively even if we can't pass --non-interactive.
585 593 extrakw['stdin'] = subprocess.PIPE
586 594 # Starting in svn 1.5 --non-interactive is a global flag
587 595 # instead of being per-command, but we need to support 1.4 so
588 596 # we have to be intelligent about what commands take
589 597 # --non-interactive.
590 598 if commands[0] in ('update', 'checkout', 'commit'):
591 599 cmd.append('--non-interactive')
592 600 cmd.extend(commands)
593 601 if filename is not None:
594 602 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
595 603 cmd.append(path)
596 604 env = dict(os.environ)
597 605 # Avoid localized output, preserve current locale for everything else.
598 606 env['LC_MESSAGES'] = 'C'
599 607 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
600 608 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
601 609 universal_newlines=True, env=env, **extrakw)
602 610 stdout, stderr = p.communicate()
603 611 stderr = stderr.strip()
604 612 if not failok:
605 613 if p.returncode:
606 614 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
607 615 if stderr:
608 616 self._ui.warn(stderr + '\n')
609 617 return stdout, stderr
610 618
611 619 @propertycache
612 620 def _svnversion(self):
613 621 output, err = self._svncommand(['--version'], filename=None)
614 622 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
615 623 if not m:
616 624 raise util.Abort(_('cannot retrieve svn tool version'))
617 625 return (int(m.group(1)), int(m.group(2)))
618 626
619 627 def _wcrevs(self):
620 628 # Get the working directory revision as well as the last
621 629 # commit revision so we can compare the subrepo state with
622 630 # both. We used to store the working directory one.
623 631 output, err = self._svncommand(['info', '--xml'])
624 632 doc = xml.dom.minidom.parseString(output)
625 633 entries = doc.getElementsByTagName('entry')
626 634 lastrev, rev = '0', '0'
627 635 if entries:
628 636 rev = str(entries[0].getAttribute('revision')) or '0'
629 637 commits = entries[0].getElementsByTagName('commit')
630 638 if commits:
631 639 lastrev = str(commits[0].getAttribute('revision')) or '0'
632 640 return (lastrev, rev)
633 641
634 642 def _wcrev(self):
635 643 return self._wcrevs()[0]
636 644
637 645 def _wcchanged(self):
638 646 """Return (changes, extchanges) where changes is True
639 647 if the working directory was changed, and extchanges is
640 648 True if any of these changes concern an external entry.
641 649 """
642 650 output, err = self._svncommand(['status', '--xml'])
643 651 externals, changes = [], []
644 652 doc = xml.dom.minidom.parseString(output)
645 653 for e in doc.getElementsByTagName('entry'):
646 654 s = e.getElementsByTagName('wc-status')
647 655 if not s:
648 656 continue
649 657 item = s[0].getAttribute('item')
650 658 props = s[0].getAttribute('props')
651 659 path = e.getAttribute('path')
652 660 if item == 'external':
653 661 externals.append(path)
654 662 if (item not in ('', 'normal', 'unversioned', 'external')
655 663 or props not in ('', 'none', 'normal')):
656 664 changes.append(path)
657 665 for path in changes:
658 666 for ext in externals:
659 667 if path == ext or path.startswith(ext + os.sep):
660 668 return True, True
661 669 return bool(changes), False
662 670
663 671 def dirty(self, ignoreupdate=False):
664 672 if not self._wcchanged()[0]:
665 673 if self._state[1] in self._wcrevs() or ignoreupdate:
666 674 return False
667 675 return True
668 676
677 def basestate(self):
678 return self._wcrev()
679
669 680 def commit(self, text, user, date):
670 681 # user and date are out of our hands since svn is centralized
671 682 changed, extchanged = self._wcchanged()
672 683 if not changed:
673 684 return self._wcrev()
674 685 if extchanged:
675 686 # Do not try to commit externals
676 687 raise util.Abort(_('cannot commit svn externals'))
677 688 commitinfo, err = self._svncommand(['commit', '-m', text])
678 689 self._ui.status(commitinfo)
679 690 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
680 691 if not newrev:
681 692 raise util.Abort(commitinfo.splitlines()[-1])
682 693 newrev = newrev.groups()[0]
683 694 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
684 695 return newrev
685 696
686 697 def remove(self):
687 698 if self.dirty():
688 699 self._ui.warn(_('not removing repo %s because '
689 700 'it has changes.\n' % self._path))
690 701 return
691 702 self._ui.note(_('removing subrepo %s\n') % self._path)
692 703
693 704 def onerror(function, path, excinfo):
694 705 if function is not os.remove:
695 706 raise
696 707 # read-only files cannot be unlinked under Windows
697 708 s = os.stat(path)
698 709 if (s.st_mode & stat.S_IWRITE) != 0:
699 710 raise
700 711 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
701 712 os.remove(path)
702 713
703 714 path = self._ctx._repo.wjoin(self._path)
704 715 shutil.rmtree(path, onerror=onerror)
705 716 try:
706 717 os.removedirs(os.path.dirname(path))
707 718 except OSError:
708 719 pass
709 720
710 721 def get(self, state, overwrite=False):
711 722 if overwrite:
712 723 self._svncommand(['revert', '--recursive'])
713 724 args = ['checkout']
714 725 if self._svnversion >= (1, 5):
715 726 args.append('--force')
716 727 # The revision must be specified at the end of the URL to properly
717 728 # update to a directory which has since been deleted and recreated.
718 729 args.append('%s@%s' % (state[0], state[1]))
719 730 status, err = self._svncommand(args, failok=True)
720 731 if not re.search('Checked out revision [0-9]+.', status):
721 732 if ('is already a working copy for a different URL' in err
722 733 and (self._wcchanged() == (False, False))):
723 734 # obstructed but clean working copy, so just blow it away.
724 735 self.remove()
725 736 self.get(state, overwrite=False)
726 737 return
727 738 raise util.Abort((status or err).splitlines()[-1])
728 739 self._ui.status(status)
729 740
730 741 def merge(self, state):
731 742 old = self._state[1]
732 743 new = state[1]
733 744 if new != self._wcrev():
734 745 dirty = old == self._wcrev() or self._wcchanged()[0]
735 746 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
736 747 self.get(state, False)
737 748
738 749 def push(self, opts):
739 750 # push is a no-op for SVN
740 751 return True
741 752
742 753 def files(self):
743 754 output = self._svncommand(['list'])
744 755 # This works because svn forbids \n in filenames.
745 756 return output.splitlines()
746 757
747 758 def filedata(self, name):
748 759 return self._svncommand(['cat'], name)
749 760
750 761
751 762 class gitsubrepo(abstractsubrepo):
752 763 def __init__(self, ctx, path, state):
753 764 # TODO add git version check.
754 765 self._state = state
755 766 self._ctx = ctx
756 767 self._path = path
757 768 self._relpath = os.path.join(reporelpath(ctx._repo), path)
758 769 self._abspath = ctx._repo.wjoin(path)
759 770 self._subparent = ctx._repo
760 771 self._ui = ctx._repo.ui
761 772
762 773 def _gitcommand(self, commands, env=None, stream=False):
763 774 return self._gitdir(commands, env=env, stream=stream)[0]
764 775
765 776 def _gitdir(self, commands, env=None, stream=False):
766 777 return self._gitnodir(commands, env=env, stream=stream,
767 778 cwd=self._abspath)
768 779
769 780 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
770 781 """Calls the git command
771 782
772 783 The methods tries to call the git command. versions previor to 1.6.0
773 784 are not supported and very probably fail.
774 785 """
775 786 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
776 787 # unless ui.quiet is set, print git's stderr,
777 788 # which is mostly progress and useful info
778 789 errpipe = None
779 790 if self._ui.quiet:
780 791 errpipe = open(os.devnull, 'w')
781 792 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
782 793 close_fds=util.closefds,
783 794 stdout=subprocess.PIPE, stderr=errpipe)
784 795 if stream:
785 796 return p.stdout, None
786 797
787 798 retdata = p.stdout.read().strip()
788 799 # wait for the child to exit to avoid race condition.
789 800 p.wait()
790 801
791 802 if p.returncode != 0 and p.returncode != 1:
792 803 # there are certain error codes that are ok
793 804 command = commands[0]
794 805 if command in ('cat-file', 'symbolic-ref'):
795 806 return retdata, p.returncode
796 807 # for all others, abort
797 808 raise util.Abort('git %s error %d in %s' %
798 809 (command, p.returncode, self._relpath))
799 810
800 811 return retdata, p.returncode
801 812
802 813 def _gitmissing(self):
803 814 return not os.path.exists(os.path.join(self._abspath, '.git'))
804 815
805 816 def _gitstate(self):
806 817 return self._gitcommand(['rev-parse', 'HEAD'])
807 818
808 819 def _gitcurrentbranch(self):
809 820 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
810 821 if err:
811 822 current = None
812 823 return current
813 824
814 825 def _gitremote(self, remote):
815 826 out = self._gitcommand(['remote', 'show', '-n', remote])
816 827 line = out.split('\n')[1]
817 828 i = line.index('URL: ') + len('URL: ')
818 829 return line[i:]
819 830
820 831 def _githavelocally(self, revision):
821 832 out, code = self._gitdir(['cat-file', '-e', revision])
822 833 return code == 0
823 834
824 835 def _gitisancestor(self, r1, r2):
825 836 base = self._gitcommand(['merge-base', r1, r2])
826 837 return base == r1
827 838
828 839 def _gitisbare(self):
829 840 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
830 841
831 842 def _gitupdatestat(self):
832 843 """This must be run before git diff-index.
833 844 diff-index only looks at changes to file stat;
834 845 this command looks at file contents and updates the stat."""
835 846 self._gitcommand(['update-index', '-q', '--refresh'])
836 847
837 848 def _gitbranchmap(self):
838 849 '''returns 2 things:
839 850 a map from git branch to revision
840 851 a map from revision to branches'''
841 852 branch2rev = {}
842 853 rev2branch = {}
843 854
844 855 out = self._gitcommand(['for-each-ref', '--format',
845 856 '%(objectname) %(refname)'])
846 857 for line in out.split('\n'):
847 858 revision, ref = line.split(' ')
848 859 if (not ref.startswith('refs/heads/') and
849 860 not ref.startswith('refs/remotes/')):
850 861 continue
851 862 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
852 863 continue # ignore remote/HEAD redirects
853 864 branch2rev[ref] = revision
854 865 rev2branch.setdefault(revision, []).append(ref)
855 866 return branch2rev, rev2branch
856 867
857 868 def _gittracking(self, branches):
858 869 'return map of remote branch to local tracking branch'
859 870 # assumes no more than one local tracking branch for each remote
860 871 tracking = {}
861 872 for b in branches:
862 873 if b.startswith('refs/remotes/'):
863 874 continue
864 875 bname = b.split('/', 2)[2]
865 876 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
866 877 if remote:
867 878 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
868 879 tracking['refs/remotes/%s/%s' %
869 880 (remote, ref.split('/', 2)[2])] = b
870 881 return tracking
871 882
872 883 def _abssource(self, source):
873 884 if '://' not in source:
874 885 # recognize the scp syntax as an absolute source
875 886 colon = source.find(':')
876 887 if colon != -1 and '/' not in source[:colon]:
877 888 return source
878 889 self._subsource = source
879 890 return _abssource(self)
880 891
881 892 def _fetch(self, source, revision):
882 893 if self._gitmissing():
883 894 source = self._abssource(source)
884 895 self._ui.status(_('cloning subrepo %s from %s\n') %
885 896 (self._relpath, source))
886 897 self._gitnodir(['clone', source, self._abspath])
887 898 if self._githavelocally(revision):
888 899 return
889 900 self._ui.status(_('pulling subrepo %s from %s\n') %
890 901 (self._relpath, self._gitremote('origin')))
891 902 # try only origin: the originally cloned repo
892 903 self._gitcommand(['fetch'])
893 904 if not self._githavelocally(revision):
894 905 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
895 906 (revision, self._relpath))
896 907
897 908 def dirty(self, ignoreupdate=False):
898 909 if self._gitmissing():
899 910 return self._state[1] != ''
900 911 if self._gitisbare():
901 912 return True
902 913 if not ignoreupdate and self._state[1] != self._gitstate():
903 914 # different version checked out
904 915 return True
905 916 # check for staged changes or modified files; ignore untracked files
906 917 self._gitupdatestat()
907 918 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
908 919 return code == 1
909 920
921 def basestate(self):
922 return self._gitstate()
923
910 924 def get(self, state, overwrite=False):
911 925 source, revision, kind = state
912 926 if not revision:
913 927 self.remove()
914 928 return
915 929 self._fetch(source, revision)
916 930 # if the repo was set to be bare, unbare it
917 931 if self._gitisbare():
918 932 self._gitcommand(['config', 'core.bare', 'false'])
919 933 if self._gitstate() == revision:
920 934 self._gitcommand(['reset', '--hard', 'HEAD'])
921 935 return
922 936 elif self._gitstate() == revision:
923 937 if overwrite:
924 938 # first reset the index to unmark new files for commit, because
925 939 # reset --hard will otherwise throw away files added for commit,
926 940 # not just unmark them.
927 941 self._gitcommand(['reset', 'HEAD'])
928 942 self._gitcommand(['reset', '--hard', 'HEAD'])
929 943 return
930 944 branch2rev, rev2branch = self._gitbranchmap()
931 945
932 946 def checkout(args):
933 947 cmd = ['checkout']
934 948 if overwrite:
935 949 # first reset the index to unmark new files for commit, because
936 950 # the -f option will otherwise throw away files added for
937 951 # commit, not just unmark them.
938 952 self._gitcommand(['reset', 'HEAD'])
939 953 cmd.append('-f')
940 954 self._gitcommand(cmd + args)
941 955
942 956 def rawcheckout():
943 957 # no branch to checkout, check it out with no branch
944 958 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
945 959 self._relpath)
946 960 self._ui.warn(_('check out a git branch if you intend '
947 961 'to make changes\n'))
948 962 checkout(['-q', revision])
949 963
950 964 if revision not in rev2branch:
951 965 rawcheckout()
952 966 return
953 967 branches = rev2branch[revision]
954 968 firstlocalbranch = None
955 969 for b in branches:
956 970 if b == 'refs/heads/master':
957 971 # master trumps all other branches
958 972 checkout(['refs/heads/master'])
959 973 return
960 974 if not firstlocalbranch and not b.startswith('refs/remotes/'):
961 975 firstlocalbranch = b
962 976 if firstlocalbranch:
963 977 checkout([firstlocalbranch])
964 978 return
965 979
966 980 tracking = self._gittracking(branch2rev.keys())
967 981 # choose a remote branch already tracked if possible
968 982 remote = branches[0]
969 983 if remote not in tracking:
970 984 for b in branches:
971 985 if b in tracking:
972 986 remote = b
973 987 break
974 988
975 989 if remote not in tracking:
976 990 # create a new local tracking branch
977 991 local = remote.split('/', 2)[2]
978 992 checkout(['-b', local, remote])
979 993 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
980 994 # When updating to a tracked remote branch,
981 995 # if the local tracking branch is downstream of it,
982 996 # a normal `git pull` would have performed a "fast-forward merge"
983 997 # which is equivalent to updating the local branch to the remote.
984 998 # Since we are only looking at branching at update, we need to
985 999 # detect this situation and perform this action lazily.
986 1000 if tracking[remote] != self._gitcurrentbranch():
987 1001 checkout([tracking[remote]])
988 1002 self._gitcommand(['merge', '--ff', remote])
989 1003 else:
990 1004 # a real merge would be required, just checkout the revision
991 1005 rawcheckout()
992 1006
993 1007 def commit(self, text, user, date):
994 1008 if self._gitmissing():
995 1009 raise util.Abort(_("subrepo %s is missing") % self._relpath)
996 1010 cmd = ['commit', '-a', '-m', text]
997 1011 env = os.environ.copy()
998 1012 if user:
999 1013 cmd += ['--author', user]
1000 1014 if date:
1001 1015 # git's date parser silently ignores when seconds < 1e9
1002 1016 # convert to ISO8601
1003 1017 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1004 1018 '%Y-%m-%dT%H:%M:%S %1%2')
1005 1019 self._gitcommand(cmd, env=env)
1006 1020 # make sure commit works otherwise HEAD might not exist under certain
1007 1021 # circumstances
1008 1022 return self._gitstate()
1009 1023
1010 1024 def merge(self, state):
1011 1025 source, revision, kind = state
1012 1026 self._fetch(source, revision)
1013 1027 base = self._gitcommand(['merge-base', revision, self._state[1]])
1014 1028 self._gitupdatestat()
1015 1029 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1016 1030
1017 1031 def mergefunc():
1018 1032 if base == revision:
1019 1033 self.get(state) # fast forward merge
1020 1034 elif base != self._state[1]:
1021 1035 self._gitcommand(['merge', '--no-commit', revision])
1022 1036
1023 1037 if self.dirty():
1024 1038 if self._gitstate() != revision:
1025 1039 dirty = self._gitstate() == self._state[1] or code != 0
1026 1040 if _updateprompt(self._ui, self, dirty,
1027 1041 self._state[1][:7], revision[:7]):
1028 1042 mergefunc()
1029 1043 else:
1030 1044 mergefunc()
1031 1045
1032 1046 def push(self, opts):
1033 1047 force = opts.get('force')
1034 1048
1035 1049 if not self._state[1]:
1036 1050 return True
1037 1051 if self._gitmissing():
1038 1052 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1039 1053 # if a branch in origin contains the revision, nothing to do
1040 1054 branch2rev, rev2branch = self._gitbranchmap()
1041 1055 if self._state[1] in rev2branch:
1042 1056 for b in rev2branch[self._state[1]]:
1043 1057 if b.startswith('refs/remotes/origin/'):
1044 1058 return True
1045 1059 for b, revision in branch2rev.iteritems():
1046 1060 if b.startswith('refs/remotes/origin/'):
1047 1061 if self._gitisancestor(self._state[1], revision):
1048 1062 return True
1049 1063 # otherwise, try to push the currently checked out branch
1050 1064 cmd = ['push']
1051 1065 if force:
1052 1066 cmd.append('--force')
1053 1067
1054 1068 current = self._gitcurrentbranch()
1055 1069 if current:
1056 1070 # determine if the current branch is even useful
1057 1071 if not self._gitisancestor(self._state[1], current):
1058 1072 self._ui.warn(_('unrelated git branch checked out '
1059 1073 'in subrepo %s\n') % self._relpath)
1060 1074 return False
1061 1075 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1062 1076 (current.split('/', 2)[2], self._relpath))
1063 1077 self._gitcommand(cmd + ['origin', current])
1064 1078 return True
1065 1079 else:
1066 1080 self._ui.warn(_('no branch checked out in subrepo %s\n'
1067 1081 'cannot push revision %s') %
1068 1082 (self._relpath, self._state[1]))
1069 1083 return False
1070 1084
1071 1085 def remove(self):
1072 1086 if self._gitmissing():
1073 1087 return
1074 1088 if self.dirty():
1075 1089 self._ui.warn(_('not removing repo %s because '
1076 1090 'it has changes.\n') % self._relpath)
1077 1091 return
1078 1092 # we can't fully delete the repository as it may contain
1079 1093 # local-only history
1080 1094 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1081 1095 self._gitcommand(['config', 'core.bare', 'true'])
1082 1096 for f in os.listdir(self._abspath):
1083 1097 if f == '.git':
1084 1098 continue
1085 1099 path = os.path.join(self._abspath, f)
1086 1100 if os.path.isdir(path) and not os.path.islink(path):
1087 1101 shutil.rmtree(path)
1088 1102 else:
1089 1103 os.remove(path)
1090 1104
1091 1105 def archive(self, ui, archiver, prefix):
1092 1106 source, revision = self._state
1093 1107 if not revision:
1094 1108 return
1095 1109 self._fetch(source, revision)
1096 1110
1097 1111 # Parse git's native archive command.
1098 1112 # This should be much faster than manually traversing the trees
1099 1113 # and objects with many subprocess calls.
1100 1114 tarstream = self._gitcommand(['archive', revision], stream=True)
1101 1115 tar = tarfile.open(fileobj=tarstream, mode='r|')
1102 1116 relpath = subrelpath(self)
1103 1117 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1104 1118 for i, info in enumerate(tar):
1105 1119 if info.isdir():
1106 1120 continue
1107 1121 if info.issym():
1108 1122 data = info.linkname
1109 1123 else:
1110 1124 data = tar.extractfile(info).read()
1111 1125 archiver.addfile(os.path.join(prefix, self._path, info.name),
1112 1126 info.mode, info.issym(), data)
1113 1127 ui.progress(_('archiving (%s)') % relpath, i + 1,
1114 1128 unit=_('files'))
1115 1129 ui.progress(_('archiving (%s)') % relpath, None)
1116 1130
1117 1131
1118 1132 def status(self, rev2, **opts):
1119 1133 rev1 = self._state[1]
1120 1134 if self._gitmissing() or not rev1:
1121 1135 # if the repo is missing, return no results
1122 1136 return [], [], [], [], [], [], []
1123 1137 modified, added, removed = [], [], []
1124 1138 self._gitupdatestat()
1125 1139 if rev2:
1126 1140 command = ['diff-tree', rev1, rev2]
1127 1141 else:
1128 1142 command = ['diff-index', rev1]
1129 1143 out = self._gitcommand(command)
1130 1144 for line in out.split('\n'):
1131 1145 tab = line.find('\t')
1132 1146 if tab == -1:
1133 1147 continue
1134 1148 status, f = line[tab - 1], line[tab + 1:]
1135 1149 if status == 'M':
1136 1150 modified.append(f)
1137 1151 elif status == 'A':
1138 1152 added.append(f)
1139 1153 elif status == 'D':
1140 1154 removed.append(f)
1141 1155
1142 1156 deleted = unknown = ignored = clean = []
1143 1157 return modified, added, removed, deleted, unknown, ignored, clean
1144 1158
1145 1159 types = {
1146 1160 'hg': hgsubrepo,
1147 1161 'svn': svnsubrepo,
1148 1162 'git': gitsubrepo,
1149 1163 }
General Comments 0
You need to be logged in to leave comments. Login now