OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 # Copyright (C) 2008 Evan Martin <martine@danga.com> | 6 # Copyright (C) 2008 Evan Martin <martine@danga.com> |
7 | 7 |
8 """A git-command for integrating reviews on Rietveld and Gerrit.""" | 8 """A git-command for integrating reviews on Rietveld and Gerrit.""" |
9 | 9 |
10 from distutils.version import LooseVersion | 10 from distutils.version import LooseVersion |
11 from multiprocessing.pool import ThreadPool | 11 from multiprocessing.pool import ThreadPool |
12 import base64 | 12 import base64 |
13 import collections | 13 import collections |
14 import glob | 14 import glob |
15 import httplib | 15 import httplib |
16 import json | 16 import json |
17 import logging | 17 import logging |
18 import multiprocessing | 18 import multiprocessing |
19 import optparse | 19 import optparse |
20 import os | 20 import os |
21 import Queue | |
22 import re | 21 import re |
23 import stat | 22 import stat |
24 import sys | 23 import sys |
25 import tempfile | |
26 import textwrap | 24 import textwrap |
27 import time | 25 import time |
28 import traceback | 26 import traceback |
29 import urllib | 27 import urllib |
30 import urllib2 | 28 import urllib2 |
31 import urlparse | 29 import urlparse |
32 import uuid | 30 import uuid |
33 import webbrowser | 31 import webbrowser |
34 import zlib | 32 import zlib |
35 | 33 |
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
125 stdout=subprocess2.PIPE, | 123 stdout=subprocess2.PIPE, |
126 stderr=stderr) | 124 stderr=stderr) |
127 return code, out[0] | 125 return code, out[0] |
128 except ValueError: | 126 except ValueError: |
129 # When the subprocess fails, it returns None. That triggers a ValueError | 127 # When the subprocess fails, it returns None. That triggers a ValueError |
130 # when trying to unpack the return value into (out, code). | 128 # when trying to unpack the return value into (out, code). |
131 return 1, '' | 129 return 1, '' |
132 | 130 |
133 | 131 |
134 def RunGitSilent(args): | 132 def RunGitSilent(args): |
135 """Returns stdout, suppresses stderr and ingores the return code.""" | 133 """Returns stdout, suppresses stderr and ignores the return code.""" |
136 return RunGitWithCode(args, suppress_stderr=True)[1] | 134 return RunGitWithCode(args, suppress_stderr=True)[1] |
137 | 135 |
138 | 136 |
139 def IsGitVersionAtLeast(min_version): | 137 def IsGitVersionAtLeast(min_version): |
140 prefix = 'git version ' | 138 prefix = 'git version ' |
141 version = RunGit(['--version']).strip() | 139 version = RunGit(['--version']).strip() |
142 return (version.startswith(prefix) and | 140 return (version.startswith(prefix) and |
143 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version)) | 141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version)) |
144 | 142 |
145 | 143 |
(...skipping 776 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
922 global settings | 920 global settings |
923 if not settings: | 921 if not settings: |
924 # Happens when git_cl.py is used as a utility library. | 922 # Happens when git_cl.py is used as a utility library. |
925 settings = Settings() | 923 settings = Settings() |
926 | 924 |
927 if issue: | 925 if issue: |
928 assert codereview, 'codereview must be known, if issue is known' | 926 assert codereview, 'codereview must be known, if issue is known' |
929 | 927 |
930 self.branchref = branchref | 928 self.branchref = branchref |
931 if self.branchref: | 929 if self.branchref: |
| 930 assert branchref.startswith('refs/heads/') |
932 self.branch = ShortBranchName(self.branchref) | 931 self.branch = ShortBranchName(self.branchref) |
933 else: | 932 else: |
934 self.branch = None | 933 self.branch = None |
935 self.upstream_branch = None | 934 self.upstream_branch = None |
936 self.lookedup_issue = False | 935 self.lookedup_issue = False |
937 self.issue = issue or None | 936 self.issue = issue or None |
938 self.has_description = False | 937 self.has_description = False |
939 self.description = None | 938 self.description = None |
940 self.lookedup_patchset = False | 939 self.lookedup_patchset = False |
941 self.patchset = None | 940 self.patchset = None |
(...skipping 500 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1442 | 1441 |
1443 def GetApprovingReviewers(self): | 1442 def GetApprovingReviewers(self): |
1444 return self._codereview_impl.GetApprovingReviewers() | 1443 return self._codereview_impl.GetApprovingReviewers() |
1445 | 1444 |
1446 def GetMostRecentPatchset(self): | 1445 def GetMostRecentPatchset(self): |
1447 return self._codereview_impl.GetMostRecentPatchset() | 1446 return self._codereview_impl.GetMostRecentPatchset() |
1448 | 1447 |
1449 def __getattr__(self, attr): | 1448 def __getattr__(self, attr): |
1450 # This is because lots of untested code accesses Rietveld-specific stuff | 1449 # This is because lots of untested code accesses Rietveld-specific stuff |
1451 # directly, and it's hard to fix for sure. So, just let it work, and fix | 1450 # directly, and it's hard to fix for sure. So, just let it work, and fix |
1452 # on a cases by case basis. | 1451 # on a case by case basis. |
1453 return getattr(self._codereview_impl, attr) | 1452 return getattr(self._codereview_impl, attr) |
1454 | 1453 |
1455 | 1454 |
1456 class _ChangelistCodereviewBase(object): | 1455 class _ChangelistCodereviewBase(object): |
1457 """Abstract base class encapsulating codereview specifics of a changelist.""" | 1456 """Abstract base class encapsulating codereview specifics of a changelist.""" |
1458 def __init__(self, changelist): | 1457 def __init__(self, changelist): |
1459 self._changelist = changelist # instance of Changelist | 1458 self._changelist = changelist # instance of Changelist |
1460 | 1459 |
1461 def __getattr__(self, attr): | 1460 def __getattr__(self, attr): |
1462 # Forward methods to changelist. | 1461 # Forward methods to changelist. |
(...skipping 465 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1928 target_ref = GetTargetRef(remote, remote_branch, options.target_branch, | 1927 target_ref = GetTargetRef(remote, remote_branch, options.target_branch, |
1929 settings.GetPendingRefPrefix()) | 1928 settings.GetPendingRefPrefix()) |
1930 if target_ref: | 1929 if target_ref: |
1931 upload_args.extend(['--target_ref', target_ref]) | 1930 upload_args.extend(['--target_ref', target_ref]) |
1932 | 1931 |
1933 # Look for dependent patchsets. See crbug.com/480453 for more details. | 1932 # Look for dependent patchsets. See crbug.com/480453 for more details. |
1934 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch()) | 1933 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch()) |
1935 upstream_branch = ShortBranchName(upstream_branch) | 1934 upstream_branch = ShortBranchName(upstream_branch) |
1936 if remote is '.': | 1935 if remote is '.': |
1937 # A local branch is being tracked. | 1936 # A local branch is being tracked. |
1938 local_branch = ShortBranchName(upstream_branch) | 1937 local_branch = upstream_branch |
1939 if settings.GetIsSkipDependencyUpload(local_branch): | 1938 if settings.GetIsSkipDependencyUpload(local_branch): |
1940 print | 1939 print |
1941 print ('Skipping dependency patchset upload because git config ' | 1940 print ('Skipping dependency patchset upload because git config ' |
1942 'branch.%s.skip-deps-uploads is set to True.' % local_branch) | 1941 'branch.%s.skip-deps-uploads is set to True.' % local_branch) |
1943 print | 1942 print |
1944 else: | 1943 else: |
1945 auth_config = auth.extract_auth_config_from_options(options) | 1944 auth_config = auth.extract_auth_config_from_options(options) |
1946 branch_cl = Changelist(branchref=local_branch, | 1945 branch_cl = Changelist(branchref='refs/heads/'+local_branch, |
1947 auth_config=auth_config) | 1946 auth_config=auth_config) |
1948 branch_cl_issue_url = branch_cl.GetIssueURL() | 1947 branch_cl_issue_url = branch_cl.GetIssueURL() |
1949 branch_cl_issue = branch_cl.GetIssue() | 1948 branch_cl_issue = branch_cl.GetIssue() |
1950 branch_cl_patchset = branch_cl.GetPatchset() | 1949 branch_cl_patchset = branch_cl.GetPatchset() |
1951 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset: | 1950 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset: |
1952 upload_args.extend( | 1951 upload_args.extend( |
1953 ['--depends_on_patchset', '%s:%s' % ( | 1952 ['--depends_on_patchset', '%s:%s' % ( |
1954 branch_cl_issue, branch_cl_patchset)]) | 1953 branch_cl_issue, branch_cl_patchset)]) |
1955 print( | 1954 print( |
1956 '\n' | 1955 '\n' |
(...skipping 961 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
2918 return { | 2917 return { |
2919 'unsent': Fore.RED, | 2918 'unsent': Fore.RED, |
2920 'waiting': Fore.BLUE, | 2919 'waiting': Fore.BLUE, |
2921 'reply': Fore.YELLOW, | 2920 'reply': Fore.YELLOW, |
2922 'lgtm': Fore.GREEN, | 2921 'lgtm': Fore.GREEN, |
2923 'commit': Fore.MAGENTA, | 2922 'commit': Fore.MAGENTA, |
2924 'closed': Fore.CYAN, | 2923 'closed': Fore.CYAN, |
2925 'error': Fore.WHITE, | 2924 'error': Fore.WHITE, |
2926 }.get(status, Fore.WHITE) | 2925 }.get(status, Fore.WHITE) |
2927 | 2926 |
2928 def fetch_cl_status(branch, auth_config=None): | |
2929 """Fetches information for an issue and returns (branch, issue, status).""" | |
2930 cl = Changelist(branchref=branch, auth_config=auth_config) | |
2931 url = cl.GetIssueURL() | |
2932 status = cl.GetStatus() | |
2933 | 2927 |
2934 if url and (not status or status == 'error'): | 2928 def get_cl_statuses(changes, fine_grained, max_processes=None): |
2935 # The issue probably doesn't exist anymore. | 2929 """Returns a blocking iterable of (cl, status) for given branches. |
2936 url += ' (broken)' | |
2937 | |
2938 return (branch, url, status) | |
2939 | |
2940 def get_cl_statuses( | |
2941 branches, fine_grained, max_processes=None, auth_config=None): | |
2942 """Returns a blocking iterable of (branch, issue, status) for given branches. | |
2943 | 2930 |
2944 If fine_grained is true, this will fetch CL statuses from the server. | 2931 If fine_grained is true, this will fetch CL statuses from the server. |
2945 Otherwise, simply indicate if there's a matching url for the given branches. | 2932 Otherwise, simply indicate if there's a matching url for the given branches. |
2946 | 2933 |
2947 If max_processes is specified, it is used as the maximum number of processes | 2934 If max_processes is specified, it is used as the maximum number of processes |
2948 to spawn to fetch CL status from the server. Otherwise 1 process per branch is | 2935 to spawn to fetch CL status from the server. Otherwise 1 process per branch is |
2949 spawned. | 2936 spawned. |
2950 | 2937 |
2951 See GetStatus() for a list of possible statuses. | 2938 See GetStatus() for a list of possible statuses. |
2952 """ | 2939 """ |
2953 def fetch(branch): | |
2954 if not branch: | |
2955 return None | |
2956 | |
2957 return fetch_cl_status(branch, auth_config=auth_config) | |
2958 | |
2959 # Silence upload.py otherwise it becomes unwieldly. | 2940 # Silence upload.py otherwise it becomes unwieldly. |
2960 upload.verbosity = 0 | 2941 upload.verbosity = 0 |
2961 | 2942 |
2962 if fine_grained: | 2943 if fine_grained: |
2963 # Process one branch synchronously to work through authentication, then | 2944 # Process one branch synchronously to work through authentication, then |
2964 # spawn processes to process all the other branches in parallel. | 2945 # spawn processes to process all the other branches in parallel. |
2965 if branches: | 2946 if changes: |
| 2947 fetch = lambda cl: (cl, cl.GetStatus()) |
| 2948 yield fetch(changes[0]) |
2966 | 2949 |
2967 yield fetch(branches[0]) | 2950 changes_to_fetch = changes[1:] |
| 2951 pool = ThreadPool( |
| 2952 min(max_processes, len(changes_to_fetch)) |
| 2953 if max_processes is not None |
| 2954 else len(changes_to_fetch)) |
2968 | 2955 |
2969 branches_to_fetch = branches[1:] | 2956 fetched_cls = set() |
2970 pool = ThreadPool( | 2957 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__() |
2971 min(max_processes, len(branches_to_fetch)) | |
2972 if max_processes is not None | |
2973 else len(branches_to_fetch)) | |
2974 | |
2975 fetched_branches = set() | |
2976 it = pool.imap_unordered(fetch, branches_to_fetch).__iter__() | |
2977 while True: | 2958 while True: |
2978 try: | 2959 try: |
2979 row = it.next(timeout=5) | 2960 row = it.next(timeout=5) |
2980 except multiprocessing.TimeoutError: | 2961 except multiprocessing.TimeoutError: |
2981 break | 2962 break |
2982 | 2963 |
2983 fetched_branches.add(row[0]) | 2964 fetched_cls.add(row[0]) |
2984 yield row | 2965 yield row |
2985 | 2966 |
2986 # Add any branches that failed to fetch. | 2967 # Add any branches that failed to fetch. |
2987 for b in set(branches_to_fetch) - fetched_branches: | 2968 for cl in set(changes_to_fetch) - fetched_cls: |
2988 cl = Changelist(branchref=b, auth_config=auth_config) | 2969 yield (cl, 'error') |
2989 yield (b, cl.GetIssueURL() if b else None, 'error') | |
2990 | 2970 |
2991 else: | 2971 else: |
2992 # Do not use GetApprovingReviewers(), since it requires an HTTP request. | 2972 # Do not use GetApprovingReviewers(), since it requires an HTTP request. |
2993 for b in branches: | 2973 for cl in changes: |
2994 cl = Changelist(branchref=b, auth_config=auth_config) | 2974 yield (cl, 'waiting' if cl.GetIssueURL() else 'error') |
2995 url = cl.GetIssueURL() if b else None | |
2996 yield (b, url, 'waiting' if url else 'error') | |
2997 | 2975 |
2998 | 2976 |
2999 def upload_branch_deps(cl, args): | 2977 def upload_branch_deps(cl, args): |
3000 """Uploads CLs of local branches that are dependents of the current branch. | 2978 """Uploads CLs of local branches that are dependents of the current branch. |
3001 | 2979 |
3002 If the local branch dependency tree looks like: | 2980 If the local branch dependency tree looks like: |
3003 test1 -> test2.1 -> test3.1 | 2981 test1 -> test2.1 -> test3.1 |
3004 -> test3.2 | 2982 -> test3.2 |
3005 -> test2.2 -> test3.3 | 2983 -> test2.2 -> test3.3 |
3006 | 2984 |
(...skipping 132 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
3139 url = cl.GetIssueURL() | 3117 url = cl.GetIssueURL() |
3140 if url: | 3118 if url: |
3141 print url | 3119 print url |
3142 return 0 | 3120 return 0 |
3143 | 3121 |
3144 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads']) | 3122 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads']) |
3145 if not branches: | 3123 if not branches: |
3146 print('No local branch found.') | 3124 print('No local branch found.') |
3147 return 0 | 3125 return 0 |
3148 | 3126 |
3149 changes = ( | 3127 changes = [ |
3150 Changelist(branchref=b, auth_config=auth_config) | 3128 Changelist(branchref=b, auth_config=auth_config) |
3151 for b in branches.splitlines()) | 3129 for b in branches.splitlines()] |
3152 # TODO(tandrii): refactor to use CLs list instead of branches list. | |
3153 branches = [c.GetBranch() for c in changes] | |
3154 alignment = max(5, max(len(b) for b in branches)) | |
3155 print 'Branches associated with reviews:' | 3130 print 'Branches associated with reviews:' |
3156 output = get_cl_statuses(branches, | 3131 output = get_cl_statuses(changes, |
3157 fine_grained=not options.fast, | 3132 fine_grained=not options.fast, |
3158 max_processes=options.maxjobs, | 3133 max_processes=options.maxjobs) |
3159 auth_config=auth_config) | |
3160 | 3134 |
3161 branch_statuses = {} | 3135 branch_statuses = {} |
3162 alignment = max(5, max(len(ShortBranchName(b)) for b in branches)) | 3136 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes)) |
3163 for branch in sorted(branches): | 3137 for cl in sorted(changes, key=lambda c: c.GetBranch()): |
| 3138 branch = cl.GetBranch() |
3164 while branch not in branch_statuses: | 3139 while branch not in branch_statuses: |
3165 b, i, status = output.next() | 3140 c, status = output.next() |
3166 branch_statuses[b] = (i, status) | 3141 branch_statuses[c.GetBranch()] = status |
3167 issue_url, status = branch_statuses.pop(branch) | 3142 status = branch_statuses.pop(branch) |
| 3143 url = cl.GetIssueURL() |
| 3144 if url and (not status or status == 'error'): |
| 3145 # The issue probably doesn't exist anymore. |
| 3146 url += ' (broken)' |
| 3147 |
3168 color = color_for_status(status) | 3148 color = color_for_status(status) |
3169 reset = Fore.RESET | 3149 reset = Fore.RESET |
3170 if not setup_color.IS_TTY: | 3150 if not setup_color.IS_TTY: |
3171 color = '' | 3151 color = '' |
3172 reset = '' | 3152 reset = '' |
3173 status_str = '(%s)' % status if status else '' | 3153 status_str = '(%s)' % status if status else '' |
3174 print ' %*s : %s%s %s%s' % ( | 3154 print ' %*s : %s%s %s%s' % ( |
3175 alignment, ShortBranchName(branch), color, issue_url, status_str, | 3155 alignment, ShortBranchName(branch), color, url, |
3176 reset) | 3156 status_str, reset) |
3177 | 3157 |
3178 cl = Changelist(auth_config=auth_config) | 3158 cl = Changelist(auth_config=auth_config) |
3179 print | 3159 print |
3180 print 'Current branch:', | 3160 print 'Current branch:', |
3181 print cl.GetBranch() | 3161 print cl.GetBranch() |
3182 if not cl.GetIssue(): | 3162 if not cl.GetIssue(): |
3183 print 'No issue assigned.' | 3163 print 'No issue assigned.' |
3184 return 0 | 3164 return 0 |
3185 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()) | 3165 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()) |
3186 if not options.fast: | 3166 if not options.fast: |
(...skipping 1720 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
4907 if __name__ == '__main__': | 4887 if __name__ == '__main__': |
4908 # These affect sys.stdout so do it outside of main() to simplify mocks in | 4888 # These affect sys.stdout so do it outside of main() to simplify mocks in |
4909 # unit testing. | 4889 # unit testing. |
4910 fix_encoding.fix_encoding() | 4890 fix_encoding.fix_encoding() |
4911 setup_color.init() | 4891 setup_color.init() |
4912 try: | 4892 try: |
4913 sys.exit(main(sys.argv[1:])) | 4893 sys.exit(main(sys.argv[1:])) |
4914 except KeyboardInterrupt: | 4894 except KeyboardInterrupt: |
4915 sys.stderr.write('interrupted\n') | 4895 sys.stderr.write('interrupted\n') |
4916 sys.exit(1) | 4896 sys.exit(1) |
OLD | NEW |