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