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