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