blob: 39550335872eb3a28ae1fa00d362ad67e11beef4 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001# Copyright (C) 2008 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -080015import errno
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070016import filecmp
17import os
18import re
19import shutil
20import stat
21import sys
22import urllib2
23
24from color import Coloring
25from git_command import GitCommand
26from git_config import GitConfig, IsId
27from gerrit_upload import UploadBundle
28from error import GitError, ImportError, UploadError
29from remote import Remote
30from codereview import proto_client
31
32HEAD = 'HEAD'
33R_HEADS = 'refs/heads/'
34R_TAGS = 'refs/tags/'
35R_PUB = 'refs/published/'
36R_M = 'refs/remotes/m/'
37
38def _warn(fmt, *args):
39 msg = fmt % args
40 print >>sys.stderr, 'warn: %s' % msg
41
42def _info(fmt, *args):
43 msg = fmt % args
44 print >>sys.stderr, 'info: %s' % msg
45
46def not_rev(r):
47 return '^' + r
48
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -080049
50hook_list = None
51def repo_hooks():
52 global hook_list
53 if hook_list is None:
54 d = os.path.abspath(os.path.dirname(__file__))
55 d = os.path.join(d , 'hooks')
56 hook_list = map(lambda x: os.path.join(d, x), os.listdir(d))
57 return hook_list
58
59def relpath(dst, src):
60 src = os.path.dirname(src)
61 top = os.path.commonprefix([dst, src])
62 if top.endswith('/'):
63 top = top[:-1]
64 else:
65 top = os.path.dirname(top)
66
67 tmp = src
68 rel = ''
69 while top != tmp:
70 rel += '../'
71 tmp = os.path.dirname(tmp)
72 return rel + dst[len(top) + 1:]
73
74
Shawn O. Pearce632768b2008-10-23 11:58:52 -070075class DownloadedChange(object):
76 _commit_cache = None
77
78 def __init__(self, project, base, change_id, ps_id, commit):
79 self.project = project
80 self.base = base
81 self.change_id = change_id
82 self.ps_id = ps_id
83 self.commit = commit
84
85 @property
86 def commits(self):
87 if self._commit_cache is None:
88 self._commit_cache = self.project.bare_git.rev_list(
89 '--abbrev=8',
90 '--abbrev-commit',
91 '--pretty=oneline',
92 '--reverse',
93 '--date-order',
94 not_rev(self.base),
95 self.commit,
96 '--')
97 return self._commit_cache
98
99
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700100class ReviewableBranch(object):
101 _commit_cache = None
102
103 def __init__(self, project, branch, base):
104 self.project = project
105 self.branch = branch
106 self.base = base
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800107 self.replace_changes = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700108
109 @property
110 def name(self):
111 return self.branch.name
112
113 @property
114 def commits(self):
115 if self._commit_cache is None:
116 self._commit_cache = self.project.bare_git.rev_list(
117 '--abbrev=8',
118 '--abbrev-commit',
119 '--pretty=oneline',
120 '--reverse',
121 '--date-order',
122 not_rev(self.base),
123 R_HEADS + self.name,
124 '--')
125 return self._commit_cache
126
127 @property
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800128 def unabbrev_commits(self):
129 r = dict()
130 for commit in self.project.bare_git.rev_list(
131 not_rev(self.base),
132 R_HEADS + self.name,
133 '--'):
134 r[commit[0:8]] = commit
135 return r
136
137 @property
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700138 def date(self):
139 return self.project.bare_git.log(
140 '--pretty=format:%cd',
141 '-n', '1',
142 R_HEADS + self.name,
143 '--')
144
145 def UploadForReview(self):
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800146 self.project.UploadForReview(self.name,
147 self.replace_changes)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700148
149 @property
150 def tip_url(self):
151 me = self.project.GetBranch(self.name)
152 commit = self.project.bare_git.rev_parse(R_HEADS + self.name)
153 return 'http://%s/r/%s' % (me.remote.review, commit[0:12])
154
Shawn O. Pearce0758d2f2008-10-22 13:13:40 -0700155 @property
156 def owner_email(self):
157 return self.project.UserEmail
158
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700159
160class StatusColoring(Coloring):
161 def __init__(self, config):
162 Coloring.__init__(self, config, 'status')
163 self.project = self.printer('header', attr = 'bold')
164 self.branch = self.printer('header', attr = 'bold')
165 self.nobranch = self.printer('nobranch', fg = 'red')
166
167 self.added = self.printer('added', fg = 'green')
168 self.changed = self.printer('changed', fg = 'red')
169 self.untracked = self.printer('untracked', fg = 'red')
170
171
172class DiffColoring(Coloring):
173 def __init__(self, config):
174 Coloring.__init__(self, config, 'diff')
175 self.project = self.printer('header', attr = 'bold')
176
177
178class _CopyFile:
179 def __init__(self, src, dest):
180 self.src = src
181 self.dest = dest
182
183 def _Copy(self):
184 src = self.src
185 dest = self.dest
186 # copy file if it does not exist or is out of date
187 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
188 try:
189 # remove existing file first, since it might be read-only
190 if os.path.exists(dest):
191 os.remove(dest)
192 shutil.copy(src, dest)
193 # make the file read-only
194 mode = os.stat(dest)[stat.ST_MODE]
195 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
196 os.chmod(dest, mode)
197 except IOError:
198 print >>sys.stderr, \
199 'error: Cannot copy file %s to %s' \
200 % (src, dest)
201
202
203class Project(object):
204 def __init__(self,
205 manifest,
206 name,
207 remote,
208 gitdir,
209 worktree,
210 relpath,
211 revision):
212 self.manifest = manifest
213 self.name = name
214 self.remote = remote
215 self.gitdir = gitdir
216 self.worktree = worktree
217 self.relpath = relpath
218 self.revision = revision
219 self.snapshots = {}
220 self.extraRemotes = {}
221 self.copyfiles = []
222 self.config = GitConfig.ForRepository(
223 gitdir = self.gitdir,
224 defaults = self.manifest.globalConfig)
225
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800226 if self.worktree:
227 self.work_git = self._GitGetByExec(self, bare=False)
228 else:
229 self.work_git = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700230 self.bare_git = self._GitGetByExec(self, bare=True)
231
232 @property
233 def Exists(self):
234 return os.path.isdir(self.gitdir)
235
236 @property
237 def CurrentBranch(self):
238 """Obtain the name of the currently checked out branch.
239 The branch name omits the 'refs/heads/' prefix.
240 None is returned if the project is on a detached HEAD.
241 """
242 try:
243 b = self.work_git.GetHead()
244 except GitError:
245 return None
246 if b.startswith(R_HEADS):
247 return b[len(R_HEADS):]
248 return None
249
250 def IsDirty(self, consider_untracked=True):
251 """Is the working directory modified in some way?
252 """
253 self.work_git.update_index('-q',
254 '--unmerged',
255 '--ignore-missing',
256 '--refresh')
257 if self.work_git.DiffZ('diff-index','-M','--cached',HEAD):
258 return True
259 if self.work_git.DiffZ('diff-files'):
260 return True
261 if consider_untracked and self.work_git.LsOthers():
262 return True
263 return False
264
265 _userident_name = None
266 _userident_email = None
267
268 @property
269 def UserName(self):
270 """Obtain the user's personal name.
271 """
272 if self._userident_name is None:
273 self._LoadUserIdentity()
274 return self._userident_name
275
276 @property
277 def UserEmail(self):
278 """Obtain the user's email address. This is very likely
279 to be their Gerrit login.
280 """
281 if self._userident_email is None:
282 self._LoadUserIdentity()
283 return self._userident_email
284
285 def _LoadUserIdentity(self):
286 u = self.bare_git.var('GIT_COMMITTER_IDENT')
287 m = re.compile("^(.*) <([^>]*)> ").match(u)
288 if m:
289 self._userident_name = m.group(1)
290 self._userident_email = m.group(2)
291 else:
292 self._userident_name = ''
293 self._userident_email = ''
294
295 def GetRemote(self, name):
296 """Get the configuration for a single remote.
297 """
298 return self.config.GetRemote(name)
299
300 def GetBranch(self, name):
301 """Get the configuration for a single branch.
302 """
303 return self.config.GetBranch(name)
304
305
306## Status Display ##
307
308 def PrintWorkTreeStatus(self):
309 """Prints the status of the repository to stdout.
310 """
311 if not os.path.isdir(self.worktree):
312 print ''
313 print 'project %s/' % self.relpath
314 print ' missing (run "repo sync")'
315 return
316
317 self.work_git.update_index('-q',
318 '--unmerged',
319 '--ignore-missing',
320 '--refresh')
321 di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD)
322 df = self.work_git.DiffZ('diff-files')
323 do = self.work_git.LsOthers()
324 if not di and not df and not do:
325 return
326
327 out = StatusColoring(self.config)
328 out.project('project %-40s', self.relpath + '/')
329
330 branch = self.CurrentBranch
331 if branch is None:
332 out.nobranch('(*** NO BRANCH ***)')
333 else:
334 out.branch('branch %s', branch)
335 out.nl()
336
337 paths = list()
338 paths.extend(di.keys())
339 paths.extend(df.keys())
340 paths.extend(do)
341
342 paths = list(set(paths))
343 paths.sort()
344
345 for p in paths:
346 try: i = di[p]
347 except KeyError: i = None
348
349 try: f = df[p]
350 except KeyError: f = None
351
352 if i: i_status = i.status.upper()
353 else: i_status = '-'
354
355 if f: f_status = f.status.lower()
356 else: f_status = '-'
357
358 if i and i.src_path:
359 line = ' %s%s\t%s => (%s%%)' % (i_status, f_status,
360 i.src_path, p, i.level)
361 else:
362 line = ' %s%s\t%s' % (i_status, f_status, p)
363
364 if i and not f:
365 out.added('%s', line)
366 elif (i and f) or (not i and f):
367 out.changed('%s', line)
368 elif not i and not f:
369 out.untracked('%s', line)
370 else:
371 out.write('%s', line)
372 out.nl()
373
374 def PrintWorkTreeDiff(self):
375 """Prints the status of the repository to stdout.
376 """
377 out = DiffColoring(self.config)
378 cmd = ['diff']
379 if out.is_on:
380 cmd.append('--color')
381 cmd.append(HEAD)
382 cmd.append('--')
383 p = GitCommand(self,
384 cmd,
385 capture_stdout = True,
386 capture_stderr = True)
387 has_diff = False
388 for line in p.process.stdout:
389 if not has_diff:
390 out.nl()
391 out.project('project %s/' % self.relpath)
392 out.nl()
393 has_diff = True
394 print line[:-1]
395 p.Wait()
396
397
398## Publish / Upload ##
399
400 def WasPublished(self, branch):
401 """Was the branch published (uploaded) for code review?
402 If so, returns the SHA-1 hash of the last published
403 state for the branch.
404 """
405 try:
406 return self.bare_git.rev_parse(R_PUB + branch)
407 except GitError:
408 return None
409
410 def CleanPublishedCache(self):
411 """Prunes any stale published refs.
412 """
413 heads = set()
414 canrm = {}
415 for name, id in self._allrefs.iteritems():
416 if name.startswith(R_HEADS):
417 heads.add(name)
418 elif name.startswith(R_PUB):
419 canrm[name] = id
420
421 for name, id in canrm.iteritems():
422 n = name[len(R_PUB):]
423 if R_HEADS + n not in heads:
424 self.bare_git.DeleteRef(name, id)
425
426 def GetUploadableBranches(self):
427 """List any branches which can be uploaded for review.
428 """
429 heads = {}
430 pubed = {}
431
432 for name, id in self._allrefs.iteritems():
433 if name.startswith(R_HEADS):
434 heads[name[len(R_HEADS):]] = id
435 elif name.startswith(R_PUB):
436 pubed[name[len(R_PUB):]] = id
437
438 ready = []
439 for branch, id in heads.iteritems():
440 if branch in pubed and pubed[branch] == id:
441 continue
442
Shawn O. Pearce35f25962008-11-11 17:03:13 -0800443 rb = self.GetUploadableBranch(branch)
444 if rb:
445 ready.append(rb)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700446 return ready
447
Shawn O. Pearce35f25962008-11-11 17:03:13 -0800448 def GetUploadableBranch(self, branch_name):
449 """Get a single uploadable branch, or None.
450 """
451 branch = self.GetBranch(branch_name)
452 base = branch.LocalMerge
453 if branch.LocalMerge:
454 rb = ReviewableBranch(self, branch, base)
455 if rb.commits:
456 return rb
457 return None
458
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800459 def UploadForReview(self, branch=None, replace_changes=None):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700460 """Uploads the named branch for code review.
461 """
462 if branch is None:
463 branch = self.CurrentBranch
464 if branch is None:
465 raise GitError('not currently on a branch')
466
467 branch = self.GetBranch(branch)
468 if not branch.LocalMerge:
469 raise GitError('branch %s does not track a remote' % branch.name)
470 if not branch.remote.review:
471 raise GitError('remote %s has no review url' % branch.remote.name)
472
473 dest_branch = branch.merge
474 if not dest_branch.startswith(R_HEADS):
475 dest_branch = R_HEADS + dest_branch
476
477 base_list = []
478 for name, id in self._allrefs.iteritems():
479 if branch.remote.WritesTo(name):
480 base_list.append(not_rev(name))
481 if not base_list:
482 raise GitError('no base refs, cannot upload %s' % branch.name)
483
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800484 if not branch.remote.projectname:
485 branch.remote.projectname = self.name
486 branch.remote.Save()
487
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700488 print >>sys.stderr, ''
489 _info("Uploading %s to %s:", branch.name, self.name)
490 try:
491 UploadBundle(project = self,
492 server = branch.remote.review,
493 email = self.UserEmail,
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800494 dest_project = branch.remote.projectname,
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700495 dest_branch = dest_branch,
496 src_branch = R_HEADS + branch.name,
Shawn O. Pearcec99883f2008-11-11 17:12:43 -0800497 bases = base_list,
498 replace_changes = replace_changes)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700499 except proto_client.ClientLoginError:
500 raise UploadError('Login failure')
501 except urllib2.HTTPError, e:
502 raise UploadError('HTTP error %d' % e.code)
503
504 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
505 self.bare_git.UpdateRef(R_PUB + branch.name,
506 R_HEADS + branch.name,
507 message = msg)
508
509
510## Sync ##
511
512 def Sync_NetworkHalf(self):
513 """Perform only the network IO portion of the sync process.
514 Local working directory/branch state is not affected.
515 """
516 if not self.Exists:
517 print >>sys.stderr
518 print >>sys.stderr, 'Initializing project %s ...' % self.name
519 self._InitGitDir()
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800520
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700521 self._InitRemote()
522 for r in self.extraRemotes.values():
523 if not self._RemoteFetch(r.name):
524 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700525 if not self._RemoteFetch():
526 return False
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800527
528 if self.worktree:
529 self._RepairAndroidImportErrors()
530 self._InitMRef()
531 else:
532 self._InitMirrorHead()
533 try:
534 os.remove(os.path.join(self.gitdir, 'FETCH_HEAD'))
535 except OSError:
536 pass
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700537 return True
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800538
539 def PostRepoUpgrade(self):
540 self._InitHooks()
541
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700542 def _CopyFiles(self):
543 for file in self.copyfiles:
544 file._Copy()
545
Shawn O. Pearce329c31d2008-10-24 09:17:25 -0700546 def _RepairAndroidImportErrors(self):
547 if self.name in ['platform/external/iptables',
548 'platform/external/libpcap',
549 'platform/external/tcpdump',
550 'platform/external/webkit',
551 'platform/system/wlan/ti']:
552 # I hate myself for doing this...
553 #
554 # In the initial Android 1.0 release these projects were
555 # shipped, some users got them, and then the history had
556 # to be rewritten to correct problems with their imports.
557 # The 'android-1.0' tag may still be pointing at the old
558 # history, so we need to drop the tag and fetch it again.
559 #
560 try:
561 remote = self.GetRemote(self.remote.name)
562 relname = remote.ToLocal(R_HEADS + 'release-1.0')
563 tagname = R_TAGS + 'android-1.0'
564 if self._revlist(not_rev(relname), tagname):
565 cmd = ['fetch', remote.name, '+%s:%s' % (tagname, tagname)]
566 GitCommand(self, cmd, bare = True).Wait()
567 except GitError:
568 pass
569
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700570 def Sync_LocalHalf(self):
571 """Perform only the local IO portion of the sync process.
572 Network access is not required.
573
574 Return:
575 True: the sync was successful
576 False: the sync requires user input
577 """
578 self._InitWorkTree()
579 self.CleanPublishedCache()
580
581 rem = self.GetRemote(self.remote.name)
582 rev = rem.ToLocal(self.revision)
583 branch = self.CurrentBranch
584
585 if branch is None:
586 # Currently on a detached HEAD. The user is assumed to
587 # not have any local modifications worth worrying about.
588 #
589 lost = self._revlist(not_rev(rev), HEAD)
590 if lost:
591 _info("[%s] Discarding %d commits", self.name, len(lost))
592 try:
593 self._Checkout(rev, quiet=True)
594 except GitError:
595 return False
596 self._CopyFiles()
597 return True
598
599 branch = self.GetBranch(branch)
600 merge = branch.LocalMerge
601
602 if not merge:
603 # The current branch has no tracking configuration.
604 # Jump off it to a deatched HEAD.
605 #
606 _info("[%s] Leaving %s"
607 " (does not track any upstream)",
608 self.name,
609 branch.name)
610 try:
611 self._Checkout(rev, quiet=True)
612 except GitError:
613 return False
614 self._CopyFiles()
615 return True
616
617 upstream_gain = self._revlist(not_rev(HEAD), rev)
618 pub = self.WasPublished(branch.name)
619 if pub:
620 not_merged = self._revlist(not_rev(rev), pub)
621 if not_merged:
622 if upstream_gain:
623 # The user has published this branch and some of those
624 # commits are not yet merged upstream. We do not want
625 # to rewrite the published commits so we punt.
626 #
627 _info("[%s] Branch %s is published,"
628 " but is now %d commits behind.",
629 self.name, branch.name, len(upstream_gain))
630 _info("[%s] Consider merging or rebasing the"
631 " unpublished commits.", self.name)
632 return True
Shawn O. Pearce23d77812008-10-30 11:06:57 -0700633 elif upstream_gain:
Shawn O. Pearcea54c5272008-10-30 11:03:00 -0700634 # We can fast-forward safely.
635 #
636 try:
637 self._FastForward(rev)
638 except GitError:
639 return False
640 self._CopyFiles()
641 return True
Shawn O. Pearce23d77812008-10-30 11:06:57 -0700642 else:
643 # Trivially no changes in the upstream.
644 #
645 return True
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700646
647 if merge == rev:
648 try:
649 old_merge = self.bare_git.rev_parse('%s@{1}' % merge)
650 except GitError:
651 old_merge = merge
Shawn O. Pearce07346002008-10-21 07:09:27 -0700652 if old_merge == '0000000000000000000000000000000000000000' \
653 or old_merge == '':
654 old_merge = merge
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700655 else:
656 # The upstream switched on us. Time to cross our fingers
657 # and pray that the old upstream also wasn't in the habit
658 # of rebasing itself.
659 #
660 _info("[%s] Manifest switched from %s to %s",
661 self.name, merge, rev)
662 old_merge = merge
663
664 if rev == old_merge:
665 upstream_lost = []
666 else:
667 upstream_lost = self._revlist(not_rev(rev), old_merge)
668
669 if not upstream_lost and not upstream_gain:
670 # Trivially no changes caused by the upstream.
671 #
672 return True
673
674 if self.IsDirty(consider_untracked=False):
675 _warn('[%s] commit (or discard) uncommitted changes'
676 ' before sync', self.name)
677 return False
678
679 if upstream_lost:
680 # Upstream rebased. Not everything in HEAD
681 # may have been caused by the user.
682 #
683 _info("[%s] Discarding %d commits removed from upstream",
684 self.name, len(upstream_lost))
685
686 branch.remote = rem
687 branch.merge = self.revision
688 branch.Save()
689
690 my_changes = self._revlist(not_rev(old_merge), HEAD)
691 if my_changes:
692 try:
693 self._Rebase(upstream = old_merge, onto = rev)
694 except GitError:
695 return False
696 elif upstream_lost:
697 try:
698 self._ResetHard(rev)
699 except GitError:
700 return False
701 else:
702 try:
703 self._FastForward(rev)
704 except GitError:
705 return False
706
707 self._CopyFiles()
708 return True
709
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700710 def AddCopyFile(self, src, dest):
711 # dest should already be an absolute path, but src is project relative
712 # make src an absolute path
713 src = os.path.join(self.worktree, src)
714 self.copyfiles.append(_CopyFile(src, dest))
715
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700716 def DownloadPatchSet(self, change_id, patch_id):
717 """Download a single patch set of a single change to FETCH_HEAD.
718 """
719 remote = self.GetRemote(self.remote.name)
720
721 cmd = ['fetch', remote.name]
722 cmd.append('refs/changes/%2.2d/%d/%d' \
723 % (change_id % 100, change_id, patch_id))
724 cmd.extend(map(lambda x: str(x), remote.fetch))
725 if GitCommand(self, cmd, bare=True).Wait() != 0:
726 return None
727 return DownloadedChange(self,
728 remote.ToLocal(self.revision),
729 change_id,
730 patch_id,
731 self.bare_git.rev_parse('FETCH_HEAD'))
732
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700733
734## Branch Management ##
735
736 def StartBranch(self, name):
737 """Create a new branch off the manifest's revision.
738 """
739 branch = self.GetBranch(name)
740 branch.remote = self.GetRemote(self.remote.name)
741 branch.merge = self.revision
742
743 rev = branch.LocalMerge
744 cmd = ['checkout', '-b', branch.name, rev]
745 if GitCommand(self, cmd).Wait() == 0:
746 branch.Save()
747 else:
748 raise GitError('%s checkout %s ' % (self.name, rev))
749
Shawn O. Pearce9fa44db2008-11-03 11:24:59 -0800750 def AbandonBranch(self, name):
751 """Destroy a local topic branch.
752 """
753 try:
754 tip_rev = self.bare_git.rev_parse(R_HEADS + name)
755 except GitError:
756 return
757
758 if self.CurrentBranch == name:
759 self._Checkout(
760 self.GetRemote(self.remote.name).ToLocal(self.revision),
761 quiet=True)
762
763 cmd = ['branch', '-D', name]
764 GitCommand(self, cmd, capture_stdout=True).Wait()
765
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700766 def PruneHeads(self):
767 """Prune any topic branches already merged into upstream.
768 """
769 cb = self.CurrentBranch
770 kill = []
771 for name in self._allrefs.keys():
772 if name.startswith(R_HEADS):
773 name = name[len(R_HEADS):]
774 if cb is None or name != cb:
775 kill.append(name)
776
777 rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
778 if cb is not None \
779 and not self._revlist(HEAD + '...' + rev) \
780 and not self.IsDirty(consider_untracked = False):
781 self.work_git.DetachHead(HEAD)
782 kill.append(cb)
783
784 deleted = set()
785 if kill:
786 try:
787 old = self.bare_git.GetHead()
788 except GitError:
789 old = 'refs/heads/please_never_use_this_as_a_branch_name'
790
791 rm_re = re.compile(r"^Deleted branch (.*)\.$")
792 try:
793 self.bare_git.DetachHead(rev)
794
795 b = ['branch', '-d']
796 b.extend(kill)
797 b = GitCommand(self, b, bare=True,
798 capture_stdout=True,
799 capture_stderr=True)
800 b.Wait()
801 finally:
802 self.bare_git.SetHead(old)
803
804 for line in b.stdout.split("\n"):
805 m = rm_re.match(line)
806 if m:
807 deleted.add(m.group(1))
808
809 if deleted:
810 self.CleanPublishedCache()
811
812 if cb and cb not in kill:
813 kill.append(cb)
814 kill.sort()
815
816 kept = []
817 for branch in kill:
818 if branch not in deleted:
819 branch = self.GetBranch(branch)
820 base = branch.LocalMerge
821 if not base:
822 base = rev
823 kept.append(ReviewableBranch(self, branch, base))
824 return kept
825
826
827## Direct Git Commands ##
828
829 def _RemoteFetch(self, name=None):
830 if not name:
831 name = self.remote.name
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800832 cmd = ['fetch']
833 if not self.worktree:
834 cmd.append('--update-head-ok')
835 cmd.append(name)
836 return GitCommand(self, cmd, bare = True).Wait() == 0
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700837
838 def _Checkout(self, rev, quiet=False):
839 cmd = ['checkout']
840 if quiet:
841 cmd.append('-q')
842 cmd.append(rev)
843 cmd.append('--')
844 if GitCommand(self, cmd).Wait() != 0:
845 if self._allrefs:
846 raise GitError('%s checkout %s ' % (self.name, rev))
847
848 def _ResetHard(self, rev, quiet=True):
849 cmd = ['reset', '--hard']
850 if quiet:
851 cmd.append('-q')
852 cmd.append(rev)
853 if GitCommand(self, cmd).Wait() != 0:
854 raise GitError('%s reset --hard %s ' % (self.name, rev))
855
856 def _Rebase(self, upstream, onto = None):
857 cmd = ['rebase', '-i']
858 if onto is not None:
859 cmd.extend(['--onto', onto])
860 cmd.append(upstream)
861 if GitCommand(self, cmd, disable_editor=True).Wait() != 0:
862 raise GitError('%s rebase %s ' % (self.name, upstream))
863
864 def _FastForward(self, head):
865 cmd = ['merge', head]
866 if GitCommand(self, cmd).Wait() != 0:
867 raise GitError('%s merge %s ' % (self.name, head))
868
869 def _InitGitDir(self):
870 if not os.path.exists(self.gitdir):
871 os.makedirs(self.gitdir)
872 self.bare_git.init()
873 self.config.SetString('core.bare', None)
874
875 hooks = self._gitdir_path('hooks')
Shawn O. Pearcede646812008-10-29 14:38:12 -0700876 try:
877 to_rm = os.listdir(hooks)
878 except OSError:
879 to_rm = []
880 for old_hook in to_rm:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700881 os.remove(os.path.join(hooks, old_hook))
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800882 self._InitHooks()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700883
884 m = self.manifest.manifestProject.config
885 for key in ['user.name', 'user.email']:
886 if m.Has(key, include_defaults = False):
887 self.config.SetString(key, m.GetString(key))
888
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800889 def _InitHooks(self):
890 hooks = self._gitdir_path('hooks')
891 if not os.path.exists(hooks):
892 os.makedirs(hooks)
893 for stock_hook in repo_hooks():
894 dst = os.path.join(hooks, os.path.basename(stock_hook))
895 try:
896 os.symlink(relpath(stock_hook, dst), dst)
897 except OSError, e:
898 if e.errno == errno.EEXIST:
899 pass
900 elif e.errno == errno.EPERM:
901 raise GitError('filesystem must support symlinks')
902 else:
903 raise
904
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700905 def _InitRemote(self):
906 if self.remote.fetchUrl:
907 remote = self.GetRemote(self.remote.name)
908
909 url = self.remote.fetchUrl
910 while url.endswith('/'):
911 url = url[:-1]
912 url += '/%s.git' % self.name
913 remote.url = url
914 remote.review = self.remote.reviewUrl
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800915 if remote.projectname is None:
916 remote.projectname = self.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700917
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800918 if self.worktree:
919 remote.ResetFetch(mirror=False)
920 else:
921 remote.ResetFetch(mirror=True)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700922 remote.Save()
923
924 for r in self.extraRemotes.values():
925 remote = self.GetRemote(r.name)
926 remote.url = r.fetchUrl
927 remote.review = r.reviewUrl
Shawn O. Pearceae6e0942008-11-06 10:25:35 -0800928 if r.projectName:
929 remote.projectname = r.projectName
930 elif remote.projectname is None:
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800931 remote.projectname = self.name
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700932 remote.ResetFetch()
933 remote.Save()
934
935 def _InitMRef(self):
936 if self.manifest.branch:
937 msg = 'manifest set to %s' % self.revision
938 ref = R_M + self.manifest.branch
939
940 if IsId(self.revision):
941 dst = self.revision + '^0',
942 self.bare_git.UpdateRef(ref, dst, message = msg, detach = True)
943 else:
944 remote = self.GetRemote(self.remote.name)
945 dst = remote.ToLocal(self.revision)
946 self.bare_git.symbolic_ref('-m', msg, ref, dst)
947
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800948 def _InitMirrorHead(self):
949 dst = self.GetRemote(self.remote.name).ToLocal(self.revision)
950 msg = 'manifest set to %s' % self.revision
951 self.bare_git.SetHead(dst, message=msg)
952
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700953 def _InitWorkTree(self):
954 dotgit = os.path.join(self.worktree, '.git')
955 if not os.path.exists(dotgit):
956 os.makedirs(dotgit)
957
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700958 for name in ['config',
959 'description',
960 'hooks',
961 'info',
962 'logs',
963 'objects',
964 'packed-refs',
965 'refs',
966 'rr-cache',
967 'svn']:
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -0800968 try:
Shawn O. Pearcec9ef7442008-11-03 10:32:09 -0800969 src = os.path.join(self.gitdir, name)
970 dst = os.path.join(dotgit, name)
971 os.symlink(relpath(src, dst), dst)
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -0800972 except OSError, e:
973 if e.errno == errno.EPERM:
974 raise GitError('filesystem must support symlinks')
975 else:
976 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700977
978 rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
979 rev = self.bare_git.rev_parse('%s^0' % rev)
980
981 f = open(os.path.join(dotgit, HEAD), 'wb')
982 f.write("%s\n" % rev)
983 f.close()
984
985 cmd = ['read-tree', '--reset', '-u']
986 cmd.append('-v')
987 cmd.append('HEAD')
988 if GitCommand(self, cmd).Wait() != 0:
989 raise GitError("cannot initialize work tree")
990
991 def _gitdir_path(self, path):
992 return os.path.join(self.gitdir, path)
993
994 def _revlist(self, *args):
995 cmd = []
996 cmd.extend(args)
997 cmd.append('--')
998 return self.work_git.rev_list(*args)
999
1000 @property
1001 def _allrefs(self):
1002 return self.bare_git.ListRefs()
1003
1004 class _GitGetByExec(object):
1005 def __init__(self, project, bare):
1006 self._project = project
1007 self._bare = bare
1008
1009 def ListRefs(self, *args):
1010 cmdv = ['for-each-ref', '--format=%(objectname) %(refname)']
1011 cmdv.extend(args)
1012 p = GitCommand(self._project,
1013 cmdv,
1014 bare = self._bare,
1015 capture_stdout = True,
1016 capture_stderr = True)
1017 r = {}
1018 for line in p.process.stdout:
1019 id, name = line[:-1].split(' ', 2)
1020 r[name] = id
1021 if p.Wait() != 0:
1022 raise GitError('%s for-each-ref %s: %s' % (
1023 self._project.name,
1024 str(args),
1025 p.stderr))
1026 return r
1027
1028 def LsOthers(self):
1029 p = GitCommand(self._project,
1030 ['ls-files',
1031 '-z',
1032 '--others',
1033 '--exclude-standard'],
1034 bare = False,
1035 capture_stdout = True,
1036 capture_stderr = True)
1037 if p.Wait() == 0:
1038 out = p.stdout
1039 if out:
1040 return out[:-1].split("\0")
1041 return []
1042
1043 def DiffZ(self, name, *args):
1044 cmd = [name]
1045 cmd.append('-z')
1046 cmd.extend(args)
1047 p = GitCommand(self._project,
1048 cmd,
1049 bare = False,
1050 capture_stdout = True,
1051 capture_stderr = True)
1052 try:
1053 out = p.process.stdout.read()
1054 r = {}
1055 if out:
1056 out = iter(out[:-1].split('\0'))
1057 while out:
Shawn O. Pearce02dbb6d2008-10-21 13:59:08 -07001058 try:
1059 info = out.next()
1060 path = out.next()
1061 except StopIteration:
1062 break
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001063
1064 class _Info(object):
1065 def __init__(self, path, omode, nmode, oid, nid, state):
1066 self.path = path
1067 self.src_path = None
1068 self.old_mode = omode
1069 self.new_mode = nmode
1070 self.old_id = oid
1071 self.new_id = nid
1072
1073 if len(state) == 1:
1074 self.status = state
1075 self.level = None
1076 else:
1077 self.status = state[:1]
1078 self.level = state[1:]
1079 while self.level.startswith('0'):
1080 self.level = self.level[1:]
1081
1082 info = info[1:].split(' ')
1083 info =_Info(path, *info)
1084 if info.status in ('R', 'C'):
1085 info.src_path = info.path
1086 info.path = out.next()
1087 r[info.path] = info
1088 return r
1089 finally:
1090 p.Wait()
1091
1092 def GetHead(self):
1093 return self.symbolic_ref(HEAD)
1094
1095 def SetHead(self, ref, message=None):
1096 cmdv = []
1097 if message is not None:
1098 cmdv.extend(['-m', message])
1099 cmdv.append(HEAD)
1100 cmdv.append(ref)
1101 self.symbolic_ref(*cmdv)
1102
1103 def DetachHead(self, new, message=None):
1104 cmdv = ['--no-deref']
1105 if message is not None:
1106 cmdv.extend(['-m', message])
1107 cmdv.append(HEAD)
1108 cmdv.append(new)
1109 self.update_ref(*cmdv)
1110
1111 def UpdateRef(self, name, new, old=None,
1112 message=None,
1113 detach=False):
1114 cmdv = []
1115 if message is not None:
1116 cmdv.extend(['-m', message])
1117 if detach:
1118 cmdv.append('--no-deref')
1119 cmdv.append(name)
1120 cmdv.append(new)
1121 if old is not None:
1122 cmdv.append(old)
1123 self.update_ref(*cmdv)
1124
1125 def DeleteRef(self, name, old=None):
1126 if not old:
1127 old = self.rev_parse(name)
1128 self.update_ref('-d', name, old)
1129
1130 def rev_list(self, *args):
1131 cmdv = ['rev-list']
1132 cmdv.extend(args)
1133 p = GitCommand(self._project,
1134 cmdv,
1135 bare = self._bare,
1136 capture_stdout = True,
1137 capture_stderr = True)
1138 r = []
1139 for line in p.process.stdout:
1140 r.append(line[:-1])
1141 if p.Wait() != 0:
1142 raise GitError('%s rev-list %s: %s' % (
1143 self._project.name,
1144 str(args),
1145 p.stderr))
1146 return r
1147
1148 def __getattr__(self, name):
1149 name = name.replace('_', '-')
1150 def runner(*args):
1151 cmdv = [name]
1152 cmdv.extend(args)
1153 p = GitCommand(self._project,
1154 cmdv,
1155 bare = self._bare,
1156 capture_stdout = True,
1157 capture_stderr = True)
1158 if p.Wait() != 0:
1159 raise GitError('%s %s: %s' % (
1160 self._project.name,
1161 name,
1162 p.stderr))
1163 r = p.stdout
1164 if r.endswith('\n') and r.index('\n') == len(r) - 1:
1165 return r[:-1]
1166 return r
1167 return runner
1168
1169
1170class MetaProject(Project):
1171 """A special project housed under .repo.
1172 """
1173 def __init__(self, manifest, name, gitdir, worktree):
1174 repodir = manifest.repodir
1175 Project.__init__(self,
1176 manifest = manifest,
1177 name = name,
1178 gitdir = gitdir,
1179 worktree = worktree,
1180 remote = Remote('origin'),
1181 relpath = '.repo/%s' % name,
1182 revision = 'refs/heads/master')
1183
1184 def PreSync(self):
1185 if self.Exists:
1186 cb = self.CurrentBranch
1187 if cb:
1188 base = self.GetBranch(cb).merge
1189 if base:
1190 self.revision = base
1191
1192 @property
1193 def HasChanges(self):
1194 """Has the remote received new commits not yet checked out?
1195 """
1196 rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
1197 if self._revlist(not_rev(HEAD), rev):
1198 return True
1199 return False