| #!/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()) |