| 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 |