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