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 |