##// END OF EJS Templates
subrepo: handle svn tracked/unknown directory collisions...
Patrick Mezard -
r14050:9e8a9d45 stable
parent child Browse files
Show More
@@ -1,1032 +1,1046 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 propertycache = util.propertycache
13 14
14 15 nullstate = ('', '', 'empty')
15 16
16 17 def state(ctx, ui):
17 18 """return a state dict, mapping subrepo paths configured in .hgsub
18 19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 20 (key in types dict))
20 21 """
21 22 p = config.config()
22 23 def read(f, sections=None, remap=None):
23 24 if f in ctx:
24 25 try:
25 26 data = ctx[f].data()
26 27 except IOError, err:
27 28 if err.errno != errno.ENOENT:
28 29 raise
29 30 # handle missing subrepo spec files as removed
30 31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 32 return
32 33 p.parse(f, data, sections, remap, read)
33 34 else:
34 35 raise util.Abort(_("subrepo spec file %s not found") % f)
35 36
36 37 if '.hgsub' in ctx:
37 38 read('.hgsub')
38 39
39 40 for path, src in ui.configitems('subpaths'):
40 41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41 42
42 43 rev = {}
43 44 if '.hgsubstate' in ctx:
44 45 try:
45 46 for l in ctx['.hgsubstate'].data().splitlines():
46 47 revision, path = l.split(" ", 1)
47 48 rev[path] = revision
48 49 except IOError, err:
49 50 if err.errno != errno.ENOENT:
50 51 raise
51 52
52 53 state = {}
53 54 for path, src in p[''].items():
54 55 kind = 'hg'
55 56 if src.startswith('['):
56 57 if ']' not in src:
57 58 raise util.Abort(_('missing ] in subrepo source'))
58 59 kind, src = src.split(']', 1)
59 60 kind = kind[1:]
60 61
61 62 for pattern, repl in p.items('subpaths'):
62 63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 64 # does a string decode.
64 65 repl = repl.encode('string-escape')
65 66 # However, we still want to allow back references to go
66 67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 68 # extra escapes are needed because re.sub string decodes.
68 69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 70 try:
70 71 src = re.sub(pattern, repl, src, 1)
71 72 except re.error, e:
72 73 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 74 % (p.source('subpaths', pattern), e))
74 75
75 76 state[path] = (src.strip(), rev.get(path, ''), kind)
76 77
77 78 return state
78 79
79 80 def writestate(repo, state):
80 81 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
81 82 repo.wwrite('.hgsubstate',
82 83 ''.join(['%s %s\n' % (state[s][1], s)
83 84 for s in sorted(state)]), '')
84 85
85 86 def submerge(repo, wctx, mctx, actx, overwrite):
86 87 """delegated from merge.applyupdates: merging of .hgsubstate file
87 88 in working context, merging context and ancestor context"""
88 89 if mctx == actx: # backwards?
89 90 actx = wctx.p1()
90 91 s1 = wctx.substate
91 92 s2 = mctx.substate
92 93 sa = actx.substate
93 94 sm = {}
94 95
95 96 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
96 97
97 98 def debug(s, msg, r=""):
98 99 if r:
99 100 r = "%s:%s:%s" % r
100 101 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
101 102
102 103 for s, l in s1.items():
103 104 a = sa.get(s, nullstate)
104 105 ld = l # local state with possible dirty flag for compares
105 106 if wctx.sub(s).dirty():
106 107 ld = (l[0], l[1] + "+")
107 108 if wctx == actx: # overwrite
108 109 a = ld
109 110
110 111 if s in s2:
111 112 r = s2[s]
112 113 if ld == r or r == a: # no change or local is newer
113 114 sm[s] = l
114 115 continue
115 116 elif ld == a: # other side changed
116 117 debug(s, "other changed, get", r)
117 118 wctx.sub(s).get(r, overwrite)
118 119 sm[s] = r
119 120 elif ld[0] != r[0]: # sources differ
120 121 if repo.ui.promptchoice(
121 122 _(' subrepository sources for %s differ\n'
122 123 'use (l)ocal source (%s) or (r)emote source (%s)?')
123 124 % (s, l[0], r[0]),
124 125 (_('&Local'), _('&Remote')), 0):
125 126 debug(s, "prompt changed, get", r)
126 127 wctx.sub(s).get(r, overwrite)
127 128 sm[s] = r
128 129 elif ld[1] == a[1]: # local side is unchanged
129 130 debug(s, "other side changed, get", r)
130 131 wctx.sub(s).get(r, overwrite)
131 132 sm[s] = r
132 133 else:
133 134 debug(s, "both sides changed, merge with", r)
134 135 wctx.sub(s).merge(r)
135 136 sm[s] = l
136 137 elif ld == a: # remote removed, local unchanged
137 138 debug(s, "remote removed, remove")
138 139 wctx.sub(s).remove()
139 140 else:
140 141 if repo.ui.promptchoice(
141 142 _(' local changed subrepository %s which remote removed\n'
142 143 'use (c)hanged version or (d)elete?') % s,
143 144 (_('&Changed'), _('&Delete')), 0):
144 145 debug(s, "prompt remove")
145 146 wctx.sub(s).remove()
146 147
147 148 for s, r in s2.items():
148 149 if s in s1:
149 150 continue
150 151 elif s not in sa:
151 152 debug(s, "remote added, get", r)
152 153 mctx.sub(s).get(r)
153 154 sm[s] = r
154 155 elif r != sa[s]:
155 156 if repo.ui.promptchoice(
156 157 _(' remote changed subrepository %s which local removed\n'
157 158 'use (c)hanged version or (d)elete?') % s,
158 159 (_('&Changed'), _('&Delete')), 0) == 0:
159 160 debug(s, "prompt recreate", r)
160 161 wctx.sub(s).get(r)
161 162 sm[s] = r
162 163
163 164 # record merged .hgsubstate
164 165 writestate(repo, sm)
165 166
166 167 def _updateprompt(ui, sub, dirty, local, remote):
167 168 if dirty:
168 169 msg = (_(' subrepository sources for %s differ\n'
169 170 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
170 171 % (subrelpath(sub), local, remote))
171 172 else:
172 173 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
173 174 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
174 175 % (subrelpath(sub), local, remote))
175 176 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
176 177
177 178 def reporelpath(repo):
178 179 """return path to this (sub)repo as seen from outermost repo"""
179 180 parent = repo
180 181 while hasattr(parent, '_subparent'):
181 182 parent = parent._subparent
182 183 return repo.root[len(parent.root)+1:]
183 184
184 185 def subrelpath(sub):
185 186 """return path to this subrepo as seen from outermost repo"""
186 187 if hasattr(sub, '_relpath'):
187 188 return sub._relpath
188 189 if not hasattr(sub, '_repo'):
189 190 return sub._path
190 191 return reporelpath(sub._repo)
191 192
192 193 def _abssource(repo, push=False, abort=True):
193 194 """return pull/push path of repo - either based on parent repo .hgsub info
194 195 or on the top repo config. Abort or return None if no source found."""
195 196 if hasattr(repo, '_subparent'):
196 197 source = repo._subsource
197 198 if source.startswith('/') or '://' in source:
198 199 return source
199 200 parent = _abssource(repo._subparent, push, abort=False)
200 201 if parent:
201 202 if '://' in parent:
202 203 if parent[-1] == '/':
203 204 parent = parent[:-1]
204 205 r = urlparse.urlparse(parent + '/' + source)
205 206 if parent.startswith('ssh://'):
206 207 host, path = r[2][2:].split('/', 1)
207 208 r2 = '//%s/%s' % (host, posixpath.normpath(path))
208 209 else:
209 210 r2 = posixpath.normpath(r[2])
210 211 r = urlparse.urlunparse((r[0], r[1], r2,
211 212 r[3], r[4], r[5]))
212 213 return r
213 214 else: # plain file system path
214 215 return posixpath.normpath(os.path.join(parent, repo._subsource))
215 216 else: # recursion reached top repo
216 217 if hasattr(repo, '_subtoppath'):
217 218 return repo._subtoppath
218 219 if push and repo.ui.config('paths', 'default-push'):
219 220 return repo.ui.config('paths', 'default-push')
220 221 if repo.ui.config('paths', 'default'):
221 222 return repo.ui.config('paths', 'default')
222 223 if abort:
223 224 raise util.Abort(_("default path for subrepository %s not found") %
224 225 reporelpath(repo))
225 226
226 227 def itersubrepos(ctx1, ctx2):
227 228 """find subrepos in ctx1 or ctx2"""
228 229 # Create a (subpath, ctx) mapping where we prefer subpaths from
229 230 # ctx1. The subpaths from ctx2 are important when the .hgsub file
230 231 # has been modified (in ctx2) but not yet committed (in ctx1).
231 232 subpaths = dict.fromkeys(ctx2.substate, ctx2)
232 233 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
233 234 for subpath, ctx in sorted(subpaths.iteritems()):
234 235 yield subpath, ctx.sub(subpath)
235 236
236 237 def subrepo(ctx, path):
237 238 """return instance of the right subrepo class for subrepo in path"""
238 239 # subrepo inherently violates our import layering rules
239 240 # because it wants to make repo objects from deep inside the stack
240 241 # so we manually delay the circular imports to not break
241 242 # scripts that don't use our demand-loading
242 243 global hg
243 244 import hg as h
244 245 hg = h
245 246
246 247 util.path_auditor(ctx._repo.root)(path)
247 248 state = ctx.substate.get(path, nullstate)
248 249 if state[2] not in types:
249 250 raise util.Abort(_('unknown subrepo type %s') % state[2])
250 251 return types[state[2]](ctx, path, state[:2])
251 252
252 253 # subrepo classes need to implement the following abstract class:
253 254
254 255 class abstractsubrepo(object):
255 256
256 257 def dirty(self, ignoreupdate=False):
257 258 """returns true if the dirstate of the subrepo is dirty or does not
258 259 match current stored state. If ignoreupdate is true, only check
259 260 whether the subrepo has uncommitted changes in its dirstate.
260 261 """
261 262 raise NotImplementedError
262 263
263 264 def checknested(self, path):
264 265 """check if path is a subrepository within this repository"""
265 266 return False
266 267
267 268 def commit(self, text, user, date):
268 269 """commit the current changes to the subrepo with the given
269 270 log message. Use given user and date if possible. Return the
270 271 new state of the subrepo.
271 272 """
272 273 raise NotImplementedError
273 274
274 275 def remove(self):
275 276 """remove the subrepo
276 277
277 278 (should verify the dirstate is not dirty first)
278 279 """
279 280 raise NotImplementedError
280 281
281 282 def get(self, state, overwrite=False):
282 283 """run whatever commands are needed to put the subrepo into
283 284 this state
284 285 """
285 286 raise NotImplementedError
286 287
287 288 def merge(self, state):
288 289 """merge currently-saved state with the new state."""
289 290 raise NotImplementedError
290 291
291 292 def push(self, force):
292 293 """perform whatever action is analogous to 'hg push'
293 294
294 295 This may be a no-op on some systems.
295 296 """
296 297 raise NotImplementedError
297 298
298 299 def add(self, ui, match, dryrun, prefix):
299 300 return []
300 301
301 302 def status(self, rev2, **opts):
302 303 return [], [], [], [], [], [], []
303 304
304 305 def diff(self, diffopts, node2, match, prefix, **opts):
305 306 pass
306 307
307 308 def outgoing(self, ui, dest, opts):
308 309 return 1
309 310
310 311 def incoming(self, ui, source, opts):
311 312 return 1
312 313
313 314 def files(self):
314 315 """return filename iterator"""
315 316 raise NotImplementedError
316 317
317 318 def filedata(self, name):
318 319 """return file data"""
319 320 raise NotImplementedError
320 321
321 322 def fileflags(self, name):
322 323 """return file flags"""
323 324 return ''
324 325
325 326 def archive(self, ui, archiver, prefix):
326 327 files = self.files()
327 328 total = len(files)
328 329 relpath = subrelpath(self)
329 330 ui.progress(_('archiving (%s)') % relpath, 0,
330 331 unit=_('files'), total=total)
331 332 for i, name in enumerate(files):
332 333 flags = self.fileflags(name)
333 334 mode = 'x' in flags and 0755 or 0644
334 335 symlink = 'l' in flags
335 336 archiver.addfile(os.path.join(prefix, self._path, name),
336 337 mode, symlink, self.filedata(name))
337 338 ui.progress(_('archiving (%s)') % relpath, i + 1,
338 339 unit=_('files'), total=total)
339 340 ui.progress(_('archiving (%s)') % relpath, None)
340 341
341 342
342 343 class hgsubrepo(abstractsubrepo):
343 344 def __init__(self, ctx, path, state):
344 345 self._path = path
345 346 self._state = state
346 347 r = ctx._repo
347 348 root = r.wjoin(path)
348 349 create = False
349 350 if not os.path.exists(os.path.join(root, '.hg')):
350 351 create = True
351 352 util.makedirs(root)
352 353 self._repo = hg.repository(r.ui, root, create=create)
353 354 self._repo._subparent = r
354 355 self._repo._subsource = state[0]
355 356
356 357 if create:
357 358 fp = self._repo.opener("hgrc", "w", text=True)
358 359 fp.write('[paths]\n')
359 360
360 361 def addpathconfig(key, value):
361 362 if value:
362 363 fp.write('%s = %s\n' % (key, value))
363 364 self._repo.ui.setconfig('paths', key, value)
364 365
365 366 defpath = _abssource(self._repo, abort=False)
366 367 defpushpath = _abssource(self._repo, True, abort=False)
367 368 addpathconfig('default', defpath)
368 369 if defpath != defpushpath:
369 370 addpathconfig('default-push', defpushpath)
370 371 fp.close()
371 372
372 373 def add(self, ui, match, dryrun, prefix):
373 374 return cmdutil.add(ui, self._repo, match, dryrun, True,
374 375 os.path.join(prefix, self._path))
375 376
376 377 def status(self, rev2, **opts):
377 378 try:
378 379 rev1 = self._state[1]
379 380 ctx1 = self._repo[rev1]
380 381 ctx2 = self._repo[rev2]
381 382 return self._repo.status(ctx1, ctx2, **opts)
382 383 except error.RepoLookupError, inst:
383 384 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
384 385 % (inst, subrelpath(self)))
385 386 return [], [], [], [], [], [], []
386 387
387 388 def diff(self, diffopts, node2, match, prefix, **opts):
388 389 try:
389 390 node1 = node.bin(self._state[1])
390 391 # We currently expect node2 to come from substate and be
391 392 # in hex format
392 393 if node2 is not None:
393 394 node2 = node.bin(node2)
394 395 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
395 396 node1, node2, match,
396 397 prefix=os.path.join(prefix, self._path),
397 398 listsubrepos=True, **opts)
398 399 except error.RepoLookupError, inst:
399 400 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
400 401 % (inst, subrelpath(self)))
401 402
402 403 def archive(self, ui, archiver, prefix):
403 404 abstractsubrepo.archive(self, ui, archiver, prefix)
404 405
405 406 rev = self._state[1]
406 407 ctx = self._repo[rev]
407 408 for subpath in ctx.substate:
408 409 s = subrepo(ctx, subpath)
409 410 s.archive(ui, archiver, os.path.join(prefix, self._path))
410 411
411 412 def dirty(self, ignoreupdate=False):
412 413 r = self._state[1]
413 414 if r == '' and not ignoreupdate: # no state recorded
414 415 return True
415 416 w = self._repo[None]
416 417 if w.p1() != self._repo[r] and not ignoreupdate:
417 418 # different version checked out
418 419 return True
419 420 return w.dirty() # working directory changed
420 421
421 422 def checknested(self, path):
422 423 return self._repo._checknested(self._repo.wjoin(path))
423 424
424 425 def commit(self, text, user, date):
425 426 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
426 427 n = self._repo.commit(text, user, date)
427 428 if not n:
428 429 return self._repo['.'].hex() # different version checked out
429 430 return node.hex(n)
430 431
431 432 def remove(self):
432 433 # we can't fully delete the repository as it may contain
433 434 # local-only history
434 435 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
435 436 hg.clean(self._repo, node.nullid, False)
436 437
437 438 def _get(self, state):
438 439 source, revision, kind = state
439 440 try:
440 441 self._repo.lookup(revision)
441 442 except error.RepoError:
442 443 self._repo._subsource = source
443 444 srcurl = _abssource(self._repo)
444 445 self._repo.ui.status(_('pulling subrepo %s from %s\n')
445 446 % (subrelpath(self), srcurl))
446 447 other = hg.repository(self._repo.ui, srcurl)
447 448 self._repo.pull(other)
448 449
449 450 def get(self, state, overwrite=False):
450 451 self._get(state)
451 452 source, revision, kind = state
452 453 self._repo.ui.debug("getting subrepo %s\n" % self._path)
453 454 hg.clean(self._repo, revision, False)
454 455
455 456 def merge(self, state):
456 457 self._get(state)
457 458 cur = self._repo['.']
458 459 dst = self._repo[state[1]]
459 460 anc = dst.ancestor(cur)
460 461
461 462 def mergefunc():
462 463 if anc == cur:
463 464 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
464 465 hg.update(self._repo, state[1])
465 466 elif anc == dst:
466 467 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
467 468 else:
468 469 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
469 470 hg.merge(self._repo, state[1], remind=False)
470 471
471 472 wctx = self._repo[None]
472 473 if self.dirty():
473 474 if anc != dst:
474 475 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
475 476 mergefunc()
476 477 else:
477 478 mergefunc()
478 479 else:
479 480 mergefunc()
480 481
481 482 def push(self, force):
482 483 # push subrepos depth-first for coherent ordering
483 484 c = self._repo['']
484 485 subs = c.substate # only repos that are committed
485 486 for s in sorted(subs):
486 487 if not c.sub(s).push(force):
487 488 return False
488 489
489 490 dsturl = _abssource(self._repo, True)
490 491 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
491 492 (subrelpath(self), dsturl))
492 493 other = hg.repository(self._repo.ui, dsturl)
493 494 return self._repo.push(other, force)
494 495
495 496 def outgoing(self, ui, dest, opts):
496 497 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
497 498
498 499 def incoming(self, ui, source, opts):
499 500 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
500 501
501 502 def files(self):
502 503 rev = self._state[1]
503 504 ctx = self._repo[rev]
504 505 return ctx.manifest()
505 506
506 507 def filedata(self, name):
507 508 rev = self._state[1]
508 509 return self._repo[rev][name].data()
509 510
510 511 def fileflags(self, name):
511 512 rev = self._state[1]
512 513 ctx = self._repo[rev]
513 514 return ctx.flags(name)
514 515
515 516
516 517 class svnsubrepo(abstractsubrepo):
517 518 def __init__(self, ctx, path, state):
518 519 self._path = path
519 520 self._state = state
520 521 self._ctx = ctx
521 522 self._ui = ctx._repo.ui
522 523
523 524 def _svncommand(self, commands, filename=''):
524 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
525 525 cmd = ['svn']
526 526 # Starting in svn 1.5 --non-interactive is a global flag
527 527 # instead of being per-command, but we need to support 1.4 so
528 528 # we have to be intelligent about what commands take
529 529 # --non-interactive.
530 530 if (not self._ui.interactive() and
531 531 commands[0] in ('update', 'checkout', 'commit')):
532 532 cmd.append('--non-interactive')
533 533 cmd.extend(commands)
534 cmd.append(path)
534 if filename is not None:
535 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
536 cmd.append(path)
535 537 env = dict(os.environ)
536 538 # Avoid localized output, preserve current locale for everything else.
537 539 env['LC_MESSAGES'] = 'C'
538 540 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
539 541 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
540 542 universal_newlines=True, env=env)
541 543 stdout, stderr = p.communicate()
542 544 stderr = stderr.strip()
543 545 if stderr:
544 546 raise util.Abort(stderr)
545 547 return stdout
546 548
549 @propertycache
550 def _svnversion(self):
551 output = self._svncommand(['--version'], filename=None)
552 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
553 if not m:
554 raise util.Abort(_('cannot retrieve svn tool version'))
555 return (int(m.group(1)), int(m.group(2)))
556
547 557 def _wcrevs(self):
548 558 # Get the working directory revision as well as the last
549 559 # commit revision so we can compare the subrepo state with
550 560 # both. We used to store the working directory one.
551 561 output = self._svncommand(['info', '--xml'])
552 562 doc = xml.dom.minidom.parseString(output)
553 563 entries = doc.getElementsByTagName('entry')
554 564 lastrev, rev = '0', '0'
555 565 if entries:
556 566 rev = str(entries[0].getAttribute('revision')) or '0'
557 567 commits = entries[0].getElementsByTagName('commit')
558 568 if commits:
559 569 lastrev = str(commits[0].getAttribute('revision')) or '0'
560 570 return (lastrev, rev)
561 571
562 572 def _wcrev(self):
563 573 return self._wcrevs()[0]
564 574
565 575 def _wcchanged(self):
566 576 """Return (changes, extchanges) where changes is True
567 577 if the working directory was changed, and extchanges is
568 578 True if any of these changes concern an external entry.
569 579 """
570 580 output = self._svncommand(['status', '--xml'])
571 581 externals, changes = [], []
572 582 doc = xml.dom.minidom.parseString(output)
573 583 for e in doc.getElementsByTagName('entry'):
574 584 s = e.getElementsByTagName('wc-status')
575 585 if not s:
576 586 continue
577 587 item = s[0].getAttribute('item')
578 588 props = s[0].getAttribute('props')
579 589 path = e.getAttribute('path')
580 590 if item == 'external':
581 591 externals.append(path)
582 592 if (item not in ('', 'normal', 'unversioned', 'external')
583 593 or props not in ('', 'none')):
584 594 changes.append(path)
585 595 for path in changes:
586 596 for ext in externals:
587 597 if path == ext or path.startswith(ext + os.sep):
588 598 return True, True
589 599 return bool(changes), False
590 600
591 601 def dirty(self, ignoreupdate=False):
592 602 if not self._wcchanged()[0]:
593 603 if self._state[1] in self._wcrevs() or ignoreupdate:
594 604 return False
595 605 return True
596 606
597 607 def commit(self, text, user, date):
598 608 # user and date are out of our hands since svn is centralized
599 609 changed, extchanged = self._wcchanged()
600 610 if not changed:
601 611 return self._wcrev()
602 612 if extchanged:
603 613 # Do not try to commit externals
604 614 raise util.Abort(_('cannot commit svn externals'))
605 615 commitinfo = self._svncommand(['commit', '-m', text])
606 616 self._ui.status(commitinfo)
607 617 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
608 618 if not newrev:
609 619 raise util.Abort(commitinfo.splitlines()[-1])
610 620 newrev = newrev.groups()[0]
611 621 self._ui.status(self._svncommand(['update', '-r', newrev]))
612 622 return newrev
613 623
614 624 def remove(self):
615 625 if self.dirty():
616 626 self._ui.warn(_('not removing repo %s because '
617 627 'it has changes.\n' % self._path))
618 628 return
619 629 self._ui.note(_('removing subrepo %s\n') % self._path)
620 630
621 631 def onerror(function, path, excinfo):
622 632 if function is not os.remove:
623 633 raise
624 634 # read-only files cannot be unlinked under Windows
625 635 s = os.stat(path)
626 636 if (s.st_mode & stat.S_IWRITE) != 0:
627 637 raise
628 638 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
629 639 os.remove(path)
630 640
631 641 path = self._ctx._repo.wjoin(self._path)
632 642 shutil.rmtree(path, onerror=onerror)
633 643 try:
634 644 os.removedirs(os.path.dirname(path))
635 645 except OSError:
636 646 pass
637 647
638 648 def get(self, state, overwrite=False):
639 649 if overwrite:
640 650 self._svncommand(['revert', '--recursive'])
641 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
651 args = ['checkout']
652 if self._svnversion >= (1, 5):
653 args.append('--force')
654 args.extend([state[0], '--revision', state[1]])
655 status = self._svncommand(args)
642 656 if not re.search('Checked out revision [0-9]+.', status):
643 657 raise util.Abort(status.splitlines()[-1])
644 658 self._ui.status(status)
645 659
646 660 def merge(self, state):
647 661 old = self._state[1]
648 662 new = state[1]
649 663 if new != self._wcrev():
650 664 dirty = old == self._wcrev() or self._wcchanged()[0]
651 665 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
652 666 self.get(state, False)
653 667
654 668 def push(self, force):
655 669 # push is a no-op for SVN
656 670 return True
657 671
658 672 def files(self):
659 673 output = self._svncommand(['list'])
660 674 # This works because svn forbids \n in filenames.
661 675 return output.splitlines()
662 676
663 677 def filedata(self, name):
664 678 return self._svncommand(['cat'], name)
665 679
666 680
667 681 class gitsubrepo(abstractsubrepo):
668 682 def __init__(self, ctx, path, state):
669 683 # TODO add git version check.
670 684 self._state = state
671 685 self._ctx = ctx
672 686 self._path = path
673 687 self._relpath = os.path.join(reporelpath(ctx._repo), path)
674 688 self._abspath = ctx._repo.wjoin(path)
675 689 self._subparent = ctx._repo
676 690 self._ui = ctx._repo.ui
677 691
678 692 def _gitcommand(self, commands, env=None, stream=False):
679 693 return self._gitdir(commands, env=env, stream=stream)[0]
680 694
681 695 def _gitdir(self, commands, env=None, stream=False):
682 696 return self._gitnodir(commands, env=env, stream=stream,
683 697 cwd=self._abspath)
684 698
685 699 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
686 700 """Calls the git command
687 701
688 702 The methods tries to call the git command. versions previor to 1.6.0
689 703 are not supported and very probably fail.
690 704 """
691 705 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
692 706 # unless ui.quiet is set, print git's stderr,
693 707 # which is mostly progress and useful info
694 708 errpipe = None
695 709 if self._ui.quiet:
696 710 errpipe = open(os.devnull, 'w')
697 711 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
698 712 close_fds=util.closefds,
699 713 stdout=subprocess.PIPE, stderr=errpipe)
700 714 if stream:
701 715 return p.stdout, None
702 716
703 717 retdata = p.stdout.read().strip()
704 718 # wait for the child to exit to avoid race condition.
705 719 p.wait()
706 720
707 721 if p.returncode != 0 and p.returncode != 1:
708 722 # there are certain error codes that are ok
709 723 command = commands[0]
710 724 if command in ('cat-file', 'symbolic-ref'):
711 725 return retdata, p.returncode
712 726 # for all others, abort
713 727 raise util.Abort('git %s error %d in %s' %
714 728 (command, p.returncode, self._relpath))
715 729
716 730 return retdata, p.returncode
717 731
718 732 def _gitmissing(self):
719 733 return not os.path.exists(os.path.join(self._abspath, '.git'))
720 734
721 735 def _gitstate(self):
722 736 return self._gitcommand(['rev-parse', 'HEAD'])
723 737
724 738 def _gitcurrentbranch(self):
725 739 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
726 740 if err:
727 741 current = None
728 742 return current
729 743
730 744 def _githavelocally(self, revision):
731 745 out, code = self._gitdir(['cat-file', '-e', revision])
732 746 return code == 0
733 747
734 748 def _gitisancestor(self, r1, r2):
735 749 base = self._gitcommand(['merge-base', r1, r2])
736 750 return base == r1
737 751
738 752 def _gitbranchmap(self):
739 753 '''returns 2 things:
740 754 a map from git branch to revision
741 755 a map from revision to branches'''
742 756 branch2rev = {}
743 757 rev2branch = {}
744 758
745 759 out = self._gitcommand(['for-each-ref', '--format',
746 760 '%(objectname) %(refname)'])
747 761 for line in out.split('\n'):
748 762 revision, ref = line.split(' ')
749 763 if (not ref.startswith('refs/heads/') and
750 764 not ref.startswith('refs/remotes/')):
751 765 continue
752 766 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
753 767 continue # ignore remote/HEAD redirects
754 768 branch2rev[ref] = revision
755 769 rev2branch.setdefault(revision, []).append(ref)
756 770 return branch2rev, rev2branch
757 771
758 772 def _gittracking(self, branches):
759 773 'return map of remote branch to local tracking branch'
760 774 # assumes no more than one local tracking branch for each remote
761 775 tracking = {}
762 776 for b in branches:
763 777 if b.startswith('refs/remotes/'):
764 778 continue
765 779 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
766 780 if remote:
767 781 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
768 782 tracking['refs/remotes/%s/%s' %
769 783 (remote, ref.split('/', 2)[2])] = b
770 784 return tracking
771 785
772 786 def _abssource(self, source):
773 787 if '://' not in source:
774 788 # recognize the scp syntax as an absolute source
775 789 colon = source.find(':')
776 790 if colon != -1 and '/' not in source[:colon]:
777 791 return source
778 792 self._subsource = source
779 793 return _abssource(self)
780 794
781 795 def _fetch(self, source, revision):
782 796 if self._gitmissing():
783 797 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
784 798 self._gitnodir(['clone', self._abssource(source), self._abspath])
785 799 if self._githavelocally(revision):
786 800 return
787 801 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
788 802 # try only origin: the originally cloned repo
789 803 self._gitcommand(['fetch'])
790 804 if not self._githavelocally(revision):
791 805 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
792 806 (revision, self._relpath))
793 807
794 808 def dirty(self, ignoreupdate=False):
795 809 if self._gitmissing():
796 810 return True
797 811 if not ignoreupdate and self._state[1] != self._gitstate():
798 812 # different version checked out
799 813 return True
800 814 # check for staged changes or modified files; ignore untracked files
801 815 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
802 816 return code == 1
803 817
804 818 def get(self, state, overwrite=False):
805 819 source, revision, kind = state
806 820 self._fetch(source, revision)
807 821 # if the repo was set to be bare, unbare it
808 822 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
809 823 self._gitcommand(['config', 'core.bare', 'false'])
810 824 if self._gitstate() == revision:
811 825 self._gitcommand(['reset', '--hard', 'HEAD'])
812 826 return
813 827 elif self._gitstate() == revision:
814 828 if overwrite:
815 829 # first reset the index to unmark new files for commit, because
816 830 # reset --hard will otherwise throw away files added for commit,
817 831 # not just unmark them.
818 832 self._gitcommand(['reset', 'HEAD'])
819 833 self._gitcommand(['reset', '--hard', 'HEAD'])
820 834 return
821 835 branch2rev, rev2branch = self._gitbranchmap()
822 836
823 837 def checkout(args):
824 838 cmd = ['checkout']
825 839 if overwrite:
826 840 # first reset the index to unmark new files for commit, because
827 841 # the -f option will otherwise throw away files added for
828 842 # commit, not just unmark them.
829 843 self._gitcommand(['reset', 'HEAD'])
830 844 cmd.append('-f')
831 845 self._gitcommand(cmd + args)
832 846
833 847 def rawcheckout():
834 848 # no branch to checkout, check it out with no branch
835 849 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
836 850 self._relpath)
837 851 self._ui.warn(_('check out a git branch if you intend '
838 852 'to make changes\n'))
839 853 checkout(['-q', revision])
840 854
841 855 if revision not in rev2branch:
842 856 rawcheckout()
843 857 return
844 858 branches = rev2branch[revision]
845 859 firstlocalbranch = None
846 860 for b in branches:
847 861 if b == 'refs/heads/master':
848 862 # master trumps all other branches
849 863 checkout(['refs/heads/master'])
850 864 return
851 865 if not firstlocalbranch and not b.startswith('refs/remotes/'):
852 866 firstlocalbranch = b
853 867 if firstlocalbranch:
854 868 checkout([firstlocalbranch])
855 869 return
856 870
857 871 tracking = self._gittracking(branch2rev.keys())
858 872 # choose a remote branch already tracked if possible
859 873 remote = branches[0]
860 874 if remote not in tracking:
861 875 for b in branches:
862 876 if b in tracking:
863 877 remote = b
864 878 break
865 879
866 880 if remote not in tracking:
867 881 # create a new local tracking branch
868 882 local = remote.split('/', 2)[2]
869 883 checkout(['-b', local, remote])
870 884 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
871 885 # When updating to a tracked remote branch,
872 886 # if the local tracking branch is downstream of it,
873 887 # a normal `git pull` would have performed a "fast-forward merge"
874 888 # which is equivalent to updating the local branch to the remote.
875 889 # Since we are only looking at branching at update, we need to
876 890 # detect this situation and perform this action lazily.
877 891 if tracking[remote] != self._gitcurrentbranch():
878 892 checkout([tracking[remote]])
879 893 self._gitcommand(['merge', '--ff', remote])
880 894 else:
881 895 # a real merge would be required, just checkout the revision
882 896 rawcheckout()
883 897
884 898 def commit(self, text, user, date):
885 899 if self._gitmissing():
886 900 raise util.Abort(_("subrepo %s is missing") % self._relpath)
887 901 cmd = ['commit', '-a', '-m', text]
888 902 env = os.environ.copy()
889 903 if user:
890 904 cmd += ['--author', user]
891 905 if date:
892 906 # git's date parser silently ignores when seconds < 1e9
893 907 # convert to ISO8601
894 908 env['GIT_AUTHOR_DATE'] = util.datestr(date,
895 909 '%Y-%m-%dT%H:%M:%S %1%2')
896 910 self._gitcommand(cmd, env=env)
897 911 # make sure commit works otherwise HEAD might not exist under certain
898 912 # circumstances
899 913 return self._gitstate()
900 914
901 915 def merge(self, state):
902 916 source, revision, kind = state
903 917 self._fetch(source, revision)
904 918 base = self._gitcommand(['merge-base', revision, self._state[1]])
905 919 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
906 920
907 921 def mergefunc():
908 922 if base == revision:
909 923 self.get(state) # fast forward merge
910 924 elif base != self._state[1]:
911 925 self._gitcommand(['merge', '--no-commit', revision])
912 926
913 927 if self.dirty():
914 928 if self._gitstate() != revision:
915 929 dirty = self._gitstate() == self._state[1] or code != 0
916 930 if _updateprompt(self._ui, self, dirty,
917 931 self._state[1][:7], revision[:7]):
918 932 mergefunc()
919 933 else:
920 934 mergefunc()
921 935
922 936 def push(self, force):
923 937 if self._gitmissing():
924 938 raise util.Abort(_("subrepo %s is missing") % self._relpath)
925 939 # if a branch in origin contains the revision, nothing to do
926 940 branch2rev, rev2branch = self._gitbranchmap()
927 941 if self._state[1] in rev2branch:
928 942 for b in rev2branch[self._state[1]]:
929 943 if b.startswith('refs/remotes/origin/'):
930 944 return True
931 945 for b, revision in branch2rev.iteritems():
932 946 if b.startswith('refs/remotes/origin/'):
933 947 if self._gitisancestor(self._state[1], revision):
934 948 return True
935 949 # otherwise, try to push the currently checked out branch
936 950 cmd = ['push']
937 951 if force:
938 952 cmd.append('--force')
939 953
940 954 current = self._gitcurrentbranch()
941 955 if current:
942 956 # determine if the current branch is even useful
943 957 if not self._gitisancestor(self._state[1], current):
944 958 self._ui.warn(_('unrelated git branch checked out '
945 959 'in subrepo %s\n') % self._relpath)
946 960 return False
947 961 self._ui.status(_('pushing branch %s of subrepo %s\n') %
948 962 (current.split('/', 2)[2], self._relpath))
949 963 self._gitcommand(cmd + ['origin', current])
950 964 return True
951 965 else:
952 966 self._ui.warn(_('no branch checked out in subrepo %s\n'
953 967 'cannot push revision %s') %
954 968 (self._relpath, self._state[1]))
955 969 return False
956 970
957 971 def remove(self):
958 972 if self._gitmissing():
959 973 return
960 974 if self.dirty():
961 975 self._ui.warn(_('not removing repo %s because '
962 976 'it has changes.\n') % self._relpath)
963 977 return
964 978 # we can't fully delete the repository as it may contain
965 979 # local-only history
966 980 self._ui.note(_('removing subrepo %s\n') % self._relpath)
967 981 self._gitcommand(['config', 'core.bare', 'true'])
968 982 for f in os.listdir(self._abspath):
969 983 if f == '.git':
970 984 continue
971 985 path = os.path.join(self._abspath, f)
972 986 if os.path.isdir(path) and not os.path.islink(path):
973 987 shutil.rmtree(path)
974 988 else:
975 989 os.remove(path)
976 990
977 991 def archive(self, ui, archiver, prefix):
978 992 source, revision = self._state
979 993 self._fetch(source, revision)
980 994
981 995 # Parse git's native archive command.
982 996 # This should be much faster than manually traversing the trees
983 997 # and objects with many subprocess calls.
984 998 tarstream = self._gitcommand(['archive', revision], stream=True)
985 999 tar = tarfile.open(fileobj=tarstream, mode='r|')
986 1000 relpath = subrelpath(self)
987 1001 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
988 1002 for i, info in enumerate(tar):
989 1003 if info.isdir():
990 1004 continue
991 1005 if info.issym():
992 1006 data = info.linkname
993 1007 else:
994 1008 data = tar.extractfile(info).read()
995 1009 archiver.addfile(os.path.join(prefix, self._path, info.name),
996 1010 info.mode, info.issym(), data)
997 1011 ui.progress(_('archiving (%s)') % relpath, i + 1,
998 1012 unit=_('files'))
999 1013 ui.progress(_('archiving (%s)') % relpath, None)
1000 1014
1001 1015
1002 1016 def status(self, rev2, **opts):
1003 1017 if self._gitmissing():
1004 1018 # if the repo is missing, return no results
1005 1019 return [], [], [], [], [], [], []
1006 1020 rev1 = self._state[1]
1007 1021 modified, added, removed = [], [], []
1008 1022 if rev2:
1009 1023 command = ['diff-tree', rev1, rev2]
1010 1024 else:
1011 1025 command = ['diff-index', rev1]
1012 1026 out = self._gitcommand(command)
1013 1027 for line in out.split('\n'):
1014 1028 tab = line.find('\t')
1015 1029 if tab == -1:
1016 1030 continue
1017 1031 status, f = line[tab - 1], line[tab + 1:]
1018 1032 if status == 'M':
1019 1033 modified.append(f)
1020 1034 elif status == 'A':
1021 1035 added.append(f)
1022 1036 elif status == 'D':
1023 1037 removed.append(f)
1024 1038
1025 1039 deleted = unknown = ignored = clean = []
1026 1040 return modified, added, removed, deleted, unknown, ignored, clean
1027 1041
1028 1042 types = {
1029 1043 'hg': hgsubrepo,
1030 1044 'svn': svnsubrepo,
1031 1045 'git': gitsubrepo,
1032 1046 }
@@ -1,278 +1,288 b''
1 1 #!/usr/bin/env python
2 2 """Test the running system for features availability. Exit with zero
3 3 if all features are there, non-zero otherwise. If a feature name is
4 4 prefixed with "no-", the absence of feature is tested.
5 5 """
6 6 import optparse
7 7 import os
8 8 import re
9 9 import sys
10 10 import tempfile
11 11
12 12 tempprefix = 'hg-hghave-'
13 13
14 14 def matchoutput(cmd, regexp, ignorestatus=False):
15 15 """Return True if cmd executes successfully and its output
16 16 is matched by the supplied regular expression.
17 17 """
18 18 r = re.compile(regexp)
19 19 fh = os.popen(cmd)
20 20 s = fh.read()
21 21 try:
22 22 ret = fh.close()
23 23 except IOError:
24 24 # Happen in Windows test environment
25 25 ret = 1
26 26 return (ignorestatus or ret is None) and r.search(s)
27 27
28 28 def has_baz():
29 29 return matchoutput('baz --version 2>&1', r'baz Bazaar version')
30 30
31 31 def has_bzr():
32 32 try:
33 33 import bzrlib
34 34 return bzrlib.__doc__ != None
35 35 except ImportError:
36 36 return False
37 37
38 38 def has_bzr114():
39 39 try:
40 40 import bzrlib
41 41 return (bzrlib.__doc__ != None
42 42 and bzrlib.version_info[:2] >= (1, 14))
43 43 except ImportError:
44 44 return False
45 45
46 46 def has_cvs():
47 47 re = r'Concurrent Versions System.*?server'
48 48 return matchoutput('cvs --version 2>&1', re)
49 49
50 50 def has_darcs():
51 51 return matchoutput('darcs --version', r'2\.[2-9]', True)
52 52
53 53 def has_mtn():
54 54 return matchoutput('mtn --version', r'monotone', True) and not matchoutput(
55 55 'mtn --version', r'monotone 0\.(\d|[12]\d|3[01])[^\d]', True)
56 56
57 57 def has_eol_in_paths():
58 58 try:
59 59 fd, path = tempfile.mkstemp(prefix=tempprefix, suffix='\n\r')
60 60 os.close(fd)
61 61 os.remove(path)
62 62 return True
63 63 except:
64 64 return False
65 65
66 66 def has_executablebit():
67 67 fd, path = tempfile.mkstemp(prefix=tempprefix)
68 68 os.close(fd)
69 69 try:
70 70 s = os.lstat(path).st_mode
71 71 os.chmod(path, s | 0100)
72 72 return (os.lstat(path).st_mode & 0100 != 0)
73 73 finally:
74 74 os.remove(path)
75 75
76 76 def has_icasefs():
77 77 # Stolen from mercurial.util
78 78 fd, path = tempfile.mkstemp(prefix=tempprefix, dir='.')
79 79 os.close(fd)
80 80 try:
81 81 s1 = os.stat(path)
82 82 d, b = os.path.split(path)
83 83 p2 = os.path.join(d, b.upper())
84 84 if path == p2:
85 85 p2 = os.path.join(d, b.lower())
86 86 try:
87 87 s2 = os.stat(p2)
88 88 return s2 == s1
89 89 except:
90 90 return False
91 91 finally:
92 92 os.remove(path)
93 93
94 94 def has_inotify():
95 95 try:
96 96 import hgext.inotify.linux.watcher
97 97 return True
98 98 except ImportError:
99 99 return False
100 100
101 101 def has_fifo():
102 102 return hasattr(os, "mkfifo")
103 103
104 104 def has_lsprof():
105 105 try:
106 106 import _lsprof
107 107 return True
108 108 except ImportError:
109 109 return False
110 110
111 111 def has_gettext():
112 112 return matchoutput('msgfmt --version', 'GNU gettext-tools')
113 113
114 114 def has_git():
115 115 return matchoutput('git --version 2>&1', r'^git version')
116 116
117 117 def has_docutils():
118 118 try:
119 119 from docutils.core import publish_cmdline
120 120 return True
121 121 except ImportError:
122 122 return False
123 123
124 def getsvnversion():
125 m = matchoutput('svn --version 2>&1', r'^svn,\s+version\s+(\d+)\.(\d+)')
126 if not m:
127 return (0, 0)
128 return (int(m.group(1)), int(m.group(2)))
129
130 def has_svn15():
131 return getsvnversion() >= (1, 5)
132
124 133 def has_svn():
125 134 return matchoutput('svn --version 2>&1', r'^svn, version') and \
126 135 matchoutput('svnadmin --version 2>&1', r'^svnadmin, version')
127 136
128 137 def has_svn_bindings():
129 138 try:
130 139 import svn.core
131 140 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
132 141 if version < (1, 4):
133 142 return False
134 143 return True
135 144 except ImportError:
136 145 return False
137 146
138 147 def has_p4():
139 148 return matchoutput('p4 -V', r'Rev\. P4/') and matchoutput('p4d -V', r'Rev\. P4D/')
140 149
141 150 def has_symlink():
142 151 return hasattr(os, "symlink")
143 152
144 153 def has_tla():
145 154 return matchoutput('tla --version 2>&1', r'The GNU Arch Revision')
146 155
147 156 def has_gpg():
148 157 return matchoutput('gpg --version 2>&1', r'GnuPG')
149 158
150 159 def has_unix_permissions():
151 160 d = tempfile.mkdtemp(prefix=tempprefix, dir=".")
152 161 try:
153 162 fname = os.path.join(d, 'foo')
154 163 for umask in (077, 007, 022):
155 164 os.umask(umask)
156 165 f = open(fname, 'w')
157 166 f.close()
158 167 mode = os.stat(fname).st_mode
159 168 os.unlink(fname)
160 169 if mode & 0777 != ~umask & 0666:
161 170 return False
162 171 return True
163 172 finally:
164 173 os.rmdir(d)
165 174
166 175 def has_pygments():
167 176 try:
168 177 import pygments
169 178 return True
170 179 except ImportError:
171 180 return False
172 181
173 182 def has_outer_repo():
174 183 return matchoutput('hg root 2>&1', r'')
175 184
176 185 def has_ssl():
177 186 try:
178 187 import ssl
179 188 import OpenSSL
180 189 OpenSSL.SSL.Context
181 190 return True
182 191 except ImportError:
183 192 return False
184 193
185 194 checks = {
186 195 "baz": (has_baz, "GNU Arch baz client"),
187 196 "bzr": (has_bzr, "Canonical's Bazaar client"),
188 197 "bzr114": (has_bzr114, "Canonical's Bazaar client >= 1.14"),
189 198 "cvs": (has_cvs, "cvs client/server"),
190 199 "darcs": (has_darcs, "darcs client"),
191 200 "docutils": (has_docutils, "Docutils text processing library"),
192 201 "eol-in-paths": (has_eol_in_paths, "end-of-lines in paths"),
193 202 "execbit": (has_executablebit, "executable bit"),
194 203 "fifo": (has_fifo, "named pipes"),
195 204 "gettext": (has_gettext, "GNU Gettext (msgfmt)"),
196 205 "git": (has_git, "git command line client"),
197 206 "gpg": (has_gpg, "gpg client"),
198 207 "icasefs": (has_icasefs, "case insensitive file system"),
199 208 "inotify": (has_inotify, "inotify extension support"),
200 209 "lsprof": (has_lsprof, "python lsprof module"),
201 210 "mtn": (has_mtn, "monotone client (> 0.31)"),
202 211 "outer-repo": (has_outer_repo, "outer repo"),
203 212 "p4": (has_p4, "Perforce server and client"),
204 213 "pygments": (has_pygments, "Pygments source highlighting library"),
205 214 "ssl": (has_ssl, "python >= 2.6 ssl module and python OpenSSL"),
206 215 "svn": (has_svn, "subversion client and admin tools"),
216 "svn15": (has_svn15, "subversion client and admin tools >= 1.5"),
207 217 "svn-bindings": (has_svn_bindings, "subversion python bindings"),
208 218 "symlink": (has_symlink, "symbolic links"),
209 219 "tla": (has_tla, "GNU Arch tla client"),
210 220 "unix-permissions": (has_unix_permissions, "unix-style permissions"),
211 221 }
212 222
213 223 def list_features():
214 224 for name, feature in checks.iteritems():
215 225 desc = feature[1]
216 226 print name + ':', desc
217 227
218 228 def test_features():
219 229 failed = 0
220 230 for name, feature in checks.iteritems():
221 231 check, _ = feature
222 232 try:
223 233 check()
224 234 except Exception, e:
225 235 print "feature %s failed: %s" % (name, e)
226 236 failed += 1
227 237 return failed
228 238
229 239 parser = optparse.OptionParser("%prog [options] [features]")
230 240 parser.add_option("--test-features", action="store_true",
231 241 help="test available features")
232 242 parser.add_option("--list-features", action="store_true",
233 243 help="list available features")
234 244 parser.add_option("-q", "--quiet", action="store_true",
235 245 help="check features silently")
236 246
237 247 if __name__ == '__main__':
238 248 options, args = parser.parse_args()
239 249 if options.list_features:
240 250 list_features()
241 251 sys.exit(0)
242 252
243 253 if options.test_features:
244 254 sys.exit(test_features())
245 255
246 256 quiet = options.quiet
247 257
248 258 failures = 0
249 259
250 260 def error(msg):
251 261 global failures
252 262 if not quiet:
253 263 sys.stderr.write(msg + '\n')
254 264 failures += 1
255 265
256 266 for feature in args:
257 267 negate = feature.startswith('no-')
258 268 if negate:
259 269 feature = feature[3:]
260 270
261 271 if feature not in checks:
262 272 error('skipped: unknown feature: ' + feature)
263 273 continue
264 274
265 275 check, desc = checks[feature]
266 276 try:
267 277 available = check()
268 278 except Exception, e:
269 279 error('hghave check failed: ' + feature)
270 280 continue
271 281
272 282 if not negate and not available:
273 283 error('skipped: missing feature: ' + desc)
274 284 elif negate and available:
275 285 error('skipped: system supports %s' % desc)
276 286
277 287 if failures != 0:
278 288 sys.exit(1)
@@ -1,440 +1,491 b''
1 1 $ "$TESTDIR/hghave" svn || exit 80
2 2
3 3 $ fix_path()
4 4 > {
5 5 > tr '\\' /
6 6 > }
7 7
8 8 SVN wants all paths to start with a slash. Unfortunately, Windows ones
9 9 don't. Handle that.
10 10
11 11 $ escapedwd=`pwd | fix_path`
12 12 $ expr "$escapedwd" : '\/' > /dev/null || escapedwd="/$escapedwd"
13 13 $ escapedwd=`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$escapedwd"`
14 14
15 15 create subversion repo
16 16
17 17 $ SVNREPO="file://$escapedwd/svn-repo"
18 18 $ WCROOT="`pwd`/svn-wc"
19 19 $ svnadmin create svn-repo
20 20 $ svn co "$SVNREPO" svn-wc
21 21 Checked out revision 0.
22 22 $ cd svn-wc
23 23 $ mkdir src
24 24 $ echo alpha > src/alpha
25 25 $ svn add src
26 26 A src
27 27 A src/alpha
28 28 $ mkdir externals
29 29 $ echo other > externals/other
30 30 $ svn add externals
31 31 A externals
32 32 A externals/other
33 33 $ svn ci -m 'Add alpha'
34 34 Adding externals
35 35 Adding externals/other
36 36 Adding src
37 37 Adding src/alpha
38 38 Transmitting file data ..
39 39 Committed revision 1.
40 40 $ svn up
41 41 At revision 1.
42 42 $ echo "externals -r1 $SVNREPO/externals" > extdef
43 43 $ svn propset -F extdef svn:externals src
44 44 property 'svn:externals' set on 'src'
45 45 $ svn ci -m 'Setting externals'
46 46 Sending src
47 47
48 48 Committed revision 2.
49 49 $ cd ..
50 50
51 51 create hg repo
52 52
53 53 $ mkdir sub
54 54 $ cd sub
55 55 $ hg init t
56 56 $ cd t
57 57
58 58 first revision, no sub
59 59
60 60 $ echo a > a
61 61 $ hg ci -Am0
62 62 adding a
63 63
64 64 add first svn sub with leading whitespaces
65 65
66 66 $ echo "s = [svn] $SVNREPO/src" >> .hgsub
67 67 $ echo "subdir/s = [svn] $SVNREPO/src" >> .hgsub
68 68 $ svn co --quiet "$SVNREPO"/src s
69 69 $ mkdir subdir
70 70 $ svn co --quiet "$SVNREPO"/src subdir/s
71 71 $ hg add .hgsub
72 72 $ hg ci -m1
73 73 committing subrepository s
74 74 committing subrepository subdir/s
75 75
76 76 make sure we avoid empty commits (issue2445)
77 77
78 78 $ hg sum
79 79 parent: 1:* tip (glob)
80 80 1
81 81 branch: default
82 82 commit: (clean)
83 83 update: (current)
84 84 $ hg ci -moops
85 85 nothing changed
86 86 [1]
87 87
88 88 debugsub
89 89
90 90 $ hg debugsub
91 91 path s
92 92 source file://*/svn-repo/src (glob)
93 93 revision 2
94 94 path subdir/s
95 95 source file://*/svn-repo/src (glob)
96 96 revision 2
97 97
98 98 change file in svn and hg, commit
99 99
100 100 $ echo a >> a
101 101 $ echo alpha >> s/alpha
102 102 $ hg sum
103 103 parent: 1:* tip (glob)
104 104 1
105 105 branch: default
106 106 commit: 1 modified, 1 subrepos
107 107 update: (current)
108 108 $ hg commit -m 'Message!'
109 109 committing subrepository s
110 110 Sending*s/alpha (glob)
111 111 Transmitting file data .
112 112 Committed revision 3.
113 113
114 114 Fetching external item into '$TESTTMP/sub/t/s/externals'
115 115 External at revision 1.
116 116
117 117 At revision 3.
118 118 $ hg debugsub
119 119 path s
120 120 source file://*/svn-repo/src (glob)
121 121 revision 3
122 122 path subdir/s
123 123 source file://*/svn-repo/src (glob)
124 124 revision 2
125 125
126 126 add an unrelated revision in svn and update the subrepo to without
127 127 bringing any changes.
128 128
129 129 $ svn mkdir "$SVNREPO/unrelated" -m 'create unrelated'
130 130
131 131 Committed revision 4.
132 132 $ svn up s
133 133
134 134 Fetching external item into 's/externals'
135 135 External at revision 1.
136 136
137 137 At revision 4.
138 138 $ hg sum
139 139 parent: 2:* tip (glob)
140 140 Message!
141 141 branch: default
142 142 commit: (clean)
143 143 update: (current)
144 144
145 145 $ echo a > s/a
146 146
147 147 should be empty despite change to s/a
148 148
149 149 $ hg st
150 150
151 151 add a commit from svn
152 152
153 153 $ cd "$WCROOT"/src
154 154 $ svn up
155 155 U alpha
156 156
157 157 Fetching external item into 'externals'
158 158 A externals/other
159 159 Updated external to revision 1.
160 160
161 161 Updated to revision 4.
162 162 $ echo xyz >> alpha
163 163 $ svn propset svn:mime-type 'text/xml' alpha
164 164 property 'svn:mime-type' set on 'alpha'
165 165 $ svn ci -m 'amend a from svn'
166 166 Sending src/alpha
167 167 Transmitting file data .
168 168 Committed revision 5.
169 169 $ cd ../../sub/t
170 170
171 171 this commit from hg will fail
172 172
173 173 $ echo zzz >> s/alpha
174 174 $ hg ci -m 'amend alpha from hg'
175 175 committing subrepository s
176 176 abort: svn: Commit failed (details follow):
177 177 svn: (Out of date)?.*/src/alpha.*(is out of date)? (re)
178 178 [255]
179 179 $ svn revert -q s/alpha
180 180
181 181 this commit fails because of meta changes
182 182
183 183 $ svn propset svn:mime-type 'text/html' s/alpha
184 184 property 'svn:mime-type' set on 's/alpha'
185 185 $ hg ci -m 'amend alpha from hg'
186 186 committing subrepository s
187 187 abort: svn: Commit failed (details follow):
188 188 svn: (Out of date)?.*/src/alpha.*(is out of date)? (re)
189 189 [255]
190 190 $ svn revert -q s/alpha
191 191
192 192 this commit fails because of externals changes
193 193
194 194 $ echo zzz > s/externals/other
195 195 $ hg ci -m 'amend externals from hg'
196 196 committing subrepository s
197 197 abort: cannot commit svn externals
198 198 [255]
199 199 $ hg diff --subrepos -r 1:2 | grep -v diff
200 200 --- a/.hgsubstate Thu Jan 01 00:00:00 1970 +0000
201 201 +++ b/.hgsubstate Thu Jan 01 00:00:00 1970 +0000
202 202 @@ -1,2 +1,2 @@
203 203 -2 s
204 204 +3 s
205 205 2 subdir/s
206 206 --- a/a Thu Jan 01 00:00:00 1970 +0000
207 207 +++ b/a Thu Jan 01 00:00:00 1970 +0000
208 208 @@ -1,1 +1,2 @@
209 209 a
210 210 +a
211 211 $ svn revert -q s/externals/other
212 212
213 213 this commit fails because of externals meta changes
214 214
215 215 $ svn propset svn:mime-type 'text/html' s/externals/other
216 216 property 'svn:mime-type' set on 's/externals/other'
217 217 $ hg ci -m 'amend externals from hg'
218 218 committing subrepository s
219 219 abort: cannot commit svn externals
220 220 [255]
221 221 $ svn revert -q s/externals/other
222 222
223 223 clone
224 224
225 225 $ cd ..
226 226 $ hg clone t tc | fix_path
227 227 updating to branch default
228 228 A tc/subdir/s/alpha
229 229 U tc/subdir/s
230 230
231 231 Fetching external item into 'tc/subdir/s/externals'
232 232 A tc/subdir/s/externals/other
233 233 Checked out external at revision 1.
234 234
235 235 Checked out revision 2.
236 236 A tc/s/alpha
237 237 U tc/s
238 238
239 239 Fetching external item into 'tc/s/externals'
240 240 A tc/s/externals/other
241 241 Checked out external at revision 1.
242 242
243 243 Checked out revision 3.
244 244 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
245 245 $ cd tc
246 246
247 247 debugsub in clone
248 248
249 249 $ hg debugsub
250 250 path s
251 251 source file://*/svn-repo/src (glob)
252 252 revision 3
253 253 path subdir/s
254 254 source file://*/svn-repo/src (glob)
255 255 revision 2
256 256
257 257 verify subrepo is contained within the repo directory
258 258
259 259 $ python -c "import os.path; print os.path.exists('s')"
260 260 True
261 261
262 262 update to nullrev (must delete the subrepo)
263 263
264 264 $ hg up null
265 265 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
266 266 $ ls
267 267
268 268 Check hg update --clean
269 269 $ cd $TESTTMP/sub/t
270 270 $ cd s
271 271 $ echo c0 > alpha
272 272 $ echo c1 > f1
273 273 $ echo c1 > f2
274 274 $ svn add f1 -q
275 275 $ svn status
276 276 ? * a (glob)
277 277 X * externals (glob)
278 278 ? * f2 (glob)
279 279 M * alpha (glob)
280 280 A * f1 (glob)
281 281
282 282 Performing status on external item at 'externals'
283 283 $ cd ../..
284 284 $ hg -R t update -C
285 285
286 286 Fetching external item into 't/s/externals'
287 287 Checked out external at revision 1.
288 288
289 289 Checked out revision 3.
290 290 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
291 291 $ cd t/s
292 292 $ svn status
293 293 ? * a (glob)
294 294 X * externals (glob)
295 295 ? * f1 (glob)
296 296 ? * f2 (glob)
297 297
298 298 Performing status on external item at 'externals'
299 299
300 300 Sticky subrepositories, no changes
301 301 $ cd $TESTTMP/sub/t
302 302 $ hg id -n
303 303 2
304 304 $ cd s
305 305 $ svnversion
306 306 3
307 307 $ cd ..
308 308 $ hg update 1
309 309 U $TESTTMP/sub/t/s/alpha
310 310
311 311 Fetching external item into '$TESTTMP/sub/t/s/externals'
312 312 Checked out external at revision 1.
313 313
314 314 Checked out revision 2.
315 315 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
316 316 $ hg id -n
317 317 1
318 318 $ cd s
319 319 $ svnversion
320 320 2
321 321 $ cd ..
322 322
323 323 Sticky subrepositorys, file changes
324 324 $ touch s/f1
325 325 $ cd s
326 326 $ svn add f1
327 327 A f1
328 328 $ cd ..
329 329 $ hg id -n
330 330 1
331 331 $ cd s
332 332 $ svnversion
333 333 2M
334 334 $ cd ..
335 335 $ hg update tip
336 336 subrepository sources for s differ
337 337 use (l)ocal source (2) or (r)emote source (3)?
338 338 l
339 339 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
340 340 $ hg id -n
341 341 2+
342 342 $ cd s
343 343 $ svnversion
344 344 2M
345 345 $ cd ..
346 346 $ hg update --clean tip
347 347 U $TESTTMP/sub/t/s/alpha
348 348
349 349 Fetching external item into '$TESTTMP/sub/t/s/externals'
350 350 Checked out external at revision 1.
351 351
352 352 Checked out revision 3.
353 353 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
354 354
355 355 Sticky subrepository, revision updates
356 356 $ hg id -n
357 357 2
358 358 $ cd s
359 359 $ svnversion
360 360 3
361 361 $ cd ..
362 362 $ cd s
363 363 $ svn update -r 1
364 364 U alpha
365 365 U .
366 366
367 367 Fetching external item into 'externals'
368 368 Updated external to revision 1.
369 369
370 370 Updated to revision 1.
371 371 $ cd ..
372 372 $ hg update 1
373 373 subrepository sources for s differ (in checked out version)
374 374 use (l)ocal source (1) or (r)emote source (2)?
375 375 l
376 376 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
377 377 $ hg id -n
378 378 1+
379 379 $ cd s
380 380 $ svnversion
381 381 1
382 382 $ cd ..
383 383
384 384 Sticky subrepository, file changes and revision updates
385 385 $ touch s/f1
386 386 $ cd s
387 387 $ svn add f1
388 388 A f1
389 389 $ svnversion
390 390 1M
391 391 $ cd ..
392 392 $ hg id -n
393 393 1+
394 394 $ hg update tip
395 395 subrepository sources for s differ
396 396 use (l)ocal source (1) or (r)emote source (3)?
397 397 l
398 398 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
399 399 $ hg id -n
400 400 2
401 401 $ cd s
402 402 $ svnversion
403 403 1M
404 404 $ cd ..
405 405
406 406 Sticky repository, update --clean
407 407 $ hg update --clean tip
408 408 U $TESTTMP/sub/t/s/alpha
409 409 U $TESTTMP/sub/t/s
410 410
411 411 Fetching external item into '$TESTTMP/sub/t/s/externals'
412 412 Checked out external at revision 1.
413 413
414 414 Checked out revision 3.
415 415 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
416 416 $ hg id -n
417 417 2
418 418 $ cd s
419 419 $ svnversion
420 420 3
421 421 $ cd ..
422 422
423 423 Test subrepo already at intended revision:
424 424 $ cd s
425 425 $ svn update -r 2
426 426 U alpha
427 427
428 428 Fetching external item into 'externals'
429 429 Updated external to revision 1.
430 430
431 431 Updated to revision 2.
432 432 $ cd ..
433 433 $ hg update 1
434 434 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
435 435 $ hg id -n
436 436 1+
437 437 $ cd s
438 438 $ svnversion
439 439 2
440 440 $ cd ..
441
442 Test case where subversion would fail to update the subrepo because there
443 are unknown directories being replaced by tracked ones (happens with rebase).
444
445 $ cd $WCROOT/src
446 $ mkdir dir
447 $ echo epsilon.py > dir/epsilon.py
448 $ svn add dir
449 A dir
450 A dir/epsilon.py
451 $ svn ci -m 'Add dir/epsilon.py'
452 Adding src/dir
453 Adding src/dir/epsilon.py
454 Transmitting file data .
455 Committed revision 6.
456 $ cd ../..
457 $ hg init rebaserepo
458 $ cd rebaserepo
459 $ svn co -r5 --quiet "$SVNREPO"/src s
460 $ echo "s = [svn] $SVNREPO/src" >> .hgsub
461 $ hg add .hgsub
462 $ hg ci -m addsub
463 committing subrepository s
464 $ echo a > a
465 $ hg ci -Am adda
466 adding a
467 $ hg up 0
468 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
469 $ svn up -r6 s
470 A s/dir
471 A s/dir/epsilon.py
472
473 Fetching external item into 's/externals'
474 Updated external to revision 1.
475
476 Updated to revision 6.
477 $ hg ci -m updatesub
478 committing subrepository s
479 created new head
480 $ echo pyc > s/dir/epsilon.pyc
481 $ hg up 1
482 D $TESTTMP/rebaserepo/s/dir
483
484 Fetching external item into '$TESTTMP/rebaserepo/s/externals'
485 Checked out external at revision 1.
486
487 Checked out revision 5.
488 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
489 $ if "$TESTDIR/hghave" -q svn15; then
490 > hg up 2 >/dev/null 2>&1 || echo update failed
491 > fi
General Comments 0
You need to be logged in to leave comments. Login now