| #!/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('-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", "-is:wip", "age:%s" % options.age] |
| 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()) |