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