diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py new file mode 100755 index 0000000000..f1183463c0 --- /dev/null +++ b/contrib/abandon_stale.py @@ -0,0 +1,185 @@ +#!/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]+)(months|years)") + 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())