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