Including the "-is:wip" term by default causes the script to fail when run against Gerrit server earlier than 2.15. Add a new --exclude-wip option to optionally add "-is:wip". Change-Id: Ia26ce274d9250a076fa9072af2b8260c7a8087f5
		
			
				
	
	
		
			226 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			226 lines
		
	
	
		
			8.7 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
 | 
						|
days, months or years (default 6 months), and then abandons them.
 | 
						|
 | 
						|
Requires the user's credentials for the Gerrit server to be declared 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.
 | 
						|
 | 
						|
See the --help output for more information about options.
 | 
						|
 | 
						|
Requires pygerrit2 (https://github.com/dpursehouse/pygerrit2) to be installed
 | 
						|
and available for import.
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
import logging
 | 
						|
import optparse
 | 
						|
import re
 | 
						|
import sys
 | 
						|
 | 
						|
from pygerrit2.rest import GerritRestAPI
 | 
						|
from pygerrit2.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='(deprecated) use HTTP basic authentication instead'
 | 
						|
                      ' of digest')
 | 
						|
    parser.add_option('-d', '--digest-auth', dest='digest_auth',
 | 
						|
                      action='store_true',
 | 
						|
                      help='use HTTP digest authentication instead of basic')
 | 
						|
    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('-t', '--test', dest='testmode', action='store_true',
 | 
						|
                      help='test mode: query changes with the `test-abandon` '
 | 
						|
                           'topic and ignore age option')
 | 
						|
    parser.add_option('-a', '--age', dest='age',
 | 
						|
                      metavar='AGE',
 | 
						|
                      default="6months",
 | 
						|
                      help='age of change since last update in days, months'
 | 
						|
                           ' or years (default: %default)')
 | 
						|
    parser.add_option('-m', '--message', dest='message',
 | 
						|
                      metavar='STRING', default=None,
 | 
						|
                      help='custom message to append to abandon message')
 | 
						|
    parser.add_option('--branch', dest='branches', metavar='BRANCH_NAME',
 | 
						|
                      default=[], action='append',
 | 
						|
                      help='abandon changes only on the given branch')
 | 
						|
    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('--project', dest='projects', metavar='PROJECT_NAME',
 | 
						|
                      default=[], action='append',
 | 
						|
                      help='abandon changes only on the given project')
 | 
						|
    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('--exclude-wip', dest='exclude_wip',
 | 
						|
                      action='store_true',
 | 
						|
                      help='Exclude changes that are Work-in-Progress')
 | 
						|
    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
 | 
						|
 | 
						|
    if options.testmode:
 | 
						|
        message = "Abandoning in test mode"
 | 
						|
    else:
 | 
						|
        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.digest_auth:
 | 
						|
        auth_type = HTTPDigestAuthFromNetrc
 | 
						|
    else:
 | 
						|
        auth_type = HTTPBasicAuthFromNetrc
 | 
						|
 | 
						|
    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
 | 
						|
        if options.testmode:
 | 
						|
            query_terms = ["status:new", "owner:self", "topic:test-abandon"]
 | 
						|
        else:
 | 
						|
            query_terms = ["status:new", "age:%s" % options.age]
 | 
						|
        if options.exclude_wip:
 | 
						|
            query_terms += ["-is:wip"]
 | 
						|
        if options.branches:
 | 
						|
            query_terms += ["branch:%s" % b for b in options.branches]
 | 
						|
        elif options.exclude_branches:
 | 
						|
            query_terms += ["-branch:%s" % b for b in options.exclude_branches]
 | 
						|
        if options.projects:
 | 
						|
            query_terms += ["project:%s" % p for p in options.projects]
 | 
						|
        elif options.exclude_projects:
 | 
						|
            query_terms = ["-project:%s" % p for p in options.exclude_projects]
 | 
						|
        if options.owner and not options.testmode:
 | 
						|
            query_terms += ["owner:%s" % options.owner]
 | 
						|
        query = "%20".join(query_terms)
 | 
						|
        while True:
 | 
						|
            q = query + "&o=DETAILED_ACCOUNTS&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"]
 | 
						|
        project = ""
 | 
						|
        if len(options.projects) != 1:
 | 
						|
            project = "%s: " % change["project"]
 | 
						|
        owner = ""
 | 
						|
        if options.verbose:
 | 
						|
            try:
 | 
						|
                o = change["owner"]["name"]
 | 
						|
            except KeyError:
 | 
						|
                o = "Unknown"
 | 
						|
            owner = " (%s)" % o
 | 
						|
        subject = change["subject"]
 | 
						|
        if len(subject) > 70:
 | 
						|
            subject = subject[:65] + " [...]"
 | 
						|
        change_id = change["id"]
 | 
						|
        logging.info("%s%s: %s%s", number, owner, project, subject)
 | 
						|
        if options.dry_run:
 | 
						|
            continue
 | 
						|
 | 
						|
        try:
 | 
						|
            gerrit.post("/changes/" + change_id + "/abandon",
 | 
						|
                        json={"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())
 |