##// END OF EJS Templates
subrepo: fix removing read-only svn files on Windows
Patrick Mezard -
r13013:92b0d669 stable
parent child Browse files
Show More
@@ -1,582 +1,594 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 import stat
9 10 from i18n import _
10 11 import config, util, node, error, cmdutil
11 12 hg = None
12 13
13 14 nullstate = ('', '', 'empty')
14 15
15 16 def state(ctx, ui):
16 17 """return a state dict, mapping subrepo paths configured in .hgsub
17 18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
18 19 (key in types dict))
19 20 """
20 21 p = config.config()
21 22 def read(f, sections=None, remap=None):
22 23 if f in ctx:
23 24 p.parse(f, ctx[f].data(), sections, remap, read)
24 25 else:
25 26 raise util.Abort(_("subrepo spec file %s not found") % f)
26 27
27 28 if '.hgsub' in ctx:
28 29 read('.hgsub')
29 30
30 31 for path, src in ui.configitems('subpaths'):
31 32 p.set('subpaths', path, src, ui.configsource('subpaths', path))
32 33
33 34 rev = {}
34 35 if '.hgsubstate' in ctx:
35 36 try:
36 37 for l in ctx['.hgsubstate'].data().splitlines():
37 38 revision, path = l.split(" ", 1)
38 39 rev[path] = revision
39 40 except IOError, err:
40 41 if err.errno != errno.ENOENT:
41 42 raise
42 43
43 44 state = {}
44 45 for path, src in p[''].items():
45 46 kind = 'hg'
46 47 if src.startswith('['):
47 48 if ']' not in src:
48 49 raise util.Abort(_('missing ] in subrepo source'))
49 50 kind, src = src.split(']', 1)
50 51 kind = kind[1:]
51 52
52 53 for pattern, repl in p.items('subpaths'):
53 54 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
54 55 # does a string decode.
55 56 repl = repl.encode('string-escape')
56 57 # However, we still want to allow back references to go
57 58 # through unharmed, so we turn r'\\1' into r'\1'. Again,
58 59 # extra escapes are needed because re.sub string decodes.
59 60 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
60 61 try:
61 62 src = re.sub(pattern, repl, src, 1)
62 63 except re.error, e:
63 64 raise util.Abort(_("bad subrepository pattern in %s: %s")
64 65 % (p.source('subpaths', pattern), e))
65 66
66 67 state[path] = (src.strip(), rev.get(path, ''), kind)
67 68
68 69 return state
69 70
70 71 def writestate(repo, state):
71 72 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
72 73 repo.wwrite('.hgsubstate',
73 74 ''.join(['%s %s\n' % (state[s][1], s)
74 75 for s in sorted(state)]), '')
75 76
76 77 def submerge(repo, wctx, mctx, actx):
77 78 """delegated from merge.applyupdates: merging of .hgsubstate file
78 79 in working context, merging context and ancestor context"""
79 80 if mctx == actx: # backwards?
80 81 actx = wctx.p1()
81 82 s1 = wctx.substate
82 83 s2 = mctx.substate
83 84 sa = actx.substate
84 85 sm = {}
85 86
86 87 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
87 88
88 89 def debug(s, msg, r=""):
89 90 if r:
90 91 r = "%s:%s:%s" % r
91 92 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
92 93
93 94 for s, l in s1.items():
94 95 a = sa.get(s, nullstate)
95 96 ld = l # local state with possible dirty flag for compares
96 97 if wctx.sub(s).dirty():
97 98 ld = (l[0], l[1] + "+")
98 99 if wctx == actx: # overwrite
99 100 a = ld
100 101
101 102 if s in s2:
102 103 r = s2[s]
103 104 if ld == r or r == a: # no change or local is newer
104 105 sm[s] = l
105 106 continue
106 107 elif ld == a: # other side changed
107 108 debug(s, "other changed, get", r)
108 109 wctx.sub(s).get(r)
109 110 sm[s] = r
110 111 elif ld[0] != r[0]: # sources differ
111 112 if repo.ui.promptchoice(
112 113 _(' subrepository sources for %s differ\n'
113 114 'use (l)ocal source (%s) or (r)emote source (%s)?')
114 115 % (s, l[0], r[0]),
115 116 (_('&Local'), _('&Remote')), 0):
116 117 debug(s, "prompt changed, get", r)
117 118 wctx.sub(s).get(r)
118 119 sm[s] = r
119 120 elif ld[1] == a[1]: # local side is unchanged
120 121 debug(s, "other side changed, get", r)
121 122 wctx.sub(s).get(r)
122 123 sm[s] = r
123 124 else:
124 125 debug(s, "both sides changed, merge with", r)
125 126 wctx.sub(s).merge(r)
126 127 sm[s] = l
127 128 elif ld == a: # remote removed, local unchanged
128 129 debug(s, "remote removed, remove")
129 130 wctx.sub(s).remove()
130 131 else:
131 132 if repo.ui.promptchoice(
132 133 _(' local changed subrepository %s which remote removed\n'
133 134 'use (c)hanged version or (d)elete?') % s,
134 135 (_('&Changed'), _('&Delete')), 0):
135 136 debug(s, "prompt remove")
136 137 wctx.sub(s).remove()
137 138
138 139 for s, r in s2.items():
139 140 if s in s1:
140 141 continue
141 142 elif s not in sa:
142 143 debug(s, "remote added, get", r)
143 144 mctx.sub(s).get(r)
144 145 sm[s] = r
145 146 elif r != sa[s]:
146 147 if repo.ui.promptchoice(
147 148 _(' remote changed subrepository %s which local removed\n'
148 149 'use (c)hanged version or (d)elete?') % s,
149 150 (_('&Changed'), _('&Delete')), 0) == 0:
150 151 debug(s, "prompt recreate", r)
151 152 wctx.sub(s).get(r)
152 153 sm[s] = r
153 154
154 155 # record merged .hgsubstate
155 156 writestate(repo, sm)
156 157
157 158 def reporelpath(repo):
158 159 """return path to this (sub)repo as seen from outermost repo"""
159 160 parent = repo
160 161 while hasattr(parent, '_subparent'):
161 162 parent = parent._subparent
162 163 return repo.root[len(parent.root)+1:]
163 164
164 165 def subrelpath(sub):
165 166 """return path to this subrepo as seen from outermost repo"""
166 167 if not hasattr(sub, '_repo'):
167 168 return sub._path
168 169 return reporelpath(sub._repo)
169 170
170 171 def _abssource(repo, push=False, abort=True):
171 172 """return pull/push path of repo - either based on parent repo .hgsub info
172 173 or on the top repo config. Abort or return None if no source found."""
173 174 if hasattr(repo, '_subparent'):
174 175 source = repo._subsource
175 176 if source.startswith('/') or '://' in source:
176 177 return source
177 178 parent = _abssource(repo._subparent, push, abort=False)
178 179 if parent:
179 180 if '://' in parent:
180 181 if parent[-1] == '/':
181 182 parent = parent[:-1]
182 183 r = urlparse.urlparse(parent + '/' + source)
183 184 r = urlparse.urlunparse((r[0], r[1],
184 185 posixpath.normpath(r[2]),
185 186 r[3], r[4], r[5]))
186 187 return r
187 188 else: # plain file system path
188 189 return posixpath.normpath(os.path.join(parent, repo._subsource))
189 190 else: # recursion reached top repo
190 191 if hasattr(repo, '_subtoppath'):
191 192 return repo._subtoppath
192 193 if push and repo.ui.config('paths', 'default-push'):
193 194 return repo.ui.config('paths', 'default-push')
194 195 if repo.ui.config('paths', 'default'):
195 196 return repo.ui.config('paths', 'default')
196 197 if abort:
197 198 raise util.Abort(_("default path for subrepository %s not found") %
198 199 reporelpath(repo))
199 200
200 201 def itersubrepos(ctx1, ctx2):
201 202 """find subrepos in ctx1 or ctx2"""
202 203 # Create a (subpath, ctx) mapping where we prefer subpaths from
203 204 # ctx1. The subpaths from ctx2 are important when the .hgsub file
204 205 # has been modified (in ctx2) but not yet committed (in ctx1).
205 206 subpaths = dict.fromkeys(ctx2.substate, ctx2)
206 207 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
207 208 for subpath, ctx in sorted(subpaths.iteritems()):
208 209 yield subpath, ctx.sub(subpath)
209 210
210 211 def subrepo(ctx, path):
211 212 """return instance of the right subrepo class for subrepo in path"""
212 213 # subrepo inherently violates our import layering rules
213 214 # because it wants to make repo objects from deep inside the stack
214 215 # so we manually delay the circular imports to not break
215 216 # scripts that don't use our demand-loading
216 217 global hg
217 218 import hg as h
218 219 hg = h
219 220
220 221 util.path_auditor(ctx._repo.root)(path)
221 222 state = ctx.substate.get(path, nullstate)
222 223 if state[2] not in types:
223 224 raise util.Abort(_('unknown subrepo type %s') % state[2])
224 225 return types[state[2]](ctx, path, state[:2])
225 226
226 227 # subrepo classes need to implement the following abstract class:
227 228
228 229 class abstractsubrepo(object):
229 230
230 231 def dirty(self):
231 232 """returns true if the dirstate of the subrepo does not match
232 233 current stored state
233 234 """
234 235 raise NotImplementedError
235 236
236 237 def checknested(self, path):
237 238 """check if path is a subrepository within this repository"""
238 239 return False
239 240
240 241 def commit(self, text, user, date):
241 242 """commit the current changes to the subrepo with the given
242 243 log message. Use given user and date if possible. Return the
243 244 new state of the subrepo.
244 245 """
245 246 raise NotImplementedError
246 247
247 248 def remove(self):
248 249 """remove the subrepo
249 250
250 251 (should verify the dirstate is not dirty first)
251 252 """
252 253 raise NotImplementedError
253 254
254 255 def get(self, state):
255 256 """run whatever commands are needed to put the subrepo into
256 257 this state
257 258 """
258 259 raise NotImplementedError
259 260
260 261 def merge(self, state):
261 262 """merge currently-saved state with the new state."""
262 263 raise NotImplementedError
263 264
264 265 def push(self, force):
265 266 """perform whatever action is analogous to 'hg push'
266 267
267 268 This may be a no-op on some systems.
268 269 """
269 270 raise NotImplementedError
270 271
271 272 def add(self, ui, match, dryrun, prefix):
272 273 return []
273 274
274 275 def status(self, rev2, **opts):
275 276 return [], [], [], [], [], [], []
276 277
277 278 def diff(self, diffopts, node2, match, prefix, **opts):
278 279 pass
279 280
280 281 def outgoing(self, ui, dest, opts):
281 282 return 1
282 283
283 284 def incoming(self, ui, source, opts):
284 285 return 1
285 286
286 287 def files(self):
287 288 """return filename iterator"""
288 289 raise NotImplementedError
289 290
290 291 def filedata(self, name):
291 292 """return file data"""
292 293 raise NotImplementedError
293 294
294 295 def fileflags(self, name):
295 296 """return file flags"""
296 297 return ''
297 298
298 299 def archive(self, archiver, prefix):
299 300 for name in self.files():
300 301 flags = self.fileflags(name)
301 302 mode = 'x' in flags and 0755 or 0644
302 303 symlink = 'l' in flags
303 304 archiver.addfile(os.path.join(prefix, self._path, name),
304 305 mode, symlink, self.filedata(name))
305 306
306 307
307 308 class hgsubrepo(abstractsubrepo):
308 309 def __init__(self, ctx, path, state):
309 310 self._path = path
310 311 self._state = state
311 312 r = ctx._repo
312 313 root = r.wjoin(path)
313 314 create = False
314 315 if not os.path.exists(os.path.join(root, '.hg')):
315 316 create = True
316 317 util.makedirs(root)
317 318 self._repo = hg.repository(r.ui, root, create=create)
318 319 self._repo._subparent = r
319 320 self._repo._subsource = state[0]
320 321
321 322 if create:
322 323 fp = self._repo.opener("hgrc", "w", text=True)
323 324 fp.write('[paths]\n')
324 325
325 326 def addpathconfig(key, value):
326 327 if value:
327 328 fp.write('%s = %s\n' % (key, value))
328 329 self._repo.ui.setconfig('paths', key, value)
329 330
330 331 defpath = _abssource(self._repo, abort=False)
331 332 defpushpath = _abssource(self._repo, True, abort=False)
332 333 addpathconfig('default', defpath)
333 334 if defpath != defpushpath:
334 335 addpathconfig('default-push', defpushpath)
335 336 fp.close()
336 337
337 338 def add(self, ui, match, dryrun, prefix):
338 339 return cmdutil.add(ui, self._repo, match, dryrun, True,
339 340 os.path.join(prefix, self._path))
340 341
341 342 def status(self, rev2, **opts):
342 343 try:
343 344 rev1 = self._state[1]
344 345 ctx1 = self._repo[rev1]
345 346 ctx2 = self._repo[rev2]
346 347 return self._repo.status(ctx1, ctx2, **opts)
347 348 except error.RepoLookupError, inst:
348 349 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
349 350 % (inst, subrelpath(self)))
350 351 return [], [], [], [], [], [], []
351 352
352 353 def diff(self, diffopts, node2, match, prefix, **opts):
353 354 try:
354 355 node1 = node.bin(self._state[1])
355 356 # We currently expect node2 to come from substate and be
356 357 # in hex format
357 358 if node2 is not None:
358 359 node2 = node.bin(node2)
359 360 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
360 361 node1, node2, match,
361 362 prefix=os.path.join(prefix, self._path),
362 363 listsubrepos=True, **opts)
363 364 except error.RepoLookupError, inst:
364 365 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
365 366 % (inst, subrelpath(self)))
366 367
367 368 def archive(self, archiver, prefix):
368 369 abstractsubrepo.archive(self, archiver, prefix)
369 370
370 371 rev = self._state[1]
371 372 ctx = self._repo[rev]
372 373 for subpath in ctx.substate:
373 374 s = subrepo(ctx, subpath)
374 375 s.archive(archiver, os.path.join(prefix, self._path))
375 376
376 377 def dirty(self):
377 378 r = self._state[1]
378 379 if r == '':
379 380 return True
380 381 w = self._repo[None]
381 382 if w.p1() != self._repo[r]: # version checked out change
382 383 return True
383 384 return w.dirty() # working directory changed
384 385
385 386 def checknested(self, path):
386 387 return self._repo._checknested(self._repo.wjoin(path))
387 388
388 389 def commit(self, text, user, date):
389 390 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
390 391 n = self._repo.commit(text, user, date)
391 392 if not n:
392 393 return self._repo['.'].hex() # different version checked out
393 394 return node.hex(n)
394 395
395 396 def remove(self):
396 397 # we can't fully delete the repository as it may contain
397 398 # local-only history
398 399 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
399 400 hg.clean(self._repo, node.nullid, False)
400 401
401 402 def _get(self, state):
402 403 source, revision, kind = state
403 404 try:
404 405 self._repo.lookup(revision)
405 406 except error.RepoError:
406 407 self._repo._subsource = source
407 408 srcurl = _abssource(self._repo)
408 409 self._repo.ui.status(_('pulling subrepo %s from %s\n')
409 410 % (subrelpath(self), srcurl))
410 411 other = hg.repository(self._repo.ui, srcurl)
411 412 self._repo.pull(other)
412 413
413 414 def get(self, state):
414 415 self._get(state)
415 416 source, revision, kind = state
416 417 self._repo.ui.debug("getting subrepo %s\n" % self._path)
417 418 hg.clean(self._repo, revision, False)
418 419
419 420 def merge(self, state):
420 421 self._get(state)
421 422 cur = self._repo['.']
422 423 dst = self._repo[state[1]]
423 424 anc = dst.ancestor(cur)
424 425 if anc == cur:
425 426 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
426 427 hg.update(self._repo, state[1])
427 428 elif anc == dst:
428 429 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
429 430 else:
430 431 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
431 432 hg.merge(self._repo, state[1], remind=False)
432 433
433 434 def push(self, force):
434 435 # push subrepos depth-first for coherent ordering
435 436 c = self._repo['']
436 437 subs = c.substate # only repos that are committed
437 438 for s in sorted(subs):
438 439 if not c.sub(s).push(force):
439 440 return False
440 441
441 442 dsturl = _abssource(self._repo, True)
442 443 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
443 444 (subrelpath(self), dsturl))
444 445 other = hg.repository(self._repo.ui, dsturl)
445 446 return self._repo.push(other, force)
446 447
447 448 def outgoing(self, ui, dest, opts):
448 449 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
449 450
450 451 def incoming(self, ui, source, opts):
451 452 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
452 453
453 454 def files(self):
454 455 rev = self._state[1]
455 456 ctx = self._repo[rev]
456 457 return ctx.manifest()
457 458
458 459 def filedata(self, name):
459 460 rev = self._state[1]
460 461 return self._repo[rev][name].data()
461 462
462 463 def fileflags(self, name):
463 464 rev = self._state[1]
464 465 ctx = self._repo[rev]
465 466 return ctx.flags(name)
466 467
467 468
468 469 class svnsubrepo(abstractsubrepo):
469 470 def __init__(self, ctx, path, state):
470 471 self._path = path
471 472 self._state = state
472 473 self._ctx = ctx
473 474 self._ui = ctx._repo.ui
474 475
475 476 def _svncommand(self, commands, filename=''):
476 477 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
477 478 cmd = ['svn'] + commands + [path]
478 479 cmd = [util.shellquote(arg) for arg in cmd]
479 480 cmd = util.quotecommand(' '.join(cmd))
480 481 env = dict(os.environ)
481 482 # Avoid localized output, preserve current locale for everything else.
482 483 env['LC_MESSAGES'] = 'C'
483 484 write, read, err = util.popen3(cmd, env=env, newlines=True)
484 485 retdata = read.read()
485 486 err = err.read().strip()
486 487 if err:
487 488 raise util.Abort(err)
488 489 return retdata
489 490
490 491 def _wcrev(self):
491 492 output = self._svncommand(['info', '--xml'])
492 493 doc = xml.dom.minidom.parseString(output)
493 494 entries = doc.getElementsByTagName('entry')
494 495 if not entries:
495 496 return '0'
496 497 return str(entries[0].getAttribute('revision')) or '0'
497 498
498 499 def _wcchanged(self):
499 500 """Return (changes, extchanges) where changes is True
500 501 if the working directory was changed, and extchanges is
501 502 True if any of these changes concern an external entry.
502 503 """
503 504 output = self._svncommand(['status', '--xml'])
504 505 externals, changes = [], []
505 506 doc = xml.dom.minidom.parseString(output)
506 507 for e in doc.getElementsByTagName('entry'):
507 508 s = e.getElementsByTagName('wc-status')
508 509 if not s:
509 510 continue
510 511 item = s[0].getAttribute('item')
511 512 props = s[0].getAttribute('props')
512 513 path = e.getAttribute('path')
513 514 if item == 'external':
514 515 externals.append(path)
515 516 if (item not in ('', 'normal', 'unversioned', 'external')
516 517 or props not in ('', 'none')):
517 518 changes.append(path)
518 519 for path in changes:
519 520 for ext in externals:
520 521 if path == ext or path.startswith(ext + os.sep):
521 522 return True, True
522 523 return bool(changes), False
523 524
524 525 def dirty(self):
525 526 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
526 527 return False
527 528 return True
528 529
529 530 def commit(self, text, user, date):
530 531 # user and date are out of our hands since svn is centralized
531 532 changed, extchanged = self._wcchanged()
532 533 if not changed:
533 534 return self._wcrev()
534 535 if extchanged:
535 536 # Do not try to commit externals
536 537 raise util.Abort(_('cannot commit svn externals'))
537 538 commitinfo = self._svncommand(['commit', '-m', text])
538 539 self._ui.status(commitinfo)
539 540 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
540 541 if not newrev:
541 542 raise util.Abort(commitinfo.splitlines()[-1])
542 543 newrev = newrev.groups()[0]
543 544 self._ui.status(self._svncommand(['update', '-r', newrev]))
544 545 return newrev
545 546
546 547 def remove(self):
547 548 if self.dirty():
548 549 self._ui.warn(_('not removing repo %s because '
549 550 'it has changes.\n' % self._path))
550 551 return
551 552 self._ui.note(_('removing subrepo %s\n') % self._path)
552 shutil.rmtree(self._ctx._repo.wjoin(self._path))
553
554 def onerror(function, path, excinfo):
555 if function is not os.remove:
556 raise
557 # read-only files cannot be unlinked under Windows
558 s = os.stat(path)
559 if (s.st_mode & stat.S_IWRITE) != 0:
560 raise
561 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
562 os.remove(path)
563
564 shutil.rmtree(self._ctx._repo.wjoin(self._path), onerror=onerror)
553 565
554 566 def get(self, state):
555 567 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
556 568 if not re.search('Checked out revision [0-9]+.', status):
557 569 raise util.Abort(status.splitlines()[-1])
558 570 self._ui.status(status)
559 571
560 572 def merge(self, state):
561 573 old = int(self._state[1])
562 574 new = int(state[1])
563 575 if new > old:
564 576 self.get(state)
565 577
566 578 def push(self, force):
567 579 # push is a no-op for SVN
568 580 return True
569 581
570 582 def files(self):
571 583 output = self._svncommand(['list'])
572 584 # This works because svn forbids \n in filenames.
573 585 return output.splitlines()
574 586
575 587 def filedata(self, name):
576 588 return self._svncommand(['cat'], name)
577 589
578 590
579 591 types = {
580 592 'hg': hgsubrepo,
581 593 'svn': svnsubrepo,
582 594 }
General Comments 0
You need to be logged in to leave comments. Login now