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