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