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