Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 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 import logging | 10 import logging |
| (...skipping 30 matching lines...) Expand all Loading... | |
| 41 import scm | 41 import scm |
| 42 import subprocess2 | 42 import subprocess2 |
| 43 import watchlists | 43 import watchlists |
| 44 | 44 |
| 45 | 45 |
| 46 DEFAULT_SERVER = 'http://codereview.appspot.com' | 46 DEFAULT_SERVER = 'http://codereview.appspot.com' |
| 47 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' | 47 POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s' |
| 48 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' | 48 DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup' |
| 49 | 49 |
| 50 | 50 |
| 51 # Initialized in main() | |
| 52 settings = None | |
| 53 | |
| 54 | |
| 51 def DieWithError(message): | 55 def DieWithError(message): |
| 52 print >> sys.stderr, message | 56 print >> sys.stderr, message |
| 53 sys.exit(1) | 57 sys.exit(1) |
| 54 | 58 |
| 55 | 59 |
| 56 def RunCommand(args, error_ok=False, error_message=None, **kwargs): | 60 def RunCommand(args, error_ok=False, error_message=None, **kwargs): |
| 57 try: | 61 try: |
| 58 return subprocess2.check_output(args, shell=False, **kwargs) | 62 return subprocess2.check_output(args, shell=False, **kwargs) |
| 59 except subprocess2.CalledProcessError, e: | 63 except subprocess2.CalledProcessError, e: |
| 60 if not error_ok: | 64 if not error_ok: |
| (...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 144 class Settings(object): | 148 class Settings(object): |
| 145 def __init__(self): | 149 def __init__(self): |
| 146 self.default_server = None | 150 self.default_server = None |
| 147 self.cc = None | 151 self.cc = None |
| 148 self.root = None | 152 self.root = None |
| 149 self.is_git_svn = None | 153 self.is_git_svn = None |
| 150 self.svn_branch = None | 154 self.svn_branch = None |
| 151 self.tree_status_url = None | 155 self.tree_status_url = None |
| 152 self.viewvc_url = None | 156 self.viewvc_url = None |
| 153 self.updated = False | 157 self.updated = False |
| 158 self.did_migrate_check = False | |
| 154 | 159 |
| 155 def LazyUpdateIfNeeded(self): | 160 def LazyUpdateIfNeeded(self): |
| 156 """Updates the settings from a codereview.settings file, if available.""" | 161 """Updates the settings from a codereview.settings file, if available.""" |
| 157 if not self.updated: | 162 if not self.updated: |
| 158 cr_settings_file = FindCodereviewSettingsFile() | 163 cr_settings_file = FindCodereviewSettingsFile() |
| 159 if cr_settings_file: | 164 if cr_settings_file: |
| 160 LoadCodereviewSettingsFromFile(cr_settings_file) | 165 LoadCodereviewSettingsFromFile(cr_settings_file) |
| 161 self.updated = True | 166 self.updated = True |
| 162 | 167 |
| 163 def GetDefaultServerUrl(self, error_ok=False): | 168 def GetDefaultServerUrl(self, error_ok=False): |
| (...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 265 return self.viewvc_url | 270 return self.viewvc_url |
| 266 | 271 |
| 267 def GetDefaultCCList(self): | 272 def GetDefaultCCList(self): |
| 268 return self._GetConfig('rietveld.cc', error_ok=True) | 273 return self._GetConfig('rietveld.cc', error_ok=True) |
| 269 | 274 |
| 270 def _GetConfig(self, param, **kwargs): | 275 def _GetConfig(self, param, **kwargs): |
| 271 self.LazyUpdateIfNeeded() | 276 self.LazyUpdateIfNeeded() |
| 272 return RunGit(['config', param], **kwargs).strip() | 277 return RunGit(['config', param], **kwargs).strip() |
| 273 | 278 |
| 274 | 279 |
| 275 settings = Settings() | |
| 276 | |
| 277 | |
| 278 did_migrate_check = False | |
| 279 def CheckForMigration(): | 280 def CheckForMigration(): |
| 280 """Migrate from the old issue format, if found. | 281 """Migrate from the old issue format, if found. |
| 281 | 282 |
| 282 We used to store the branch<->issue mapping in a file in .git, but it's | 283 We used to store the branch<->issue mapping in a file in .git, but it's |
| 283 better to store it in the .git/config, since deleting a branch deletes that | 284 better to store it in the .git/config, since deleting a branch deletes that |
| 284 branch's entry there. | 285 branch's entry there. |
| 285 """ | 286 """ |
| 286 | 287 |
| 287 # Don't run more than once. | 288 # Don't run more than once. |
| 288 global did_migrate_check | 289 if settings.did_migrate_check: |
| 289 if did_migrate_check: | |
| 290 return | 290 return |
| 291 | 291 |
| 292 gitdir = RunGit(['rev-parse', '--git-dir']).strip() | 292 gitdir = RunGit(['rev-parse', '--git-dir']).strip() |
| 293 storepath = os.path.join(gitdir, 'cl-mapping') | 293 storepath = os.path.join(gitdir, 'cl-mapping') |
| 294 if os.path.exists(storepath): | 294 if os.path.exists(storepath): |
| 295 print "old-style git-cl mapping file (%s) found; migrating." % storepath | 295 print "old-style git-cl mapping file (%s) found; migrating." % storepath |
| 296 store = open(storepath, 'r') | 296 store = open(storepath, 'r') |
| 297 for line in store: | 297 for line in store: |
| 298 branch, issue = line.strip().split() | 298 branch, issue = line.strip().split() |
| 299 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch), | 299 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch), |
| 300 issue]) | 300 issue]) |
| 301 store.close() | 301 store.close() |
| 302 os.remove(storepath) | 302 os.remove(storepath) |
| 303 did_migrate_check = True | 303 settings.did_migrate_check = True |
| 304 | 304 |
| 305 | 305 |
| 306 def ShortBranchName(branch): | 306 def ShortBranchName(branch): |
| 307 """Convert a name like 'refs/heads/foo' to just 'foo'.""" | 307 """Convert a name like 'refs/heads/foo' to just 'foo'.""" |
| 308 return branch.replace('refs/heads/', '') | 308 return branch.replace('refs/heads/', '') |
| 309 | 309 |
| 310 | 310 |
| 311 class Changelist(object): | 311 class Changelist(object): |
| 312 def __init__(self, branchref=None): | 312 def __init__(self, branchref=None): |
| 313 # Poke settings so we get the "configure your server" message if necessary. | 313 # Poke settings so we get the "configure your server" message if necessary. |
| (...skipping 321 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 635 self.log_desc = log_desc | 635 self.log_desc = log_desc |
| 636 self.reviewers = reviewers | 636 self.reviewers = reviewers |
| 637 self.description = self.log_desc | 637 self.description = self.log_desc |
| 638 | 638 |
| 639 def Update(self): | 639 def Update(self): |
| 640 initial_text = """# Enter a description of the change. | 640 initial_text = """# Enter a description of the change. |
| 641 # This will displayed on the codereview site. | 641 # This will displayed on the codereview site. |
| 642 # The first line will also be used as the subject of the review. | 642 # The first line will also be used as the subject of the review. |
| 643 """ | 643 """ |
| 644 initial_text += self.description | 644 initial_text += self.description |
| 645 if 'R=' not in self.description and self.reviewers: | 645 if ('\nR=' not in self.description and |
| 646 '\nTBR=' not in self.description and | |
| 647 self.reviewers): | |
| 646 initial_text += '\nR=' + self.reviewers | 648 initial_text += '\nR=' + self.reviewers |
| 647 if 'BUG=' not in self.description: | 649 if '\nBUG=' not in self.description: |
| 648 initial_text += '\nBUG=' | 650 initial_text += '\nBUG=' |
| 649 if 'TEST=' not in self.description: | 651 if '\nTEST=' not in self.description: |
| 650 initial_text += '\nTEST=' | 652 initial_text += '\nTEST=' |
| 653 initial_text = initial_text.rstrip('\n') + '\n' | |
| 651 content = gclient_utils.RunEditor(initial_text, True) | 654 content = gclient_utils.RunEditor(initial_text, True) |
| 652 if not content: | 655 if not content: |
| 653 DieWithError('Running editor failed') | 656 DieWithError('Running editor failed') |
| 654 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip() | 657 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip() |
| 655 if not content: | 658 if not content: |
| 656 DieWithError('No CL description, aborting') | 659 DieWithError('No CL description, aborting') |
| 657 self._ParseDescription(content) | 660 self._ParseDescription(content) |
| 658 | 661 |
| 659 def _ParseDescription(self, description): | 662 def _ParseDescription(self, description): |
| 663 """Updates the list of reviewers and subject from the description.""" | |
| 660 if not description: | 664 if not description: |
| 661 self.description = description | 665 self.description = description |
| 662 return | 666 return |
| 663 | 667 |
| 664 parsed_lines = [] | 668 self.description = description.strip('\n') + '\n' |
| 665 reviewers_regexp = re.compile('\s*R=(.+)') | 669 self.subject = description.split('\n', 1)[0] |
| 666 reviewers = '' | 670 # Retrieves all reviewer lines |
| 667 subject = '' | 671 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE) |
|
Dirk Pranke
2011/11/29 23:51:57
Nit: I'm not sure you need or want re.MULTILINE he
M-A Ruel
2011/11/30 14:14:18
Yes it's necessary.
| |
| 668 for l in description.splitlines(): | 672 self.reviewers = ','.join( |
| 669 if not subject: | 673 i.group(2).strip() for i in regexp.finditer(self.description)) |
| 670 subject = l | |
| 671 matched_reviewers = reviewers_regexp.match(l) | |
| 672 if matched_reviewers: | |
| 673 reviewers = matched_reviewers.group(1) | |
| 674 parsed_lines.append(l) | |
| 675 | |
| 676 self.description = '\n'.join(parsed_lines) + '\n' | |
| 677 self.subject = subject | |
| 678 self.reviewers = reviewers | |
| 679 | 674 |
| 680 def IsEmpty(self): | 675 def IsEmpty(self): |
| 681 return not self.description | 676 return not self.description |
| 682 | 677 |
| 683 | 678 |
| 684 def FindCodereviewSettingsFile(filename='codereview.settings'): | 679 def FindCodereviewSettingsFile(filename='codereview.settings'): |
| 685 """Finds the given file starting in the cwd and going up. | 680 """Finds the given file starting in the cwd and going up. |
| 686 | 681 |
| 687 Only looks up to the top of the repository unless an | 682 Only looks up to the top of the repository unless an |
| 688 'inherit-review-settings-ok' file exists in the root of the repository. | 683 'inherit-review-settings-ok' file exists in the root of the repository. |
| (...skipping 680 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1369 command = '<command>' | 1364 command = '<command>' |
| 1370 else: | 1365 else: |
| 1371 # OptParser.description prefer nicely non-formatted strings. | 1366 # OptParser.description prefer nicely non-formatted strings. |
| 1372 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__) | 1367 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__) |
| 1373 parser.set_usage('usage: %%prog %s [options] %s' % (command, more)) | 1368 parser.set_usage('usage: %%prog %s [options] %s' % (command, more)) |
| 1374 | 1369 |
| 1375 | 1370 |
| 1376 def main(argv): | 1371 def main(argv): |
| 1377 """Doesn't parse the arguments here, just find the right subcommand to | 1372 """Doesn't parse the arguments here, just find the right subcommand to |
| 1378 execute.""" | 1373 execute.""" |
| 1374 # Reload settings. | |
| 1375 global settings | |
| 1376 settings = Settings() | |
| 1377 | |
| 1379 # Do it late so all commands are listed. | 1378 # Do it late so all commands are listed. |
| 1380 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([ | 1379 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([ |
| 1381 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip()) | 1380 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip()) |
| 1382 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')])) | 1381 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')])) |
| 1383 | 1382 |
| 1384 # Create the option parse and add --verbose support. | 1383 # Create the option parse and add --verbose support. |
| 1385 parser = optparse.OptionParser() | 1384 parser = optparse.OptionParser() |
| 1386 parser.add_option( | 1385 parser.add_option( |
| 1387 '-v', '--verbose', action='count', default=0, | 1386 '-v', '--verbose', action='count', default=0, |
| 1388 help='Use 2 times for more debugging info') | 1387 help='Use 2 times for more debugging info') |
| (...skipping 24 matching lines...) Expand all Loading... | |
| 1413 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) | 1412 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e))) |
| 1414 | 1413 |
| 1415 # Not a known command. Default to help. | 1414 # Not a known command. Default to help. |
| 1416 GenUsage(parser, 'help') | 1415 GenUsage(parser, 'help') |
| 1417 return CMDhelp(parser, argv) | 1416 return CMDhelp(parser, argv) |
| 1418 | 1417 |
| 1419 | 1418 |
| 1420 if __name__ == '__main__': | 1419 if __name__ == '__main__': |
| 1421 fix_encoding.fix_encoding() | 1420 fix_encoding.fix_encoding() |
| 1422 sys.exit(main(sys.argv[1:])) | 1421 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |