| 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.""" | 8 """A git-command for integrating reviews on Rietveld.""" |
| 9 | 9 |
| 10 from distutils.version import LooseVersion | 10 from distutils.version import LooseVersion |
| (...skipping 831 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 842 'tree-status-url', False) | 842 'tree-status-url', False) |
| 843 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True) | 843 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True) |
| 844 | 844 |
| 845 # TODO: configure a default branch to diff against, rather than this | 845 # TODO: configure a default branch to diff against, rather than this |
| 846 # svn-based hackery. | 846 # svn-based hackery. |
| 847 | 847 |
| 848 | 848 |
| 849 class ChangeDescription(object): | 849 class ChangeDescription(object): |
| 850 """Contains a parsed form of the change description.""" | 850 """Contains a parsed form of the change description.""" |
| 851 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$' | 851 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$' |
| 852 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$' |
| 852 | 853 |
| 853 def __init__(self, description): | 854 def __init__(self, description): |
| 854 self._description = (description or '').strip() | 855 self._description_lines = (description or '').strip().splitlines() |
| 855 | 856 |
| 856 @property | 857 @property # www.logilab.org/ticket/89786 |
| 857 def description(self): | 858 def description(self): # pylint: disable=E0202 |
| 858 return self._description | 859 return '\n'.join(self._description_lines) |
| 860 |
| 861 def set_description(self, desc): |
| 862 if isinstance(desc, basestring): |
| 863 lines = desc.splitlines() |
| 864 else: |
| 865 lines = [line.rstrip() for line in desc] |
| 866 while lines and not lines[0]: |
| 867 lines.pop(0) |
| 868 while lines and not lines[-1]: |
| 869 lines.pop(-1) |
| 870 self._description_lines = lines |
| 859 | 871 |
| 860 def update_reviewers(self, reviewers): | 872 def update_reviewers(self, reviewers): |
| 861 """Rewrites the R=/TBR= line(s) as a single line.""" | 873 """Rewrites the R=/TBR= line(s) as a single line each.""" |
| 862 assert isinstance(reviewers, list), reviewers | 874 assert isinstance(reviewers, list), reviewers |
| 863 if not reviewers: | 875 if not reviewers: |
| 864 return | 876 return |
| 865 regexp = re.compile(self.R_LINE, re.MULTILINE) | 877 reviewers = reviewers[:] |
| 866 matches = list(regexp.finditer(self._description)) | |
| 867 is_tbr = any(m.group(1) == 'TBR' for m in matches) | |
| 868 if len(matches) > 1: | |
| 869 # Erase all except the first one. | |
| 870 for i in xrange(len(matches) - 1, 0, -1): | |
| 871 self._description = ( | |
| 872 self._description[:matches[i].start()] + | |
| 873 self._description[matches[i].end():]) | |
| 874 | 878 |
| 875 if is_tbr: | 879 # Get the set of R= and TBR= lines and remove them from the desciption. |
| 876 new_r_line = 'TBR=' + ', '.join(reviewers) | 880 regexp = re.compile(self.R_LINE) |
| 881 matches = [regexp.match(line) for line in self._description_lines] |
| 882 new_desc = [l for i, l in enumerate(self._description_lines) |
| 883 if not matches[i]] |
| 884 self.set_description(new_desc) |
| 885 |
| 886 # Construct new unified R= and TBR= lines. |
| 887 r_names = [] |
| 888 tbr_names = [] |
| 889 for match in matches: |
| 890 if not match: |
| 891 continue |
| 892 people = cleanup_list([match.group(2).strip()]) |
| 893 if match.group(1) == 'TBR': |
| 894 tbr_names.extend(people) |
| 895 else: |
| 896 r_names.extend(people) |
| 897 for name in r_names: |
| 898 if name not in reviewers: |
| 899 reviewers.append(name) |
| 900 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None |
| 901 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None |
| 902 |
| 903 # Put the new lines in the description where the old first R= line was. |
| 904 line_loc = next((i for i, match in enumerate(matches) if match), -1) |
| 905 if 0 <= line_loc < len(self._description_lines): |
| 906 if new_tbr_line: |
| 907 self._description_lines.insert(line_loc, new_tbr_line) |
| 908 if new_r_line: |
| 909 self._description_lines.insert(line_loc, new_r_line) |
| 877 else: | 910 else: |
| 878 new_r_line = 'R=' + ', '.join(reviewers) | 911 if new_r_line: |
| 879 | 912 self.append_footer(new_r_line) |
| 880 if matches: | 913 if new_tbr_line: |
| 881 self._description = ( | 914 self.append_footer(new_tbr_line) |
| 882 self._description[:matches[0].start()] + new_r_line + | |
| 883 self._description[matches[0].end():]).strip() | |
| 884 else: | |
| 885 self.append_footer(new_r_line) | |
| 886 | 915 |
| 887 def prompt(self): | 916 def prompt(self): |
| 888 """Asks the user to update the description.""" | 917 """Asks the user to update the description.""" |
| 889 self._description = ( | 918 self.set_description([ |
| 890 '# Enter a description of the change.\n' | 919 '# Enter a description of the change.', |
| 891 '# This will be displayed on the codereview site.\n' | 920 '# This will be displayed on the codereview site.', |
| 892 '# The first line will also be used as the subject of the review.\n' | 921 '# The first line will also be used as the subject of the review.', |
| 893 '#--------------------This line is 72 characters long' | 922 '#--------------------This line is 72 characters long' |
| 894 '--------------------\n' | 923 '--------------------', |
| 895 ) + self._description | 924 ] + self._description_lines) |
| 896 | 925 |
| 897 if '\nBUG=' not in self._description: | 926 regexp = re.compile(self.BUG_LINE) |
| 927 if not any((regexp.match(line) for line in self._description_lines)): |
| 898 self.append_footer('BUG=') | 928 self.append_footer('BUG=') |
| 899 content = gclient_utils.RunEditor(self._description, True, | 929 content = gclient_utils.RunEditor(self.description, True, |
| 900 git_editor=settings.GetGitEditor()) | 930 git_editor=settings.GetGitEditor()) |
| 901 if not content: | 931 if not content: |
| 902 DieWithError('Running editor failed') | 932 DieWithError('Running editor failed') |
| 933 lines = content.splitlines() |
| 903 | 934 |
| 904 # Strip off comments. | 935 # Strip off comments. |
| 905 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip() | 936 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')] |
| 906 if not content: | 937 if not clean_lines: |
| 907 DieWithError('No CL description, aborting') | 938 DieWithError('No CL description, aborting') |
| 908 self._description = content | 939 self.set_description(clean_lines) |
| 909 | 940 |
| 910 def append_footer(self, line): | 941 def append_footer(self, line): |
| 911 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't. | 942 if self._description_lines: |
| 912 if self._description: | 943 # Add an empty line if either the last line or the new line isn't a tag. |
| 913 if '\n' not in self._description: | 944 last_line = self._description_lines[-1] |
| 914 self._description += '\n' | 945 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or |
| 915 else: | 946 not presubmit_support.Change.TAG_LINE_RE.match(line)): |
| 916 last_line = self._description.rsplit('\n', 1)[1] | 947 self._description_lines.append('') |
| 917 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or | 948 self._description_lines.append(line) |
| 918 not presubmit_support.Change.TAG_LINE_RE.match(line)): | |
| 919 self._description += '\n' | |
| 920 self._description += '\n' + line | |
| 921 | 949 |
| 922 def get_reviewers(self): | 950 def get_reviewers(self): |
| 923 """Retrieves the list of reviewers.""" | 951 """Retrieves the list of reviewers.""" |
| 924 regexp = re.compile(self.R_LINE, re.MULTILINE) | 952 matches = [re.match(self.R_LINE, line) for line in self._description_lines] |
| 925 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)] | 953 reviewers = [match.group(2).strip() for match in matches if match] |
| 926 return cleanup_list(reviewers) | 954 return cleanup_list(reviewers) |
| 927 | 955 |
| 928 | 956 |
| 929 def get_approving_reviewers(props): | 957 def get_approving_reviewers(props): |
| 930 """Retrieves the reviewers that approved a CL from the issue properties with | 958 """Retrieves the reviewers that approved a CL from the issue properties with |
| 931 messages. | 959 messages. |
| 932 | 960 |
| 933 Note that the list may contain reviewers that are not committer, thus are not | 961 Note that the list may contain reviewers that are not committer, thus are not |
| 934 considered by the CQ. | 962 considered by the CQ. |
| 935 """ | 963 """ |
| (...skipping 1261 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2197 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' | 2225 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith ' |
| 2198 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) | 2226 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) |
| 2199 | 2227 |
| 2200 | 2228 |
| 2201 if __name__ == '__main__': | 2229 if __name__ == '__main__': |
| 2202 # These affect sys.stdout so do it outside of main() to simplify mocks in | 2230 # These affect sys.stdout so do it outside of main() to simplify mocks in |
| 2203 # unit testing. | 2231 # unit testing. |
| 2204 fix_encoding.fix_encoding() | 2232 fix_encoding.fix_encoding() |
| 2205 colorama.init() | 2233 colorama.init() |
| 2206 sys.exit(main(sys.argv[1:])) | 2234 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |