186 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			186 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python
 | |
| # -*- coding: utf-8 -*-
 | |
| 
 | |
| # The MIT License
 | |
| #
 | |
| # Copyright 2014 Sony Mobile Communications. All rights reserved.
 | |
| #
 | |
| # Permission is hereby granted, free of charge, to any person obtaining a copy
 | |
| # of this software and associated documentation files (the "Software"), to deal
 | |
| # in the Software without restriction, including without limitation the rights
 | |
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | |
| # copies of the Software, and to permit persons to whom the Software is
 | |
| # furnished to do so, subject to the following conditions:
 | |
| #
 | |
| # The above copyright notice and this permission notice shall be included in
 | |
| # all copies or substantial portions of the Software.
 | |
| #
 | |
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | |
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | |
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | |
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | |
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | |
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | |
| # THE SOFTWARE.
 | |
| 
 | |
| """ Script to abandon stale changes from the review server.
 | |
| 
 | |
| Fetches a list of open changes that have not been updated since a
 | |
| given age in months or years (default 6 months), and then abandons them.
 | |
| 
 | |
| Assumes that the user's credentials are in the .netrc file.  Supports
 | |
| either basic or digest authentication.
 | |
| 
 | |
| Example to abandon changes that have not been updated for 3 months:
 | |
| 
 | |
|   ./abandon_stale --gerrit-url http://review.example.com/ --age 3months
 | |
| 
 | |
| Supports dry-run mode to only list the stale changes but not actually
 | |
| abandon them.
 | |
| 
 | |
| Requires pygerrit (https://github.com/sonyxperiadev/pygerrit).
 | |
| 
 | |
| """
 | |
| 
 | |
| import logging
 | |
| import optparse
 | |
| import re
 | |
| import sys
 | |
| 
 | |
| from pygerrit.rest import GerritRestAPI
 | |
| from pygerrit.rest.auth import HTTPBasicAuthFromNetrc, HTTPDigestAuthFromNetrc
 | |
| 
 | |
| 
 | |
| def _main():
 | |
|     parser = optparse.OptionParser()
 | |
|     parser.add_option('-g', '--gerrit-url', dest='gerrit_url',
 | |
|                       metavar='URL',
 | |
|                       default=None,
 | |
|                       help='gerrit server URL')
 | |
|     parser.add_option('-b', '--basic-auth', dest='basic_auth',
 | |
|                       action='store_true',
 | |
|                       help='use HTTP basic authentication instead of digest')
 | |
|     parser.add_option('-n', '--dry-run', dest='dry_run',
 | |
|                       action='store_true',
 | |
|                       help='enable dry-run mode: show stale changes but do '
 | |
|                            'not abandon them')
 | |
|     parser.add_option('-a', '--age', dest='age',
 | |
|                       metavar='AGE',
 | |
|                       default="6months",
 | |
|                       help='age of change since last update '
 | |
|                            '(default: %default)')
 | |
|     parser.add_option('-m', '--message', dest='message',
 | |
|                       metavar='STRING', default=None,
 | |
|                       help='Custom message to append to abandon message')
 | |
|     parser.add_option('--exclude-branch', dest='exclude_branches',
 | |
|                       metavar='BRANCH_NAME',
 | |
|                       default=[],
 | |
|                       action='append',
 | |
|                       help='Do not abandon changes on given branch')
 | |
|     parser.add_option('--exclude-project', dest='exclude_projects',
 | |
|                       metavar='PROJECT_NAME',
 | |
|                       default=[],
 | |
|                       action='append',
 | |
|                       help='Do not abandon changes on given project')
 | |
|     parser.add_option('--owner', dest='owner',
 | |
|                       metavar='USERNAME',
 | |
|                       default=None,
 | |
|                       action='store',
 | |
|                       help='Only abandon changes owned by the given user')
 | |
|     parser.add_option('-v', '--verbose', dest='verbose',
 | |
|                       action='store_true',
 | |
|                       help='enable verbose (debug) logging')
 | |
| 
 | |
|     (options, _args) = parser.parse_args()
 | |
| 
 | |
|     level = logging.DEBUG if options.verbose else logging.INFO
 | |
|     logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
 | |
|                         level=level)
 | |
| 
 | |
|     if not options.gerrit_url:
 | |
|         logging.error("Gerrit URL is required")
 | |
|         return 1
 | |
| 
 | |
|     pattern = re.compile(r"^([\d]+)(month[s]?|year[s]?|week[s]?)")
 | |
|     match = pattern.match(options.age)
 | |
|     if not match:
 | |
|         logging.error("Invalid age: %s", options.age)
 | |
|         return 1
 | |
|     message = "Abandoning after %s %s or more of inactivity." % \
 | |
|         (match.group(1), match.group(2))
 | |
| 
 | |
|     if options.basic_auth:
 | |
|         auth_type = HTTPBasicAuthFromNetrc
 | |
|     else:
 | |
|         auth_type = HTTPDigestAuthFromNetrc
 | |
| 
 | |
|     try:
 | |
|         auth = auth_type(url=options.gerrit_url)
 | |
|         gerrit = GerritRestAPI(url=options.gerrit_url, auth=auth)
 | |
|     except Exception as e:
 | |
|         logging.error(e)
 | |
|         return 1
 | |
| 
 | |
|     logging.info(message)
 | |
|     try:
 | |
|         stale_changes = []
 | |
|         offset = 0
 | |
|         step = 500
 | |
|         query_terms = ["status:new", "age:%s" % options.age] + \
 | |
|                       ["-branch:%s" % b for b in options.exclude_branches] + \
 | |
|                       ["-project:%s" % p for p in options.exclude_projects]
 | |
|         if options.owner:
 | |
|             query_terms += ["owner:%s" % options.owner]
 | |
|         query = "%20".join(query_terms)
 | |
|         while True:
 | |
|             q = query + "&n=%d&S=%d" % (step, offset)
 | |
|             logging.debug("Query: %s", q)
 | |
|             url = "/changes/?q=" + q
 | |
|             result = gerrit.get(url)
 | |
|             logging.debug("%d changes", len(result))
 | |
|             if not result:
 | |
|                 break
 | |
|             stale_changes += result
 | |
|             last = result[-1]
 | |
|             if "_more_changes" in last:
 | |
|                 logging.debug("More...")
 | |
|                 offset += step
 | |
|             else:
 | |
|                 break
 | |
|     except Exception as e:
 | |
|         logging.error(e)
 | |
|         return 1
 | |
| 
 | |
|     abandoned = 0
 | |
|     errors = 0
 | |
|     abandon_message = message
 | |
|     if options.message:
 | |
|         abandon_message += "\n\n" + options.message
 | |
|     for change in stale_changes:
 | |
|         number = change["_number"]
 | |
|         try:
 | |
|             owner = change["owner"]["name"]
 | |
|         except:
 | |
|             owner = "Unknown"
 | |
|         subject = change["subject"]
 | |
|         if len(subject) > 70:
 | |
|             subject = subject[:65] + " [...]"
 | |
|         change_id = change["id"]
 | |
|         logging.info("%s (%s): %s", number, owner, subject)
 | |
|         if options.dry_run:
 | |
|             continue
 | |
| 
 | |
|         try:
 | |
|             gerrit.post("/changes/" + change_id + "/abandon",
 | |
|                         data='{"message" : "%s"}' % abandon_message)
 | |
|             abandoned += 1
 | |
|         except Exception as e:
 | |
|             errors += 1
 | |
|             logging.error(e)
 | |
|     logging.info("Total %d stale open changes", len(stale_changes))
 | |
|     if not options.dry_run:
 | |
|         logging.info("Abandoned %d changes. %d errors.", abandoned, errors)
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     sys.exit(_main())
 | 
