blob: 874a40bdddd6fe27c01568f630721ffd1f2a196f [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. Pearce632768b2008-10-23 11:58:52 -070049class DownloadedChange(object):
50 _commit_cache = None
51
52 def __init__(self, project, base, change_id, ps_id, commit):
53 self.project = project
54 self.base = base
55 self.change_id = change_id
56 self.ps_id = ps_id
57 self.commit = commit
58
59 @property
60 def commits(self):
61 if self._commit_cache is None:
62 self._commit_cache = self.project.bare_git.rev_list(
63 '--abbrev=8',
64 '--abbrev-commit',
65 '--pretty=oneline',
66 '--reverse',
67 '--date-order',
68 not_rev(self.base),
69 self.commit,
70 '--')
71 return self._commit_cache
72
73
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070074class ReviewableBranch(object):
75 _commit_cache = None
76
77 def __init__(self, project, branch, base):
78 self.project = project
79 self.branch = branch
80 self.base = base
81
82 @property
83 def name(self):
84 return self.branch.name
85
86 @property
87 def commits(self):
88 if self._commit_cache is None:
89 self._commit_cache = self.project.bare_git.rev_list(
90 '--abbrev=8',
91 '--abbrev-commit',
92 '--pretty=oneline',
93 '--reverse',
94 '--date-order',
95 not_rev(self.base),
96 R_HEADS + self.name,
97 '--')
98 return self._commit_cache
99
100 @property
101 def date(self):
102 return self.project.bare_git.log(
103 '--pretty=format:%cd',
104 '-n', '1',
105 R_HEADS + self.name,
106 '--')
107
108 def UploadForReview(self):
109 self.project.UploadForReview(self.name)
110
111 @property
112 def tip_url(self):
113 me = self.project.GetBranch(self.name)
114 commit = self.project.bare_git.rev_parse(R_HEADS + self.name)
115 return 'http://%s/r/%s' % (me.remote.review, commit[0:12])
116
Shawn O. Pearce0758d2f2008-10-22 13:13:40 -0700117 @property
118 def owner_email(self):
119 return self.project.UserEmail
120
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700121
122class StatusColoring(Coloring):
123 def __init__(self, config):
124 Coloring.__init__(self, config, 'status')
125 self.project = self.printer('header', attr = 'bold')
126 self.branch = self.printer('header', attr = 'bold')
127 self.nobranch = self.printer('nobranch', fg = 'red')
128
129 self.added = self.printer('added', fg = 'green')
130 self.changed = self.printer('changed', fg = 'red')
131 self.untracked = self.printer('untracked', fg = 'red')
132
133
134class DiffColoring(Coloring):
135 def __init__(self, config):
136 Coloring.__init__(self, config, 'diff')
137 self.project = self.printer('header', attr = 'bold')
138
139
140class _CopyFile:
141 def __init__(self, src, dest):
142 self.src = src
143 self.dest = dest
144
145 def _Copy(self):
146 src = self.src
147 dest = self.dest
148 # copy file if it does not exist or is out of date
149 if not os.path.exists(dest) or not filecmp.cmp(src, dest):
150 try:
151 # remove existing file first, since it might be read-only
152 if os.path.exists(dest):
153 os.remove(dest)
154 shutil.copy(src, dest)
155 # make the file read-only
156 mode = os.stat(dest)[stat.ST_MODE]
157 mode = mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH)
158 os.chmod(dest, mode)
159 except IOError:
160 print >>sys.stderr, \
161 'error: Cannot copy file %s to %s' \
162 % (src, dest)
163
164
165class Project(object):
166 def __init__(self,
167 manifest,
168 name,
169 remote,
170 gitdir,
171 worktree,
172 relpath,
173 revision):
174 self.manifest = manifest
175 self.name = name
176 self.remote = remote
177 self.gitdir = gitdir
178 self.worktree = worktree
179 self.relpath = relpath
180 self.revision = revision
181 self.snapshots = {}
182 self.extraRemotes = {}
183 self.copyfiles = []
184 self.config = GitConfig.ForRepository(
185 gitdir = self.gitdir,
186 defaults = self.manifest.globalConfig)
187
188 self.work_git = self._GitGetByExec(self, bare=False)
189 self.bare_git = self._GitGetByExec(self, bare=True)
190
191 @property
192 def Exists(self):
193 return os.path.isdir(self.gitdir)
194
195 @property
196 def CurrentBranch(self):
197 """Obtain the name of the currently checked out branch.
198 The branch name omits the 'refs/heads/' prefix.
199 None is returned if the project is on a detached HEAD.
200 """
201 try:
202 b = self.work_git.GetHead()
203 except GitError:
204 return None
205 if b.startswith(R_HEADS):
206 return b[len(R_HEADS):]
207 return None
208
209 def IsDirty(self, consider_untracked=True):
210 """Is the working directory modified in some way?
211 """
212 self.work_git.update_index('-q',
213 '--unmerged',
214 '--ignore-missing',
215 '--refresh')
216 if self.work_git.DiffZ('diff-index','-M','--cached',HEAD):
217 return True
218 if self.work_git.DiffZ('diff-files'):
219 return True
220 if consider_untracked and self.work_git.LsOthers():
221 return True
222 return False
223
224 _userident_name = None
225 _userident_email = None
226
227 @property
228 def UserName(self):
229 """Obtain the user's personal name.
230 """
231 if self._userident_name is None:
232 self._LoadUserIdentity()
233 return self._userident_name
234
235 @property
236 def UserEmail(self):
237 """Obtain the user's email address. This is very likely
238 to be their Gerrit login.
239 """
240 if self._userident_email is None:
241 self._LoadUserIdentity()
242 return self._userident_email
243
244 def _LoadUserIdentity(self):
245 u = self.bare_git.var('GIT_COMMITTER_IDENT')
246 m = re.compile("^(.*) <([^>]*)> ").match(u)
247 if m:
248 self._userident_name = m.group(1)
249 self._userident_email = m.group(2)
250 else:
251 self._userident_name = ''
252 self._userident_email = ''
253
254 def GetRemote(self, name):
255 """Get the configuration for a single remote.
256 """
257 return self.config.GetRemote(name)
258
259 def GetBranch(self, name):
260 """Get the configuration for a single branch.
261 """
262 return self.config.GetBranch(name)
263
264
265## Status Display ##
266
267 def PrintWorkTreeStatus(self):
268 """Prints the status of the repository to stdout.
269 """
270 if not os.path.isdir(self.worktree):
271 print ''
272 print 'project %s/' % self.relpath
273 print ' missing (run "repo sync")'
274 return
275
276 self.work_git.update_index('-q',
277 '--unmerged',
278 '--ignore-missing',
279 '--refresh')
280 di = self.work_git.DiffZ('diff-index', '-M', '--cached', HEAD)
281 df = self.work_git.DiffZ('diff-files')
282 do = self.work_git.LsOthers()
283 if not di and not df and not do:
284 return
285
286 out = StatusColoring(self.config)
287 out.project('project %-40s', self.relpath + '/')
288
289 branch = self.CurrentBranch
290 if branch is None:
291 out.nobranch('(*** NO BRANCH ***)')
292 else:
293 out.branch('branch %s', branch)
294 out.nl()
295
296 paths = list()
297 paths.extend(di.keys())
298 paths.extend(df.keys())
299 paths.extend(do)
300
301 paths = list(set(paths))
302 paths.sort()
303
304 for p in paths:
305 try: i = di[p]
306 except KeyError: i = None
307
308 try: f = df[p]
309 except KeyError: f = None
310
311 if i: i_status = i.status.upper()
312 else: i_status = '-'
313
314 if f: f_status = f.status.lower()
315 else: f_status = '-'
316
317 if i and i.src_path:
318 line = ' %s%s\t%s => (%s%%)' % (i_status, f_status,
319 i.src_path, p, i.level)
320 else:
321 line = ' %s%s\t%s' % (i_status, f_status, p)
322
323 if i and not f:
324 out.added('%s', line)
325 elif (i and f) or (not i and f):
326 out.changed('%s', line)
327 elif not i and not f:
328 out.untracked('%s', line)
329 else:
330 out.write('%s', line)
331 out.nl()
332
333 def PrintWorkTreeDiff(self):
334 """Prints the status of the repository to stdout.
335 """
336 out = DiffColoring(self.config)
337 cmd = ['diff']
338 if out.is_on:
339 cmd.append('--color')
340 cmd.append(HEAD)
341 cmd.append('--')
342 p = GitCommand(self,
343 cmd,
344 capture_stdout = True,
345 capture_stderr = True)
346 has_diff = False
347 for line in p.process.stdout:
348 if not has_diff:
349 out.nl()
350 out.project('project %s/' % self.relpath)
351 out.nl()
352 has_diff = True
353 print line[:-1]
354 p.Wait()
355
356
357## Publish / Upload ##
358
359 def WasPublished(self, branch):
360 """Was the branch published (uploaded) for code review?
361 If so, returns the SHA-1 hash of the last published
362 state for the branch.
363 """
364 try:
365 return self.bare_git.rev_parse(R_PUB + branch)
366 except GitError:
367 return None
368
369 def CleanPublishedCache(self):
370 """Prunes any stale published refs.
371 """
372 heads = set()
373 canrm = {}
374 for name, id in self._allrefs.iteritems():
375 if name.startswith(R_HEADS):
376 heads.add(name)
377 elif name.startswith(R_PUB):
378 canrm[name] = id
379
380 for name, id in canrm.iteritems():
381 n = name[len(R_PUB):]
382 if R_HEADS + n not in heads:
383 self.bare_git.DeleteRef(name, id)
384
385 def GetUploadableBranches(self):
386 """List any branches which can be uploaded for review.
387 """
388 heads = {}
389 pubed = {}
390
391 for name, id in self._allrefs.iteritems():
392 if name.startswith(R_HEADS):
393 heads[name[len(R_HEADS):]] = id
394 elif name.startswith(R_PUB):
395 pubed[name[len(R_PUB):]] = id
396
397 ready = []
398 for branch, id in heads.iteritems():
399 if branch in pubed and pubed[branch] == id:
400 continue
401
402 branch = self.GetBranch(branch)
403 base = branch.LocalMerge
404 if branch.LocalMerge:
405 rb = ReviewableBranch(self, branch, base)
406 if rb.commits:
407 ready.append(rb)
408 return ready
409
410 def UploadForReview(self, branch=None):
411 """Uploads the named branch for code review.
412 """
413 if branch is None:
414 branch = self.CurrentBranch
415 if branch is None:
416 raise GitError('not currently on a branch')
417
418 branch = self.GetBranch(branch)
419 if not branch.LocalMerge:
420 raise GitError('branch %s does not track a remote' % branch.name)
421 if not branch.remote.review:
422 raise GitError('remote %s has no review url' % branch.remote.name)
423
424 dest_branch = branch.merge
425 if not dest_branch.startswith(R_HEADS):
426 dest_branch = R_HEADS + dest_branch
427
428 base_list = []
429 for name, id in self._allrefs.iteritems():
430 if branch.remote.WritesTo(name):
431 base_list.append(not_rev(name))
432 if not base_list:
433 raise GitError('no base refs, cannot upload %s' % branch.name)
434
435 print >>sys.stderr, ''
436 _info("Uploading %s to %s:", branch.name, self.name)
437 try:
438 UploadBundle(project = self,
439 server = branch.remote.review,
440 email = self.UserEmail,
441 dest_project = self.name,
442 dest_branch = dest_branch,
443 src_branch = R_HEADS + branch.name,
444 bases = base_list)
445 except proto_client.ClientLoginError:
446 raise UploadError('Login failure')
447 except urllib2.HTTPError, e:
448 raise UploadError('HTTP error %d' % e.code)
449
450 msg = "posted to %s for %s" % (branch.remote.review, dest_branch)
451 self.bare_git.UpdateRef(R_PUB + branch.name,
452 R_HEADS + branch.name,
453 message = msg)
454
455
456## Sync ##
457
458 def Sync_NetworkHalf(self):
459 """Perform only the network IO portion of the sync process.
460 Local working directory/branch state is not affected.
461 """
462 if not self.Exists:
463 print >>sys.stderr
464 print >>sys.stderr, 'Initializing project %s ...' % self.name
465 self._InitGitDir()
466 self._InitRemote()
467 for r in self.extraRemotes.values():
468 if not self._RemoteFetch(r.name):
469 return False
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700470 if not self._RemoteFetch():
471 return False
Shawn O. Pearce329c31d2008-10-24 09:17:25 -0700472 self._RepairAndroidImportErrors()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700473 self._InitMRef()
474 return True
475
476 def _CopyFiles(self):
477 for file in self.copyfiles:
478 file._Copy()
479
Shawn O. Pearce329c31d2008-10-24 09:17:25 -0700480 def _RepairAndroidImportErrors(self):
481 if self.name in ['platform/external/iptables',
482 'platform/external/libpcap',
483 'platform/external/tcpdump',
484 'platform/external/webkit',
485 'platform/system/wlan/ti']:
486 # I hate myself for doing this...
487 #
488 # In the initial Android 1.0 release these projects were
489 # shipped, some users got them, and then the history had
490 # to be rewritten to correct problems with their imports.
491 # The 'android-1.0' tag may still be pointing at the old
492 # history, so we need to drop the tag and fetch it again.
493 #
494 try:
495 remote = self.GetRemote(self.remote.name)
496 relname = remote.ToLocal(R_HEADS + 'release-1.0')
497 tagname = R_TAGS + 'android-1.0'
498 if self._revlist(not_rev(relname), tagname):
499 cmd = ['fetch', remote.name, '+%s:%s' % (tagname, tagname)]
500 GitCommand(self, cmd, bare = True).Wait()
501 except GitError:
502 pass
503
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700504 def Sync_LocalHalf(self):
505 """Perform only the local IO portion of the sync process.
506 Network access is not required.
507
508 Return:
509 True: the sync was successful
510 False: the sync requires user input
511 """
512 self._InitWorkTree()
513 self.CleanPublishedCache()
514
515 rem = self.GetRemote(self.remote.name)
516 rev = rem.ToLocal(self.revision)
517 branch = self.CurrentBranch
518
519 if branch is None:
520 # Currently on a detached HEAD. The user is assumed to
521 # not have any local modifications worth worrying about.
522 #
523 lost = self._revlist(not_rev(rev), HEAD)
524 if lost:
525 _info("[%s] Discarding %d commits", self.name, len(lost))
526 try:
527 self._Checkout(rev, quiet=True)
528 except GitError:
529 return False
530 self._CopyFiles()
531 return True
532
533 branch = self.GetBranch(branch)
534 merge = branch.LocalMerge
535
536 if not merge:
537 # The current branch has no tracking configuration.
538 # Jump off it to a deatched HEAD.
539 #
540 _info("[%s] Leaving %s"
541 " (does not track any upstream)",
542 self.name,
543 branch.name)
544 try:
545 self._Checkout(rev, quiet=True)
546 except GitError:
547 return False
548 self._CopyFiles()
549 return True
550
551 upstream_gain = self._revlist(not_rev(HEAD), rev)
552 pub = self.WasPublished(branch.name)
553 if pub:
554 not_merged = self._revlist(not_rev(rev), pub)
555 if not_merged:
556 if upstream_gain:
557 # The user has published this branch and some of those
558 # commits are not yet merged upstream. We do not want
559 # to rewrite the published commits so we punt.
560 #
561 _info("[%s] Branch %s is published,"
562 " but is now %d commits behind.",
563 self.name, branch.name, len(upstream_gain))
564 _info("[%s] Consider merging or rebasing the"
565 " unpublished commits.", self.name)
566 return True
Shawn O. Pearce23d77812008-10-30 11:06:57 -0700567 elif upstream_gain:
Shawn O. Pearcea54c5272008-10-30 11:03:00 -0700568 # We can fast-forward safely.
569 #
570 try:
571 self._FastForward(rev)
572 except GitError:
573 return False
574 self._CopyFiles()
575 return True
Shawn O. Pearce23d77812008-10-30 11:06:57 -0700576 else:
577 # Trivially no changes in the upstream.
578 #
579 return True
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700580
581 if merge == rev:
582 try:
583 old_merge = self.bare_git.rev_parse('%s@{1}' % merge)
584 except GitError:
585 old_merge = merge
Shawn O. Pearce07346002008-10-21 07:09:27 -0700586 if old_merge == '0000000000000000000000000000000000000000' \
587 or old_merge == '':
588 old_merge = merge
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700589 else:
590 # The upstream switched on us. Time to cross our fingers
591 # and pray that the old upstream also wasn't in the habit
592 # of rebasing itself.
593 #
594 _info("[%s] Manifest switched from %s to %s",
595 self.name, merge, rev)
596 old_merge = merge
597
598 if rev == old_merge:
599 upstream_lost = []
600 else:
601 upstream_lost = self._revlist(not_rev(rev), old_merge)
602
603 if not upstream_lost and not upstream_gain:
604 # Trivially no changes caused by the upstream.
605 #
606 return True
607
608 if self.IsDirty(consider_untracked=False):
609 _warn('[%s] commit (or discard) uncommitted changes'
610 ' before sync', self.name)
611 return False
612
613 if upstream_lost:
614 # Upstream rebased. Not everything in HEAD
615 # may have been caused by the user.
616 #
617 _info("[%s] Discarding %d commits removed from upstream",
618 self.name, len(upstream_lost))
619
620 branch.remote = rem
621 branch.merge = self.revision
622 branch.Save()
623
624 my_changes = self._revlist(not_rev(old_merge), HEAD)
625 if my_changes:
626 try:
627 self._Rebase(upstream = old_merge, onto = rev)
628 except GitError:
629 return False
630 elif upstream_lost:
631 try:
632 self._ResetHard(rev)
633 except GitError:
634 return False
635 else:
636 try:
637 self._FastForward(rev)
638 except GitError:
639 return False
640
641 self._CopyFiles()
642 return True
643
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700644 def AddCopyFile(self, src, dest):
645 # dest should already be an absolute path, but src is project relative
646 # make src an absolute path
647 src = os.path.join(self.worktree, src)
648 self.copyfiles.append(_CopyFile(src, dest))
649
Shawn O. Pearce632768b2008-10-23 11:58:52 -0700650 def DownloadPatchSet(self, change_id, patch_id):
651 """Download a single patch set of a single change to FETCH_HEAD.
652 """
653 remote = self.GetRemote(self.remote.name)
654
655 cmd = ['fetch', remote.name]
656 cmd.append('refs/changes/%2.2d/%d/%d' \
657 % (change_id % 100, change_id, patch_id))
658 cmd.extend(map(lambda x: str(x), remote.fetch))
659 if GitCommand(self, cmd, bare=True).Wait() != 0:
660 return None
661 return DownloadedChange(self,
662 remote.ToLocal(self.revision),
663 change_id,
664 patch_id,
665 self.bare_git.rev_parse('FETCH_HEAD'))
666
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700667
668## Branch Management ##
669
670 def StartBranch(self, name):
671 """Create a new branch off the manifest's revision.
672 """
673 branch = self.GetBranch(name)
674 branch.remote = self.GetRemote(self.remote.name)
675 branch.merge = self.revision
676
677 rev = branch.LocalMerge
678 cmd = ['checkout', '-b', branch.name, rev]
679 if GitCommand(self, cmd).Wait() == 0:
680 branch.Save()
681 else:
682 raise GitError('%s checkout %s ' % (self.name, rev))
683
684 def PruneHeads(self):
685 """Prune any topic branches already merged into upstream.
686 """
687 cb = self.CurrentBranch
688 kill = []
689 for name in self._allrefs.keys():
690 if name.startswith(R_HEADS):
691 name = name[len(R_HEADS):]
692 if cb is None or name != cb:
693 kill.append(name)
694
695 rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
696 if cb is not None \
697 and not self._revlist(HEAD + '...' + rev) \
698 and not self.IsDirty(consider_untracked = False):
699 self.work_git.DetachHead(HEAD)
700 kill.append(cb)
701
702 deleted = set()
703 if kill:
704 try:
705 old = self.bare_git.GetHead()
706 except GitError:
707 old = 'refs/heads/please_never_use_this_as_a_branch_name'
708
709 rm_re = re.compile(r"^Deleted branch (.*)\.$")
710 try:
711 self.bare_git.DetachHead(rev)
712
713 b = ['branch', '-d']
714 b.extend(kill)
715 b = GitCommand(self, b, bare=True,
716 capture_stdout=True,
717 capture_stderr=True)
718 b.Wait()
719 finally:
720 self.bare_git.SetHead(old)
721
722 for line in b.stdout.split("\n"):
723 m = rm_re.match(line)
724 if m:
725 deleted.add(m.group(1))
726
727 if deleted:
728 self.CleanPublishedCache()
729
730 if cb and cb not in kill:
731 kill.append(cb)
732 kill.sort()
733
734 kept = []
735 for branch in kill:
736 if branch not in deleted:
737 branch = self.GetBranch(branch)
738 base = branch.LocalMerge
739 if not base:
740 base = rev
741 kept.append(ReviewableBranch(self, branch, base))
742 return kept
743
744
745## Direct Git Commands ##
746
747 def _RemoteFetch(self, name=None):
748 if not name:
749 name = self.remote.name
Shawn O. Pearcece03a402008-10-28 16:12:03 -0700750 return GitCommand(self,
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700751 ['fetch', name],
Shawn O. Pearcece03a402008-10-28 16:12:03 -0700752 bare = True).Wait() == 0
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700753
754 def _Checkout(self, rev, quiet=False):
755 cmd = ['checkout']
756 if quiet:
757 cmd.append('-q')
758 cmd.append(rev)
759 cmd.append('--')
760 if GitCommand(self, cmd).Wait() != 0:
761 if self._allrefs:
762 raise GitError('%s checkout %s ' % (self.name, rev))
763
764 def _ResetHard(self, rev, quiet=True):
765 cmd = ['reset', '--hard']
766 if quiet:
767 cmd.append('-q')
768 cmd.append(rev)
769 if GitCommand(self, cmd).Wait() != 0:
770 raise GitError('%s reset --hard %s ' % (self.name, rev))
771
772 def _Rebase(self, upstream, onto = None):
773 cmd = ['rebase', '-i']
774 if onto is not None:
775 cmd.extend(['--onto', onto])
776 cmd.append(upstream)
777 if GitCommand(self, cmd, disable_editor=True).Wait() != 0:
778 raise GitError('%s rebase %s ' % (self.name, upstream))
779
780 def _FastForward(self, head):
781 cmd = ['merge', head]
782 if GitCommand(self, cmd).Wait() != 0:
783 raise GitError('%s merge %s ' % (self.name, head))
784
785 def _InitGitDir(self):
786 if not os.path.exists(self.gitdir):
787 os.makedirs(self.gitdir)
788 self.bare_git.init()
789 self.config.SetString('core.bare', None)
790
791 hooks = self._gitdir_path('hooks')
Shawn O. Pearcede646812008-10-29 14:38:12 -0700792 try:
793 to_rm = os.listdir(hooks)
794 except OSError:
795 to_rm = []
796 for old_hook in to_rm:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700797 os.remove(os.path.join(hooks, old_hook))
798
799 # TODO(sop) install custom repo hooks
800
801 m = self.manifest.manifestProject.config
802 for key in ['user.name', 'user.email']:
803 if m.Has(key, include_defaults = False):
804 self.config.SetString(key, m.GetString(key))
805
806 def _InitRemote(self):
807 if self.remote.fetchUrl:
808 remote = self.GetRemote(self.remote.name)
809
810 url = self.remote.fetchUrl
811 while url.endswith('/'):
812 url = url[:-1]
813 url += '/%s.git' % self.name
814 remote.url = url
815 remote.review = self.remote.reviewUrl
816
817 remote.ResetFetch()
818 remote.Save()
819
820 for r in self.extraRemotes.values():
821 remote = self.GetRemote(r.name)
822 remote.url = r.fetchUrl
823 remote.review = r.reviewUrl
824 remote.ResetFetch()
825 remote.Save()
826
827 def _InitMRef(self):
828 if self.manifest.branch:
829 msg = 'manifest set to %s' % self.revision
830 ref = R_M + self.manifest.branch
831
832 if IsId(self.revision):
833 dst = self.revision + '^0',
834 self.bare_git.UpdateRef(ref, dst, message = msg, detach = True)
835 else:
836 remote = self.GetRemote(self.remote.name)
837 dst = remote.ToLocal(self.revision)
838 self.bare_git.symbolic_ref('-m', msg, ref, dst)
839
840 def _InitWorkTree(self):
841 dotgit = os.path.join(self.worktree, '.git')
842 if not os.path.exists(dotgit):
843 os.makedirs(dotgit)
844
845 topdir = os.path.commonprefix([self.gitdir, dotgit])
846 if topdir.endswith('/'):
847 topdir = topdir[:-1]
848 else:
849 topdir = os.path.dirname(topdir)
850
851 tmpdir = dotgit
852 relgit = ''
853 while topdir != tmpdir:
854 relgit += '../'
855 tmpdir = os.path.dirname(tmpdir)
856 relgit += self.gitdir[len(topdir) + 1:]
857
858 for name in ['config',
859 'description',
860 'hooks',
861 'info',
862 'logs',
863 'objects',
864 'packed-refs',
865 'refs',
866 'rr-cache',
867 'svn']:
Shawn O. Pearce438ee1c2008-11-03 09:59:36 -0800868 try:
869 os.symlink(os.path.join(relgit, name),
870 os.path.join(dotgit, name))
871 except OSError, e:
872 if e.errno == errno.EPERM:
873 raise GitError('filesystem must support symlinks')
874 else:
875 raise
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700876
877 rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
878 rev = self.bare_git.rev_parse('%s^0' % rev)
879
880 f = open(os.path.join(dotgit, HEAD), 'wb')
881 f.write("%s\n" % rev)
882 f.close()
883
884 cmd = ['read-tree', '--reset', '-u']
885 cmd.append('-v')
886 cmd.append('HEAD')
887 if GitCommand(self, cmd).Wait() != 0:
888 raise GitError("cannot initialize work tree")
889
890 def _gitdir_path(self, path):
891 return os.path.join(self.gitdir, path)
892
893 def _revlist(self, *args):
894 cmd = []
895 cmd.extend(args)
896 cmd.append('--')
897 return self.work_git.rev_list(*args)
898
899 @property
900 def _allrefs(self):
901 return self.bare_git.ListRefs()
902
903 class _GitGetByExec(object):
904 def __init__(self, project, bare):
905 self._project = project
906 self._bare = bare
907
908 def ListRefs(self, *args):
909 cmdv = ['for-each-ref', '--format=%(objectname) %(refname)']
910 cmdv.extend(args)
911 p = GitCommand(self._project,
912 cmdv,
913 bare = self._bare,
914 capture_stdout = True,
915 capture_stderr = True)
916 r = {}
917 for line in p.process.stdout:
918 id, name = line[:-1].split(' ', 2)
919 r[name] = id
920 if p.Wait() != 0:
921 raise GitError('%s for-each-ref %s: %s' % (
922 self._project.name,
923 str(args),
924 p.stderr))
925 return r
926
927 def LsOthers(self):
928 p = GitCommand(self._project,
929 ['ls-files',
930 '-z',
931 '--others',
932 '--exclude-standard'],
933 bare = False,
934 capture_stdout = True,
935 capture_stderr = True)
936 if p.Wait() == 0:
937 out = p.stdout
938 if out:
939 return out[:-1].split("\0")
940 return []
941
942 def DiffZ(self, name, *args):
943 cmd = [name]
944 cmd.append('-z')
945 cmd.extend(args)
946 p = GitCommand(self._project,
947 cmd,
948 bare = False,
949 capture_stdout = True,
950 capture_stderr = True)
951 try:
952 out = p.process.stdout.read()
953 r = {}
954 if out:
955 out = iter(out[:-1].split('\0'))
956 while out:
Shawn O. Pearce02dbb6d2008-10-21 13:59:08 -0700957 try:
958 info = out.next()
959 path = out.next()
960 except StopIteration:
961 break
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700962
963 class _Info(object):
964 def __init__(self, path, omode, nmode, oid, nid, state):
965 self.path = path
966 self.src_path = None
967 self.old_mode = omode
968 self.new_mode = nmode
969 self.old_id = oid
970 self.new_id = nid
971
972 if len(state) == 1:
973 self.status = state
974 self.level = None
975 else:
976 self.status = state[:1]
977 self.level = state[1:]
978 while self.level.startswith('0'):
979 self.level = self.level[1:]
980
981 info = info[1:].split(' ')
982 info =_Info(path, *info)
983 if info.status in ('R', 'C'):
984 info.src_path = info.path
985 info.path = out.next()
986 r[info.path] = info
987 return r
988 finally:
989 p.Wait()
990
991 def GetHead(self):
992 return self.symbolic_ref(HEAD)
993
994 def SetHead(self, ref, message=None):
995 cmdv = []
996 if message is not None:
997 cmdv.extend(['-m', message])
998 cmdv.append(HEAD)
999 cmdv.append(ref)
1000 self.symbolic_ref(*cmdv)
1001
1002 def DetachHead(self, new, message=None):
1003 cmdv = ['--no-deref']
1004 if message is not None:
1005 cmdv.extend(['-m', message])
1006 cmdv.append(HEAD)
1007 cmdv.append(new)
1008 self.update_ref(*cmdv)
1009
1010 def UpdateRef(self, name, new, old=None,
1011 message=None,
1012 detach=False):
1013 cmdv = []
1014 if message is not None:
1015 cmdv.extend(['-m', message])
1016 if detach:
1017 cmdv.append('--no-deref')
1018 cmdv.append(name)
1019 cmdv.append(new)
1020 if old is not None:
1021 cmdv.append(old)
1022 self.update_ref(*cmdv)
1023
1024 def DeleteRef(self, name, old=None):
1025 if not old:
1026 old = self.rev_parse(name)
1027 self.update_ref('-d', name, old)
1028
1029 def rev_list(self, *args):
1030 cmdv = ['rev-list']
1031 cmdv.extend(args)
1032 p = GitCommand(self._project,
1033 cmdv,
1034 bare = self._bare,
1035 capture_stdout = True,
1036 capture_stderr = True)
1037 r = []
1038 for line in p.process.stdout:
1039 r.append(line[:-1])
1040 if p.Wait() != 0:
1041 raise GitError('%s rev-list %s: %s' % (
1042 self._project.name,
1043 str(args),
1044 p.stderr))
1045 return r
1046
1047 def __getattr__(self, name):
1048 name = name.replace('_', '-')
1049 def runner(*args):
1050 cmdv = [name]
1051 cmdv.extend(args)
1052 p = GitCommand(self._project,
1053 cmdv,
1054 bare = self._bare,
1055 capture_stdout = True,
1056 capture_stderr = True)
1057 if p.Wait() != 0:
1058 raise GitError('%s %s: %s' % (
1059 self._project.name,
1060 name,
1061 p.stderr))
1062 r = p.stdout
1063 if r.endswith('\n') and r.index('\n') == len(r) - 1:
1064 return r[:-1]
1065 return r
1066 return runner
1067
1068
1069class MetaProject(Project):
1070 """A special project housed under .repo.
1071 """
1072 def __init__(self, manifest, name, gitdir, worktree):
1073 repodir = manifest.repodir
1074 Project.__init__(self,
1075 manifest = manifest,
1076 name = name,
1077 gitdir = gitdir,
1078 worktree = worktree,
1079 remote = Remote('origin'),
1080 relpath = '.repo/%s' % name,
1081 revision = 'refs/heads/master')
1082
1083 def PreSync(self):
1084 if self.Exists:
1085 cb = self.CurrentBranch
1086 if cb:
1087 base = self.GetBranch(cb).merge
1088 if base:
1089 self.revision = base
1090
1091 @property
1092 def HasChanges(self):
1093 """Has the remote received new commits not yet checked out?
1094 """
1095 rev = self.GetRemote(self.remote.name).ToLocal(self.revision)
1096 if self._revlist(not_rev(HEAD), rev):
1097 return True
1098 return False