blob: 44e7e0e608e76a2f4f454f7ba180c4697edd6f67 [file] [log] [blame]
#!/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 pygerrit2 (https://github.com/dpursehouse/pygerrit2).
"""
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('-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('--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
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
query_terms = ["status:new", "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:
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())