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