Script to abandon stale changes from the review server
Fetches a list of open changes that have not been updated since a given age (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 years: ./abandon_stale --gerrit-url http://review.example.com/ --age 3years Supports dry-run mode to only list the stale changes but not actually abandon them. Requires pygerrit (https://github.com/sonyxperiadev/pygerrit). Change-Id: Ie0edb54847f9f2ab8204647e17e3893ed0a057ea
This commit is contained in:
parent
ee55115bed
commit
522e4f8bd4
185
contrib/abandon_stale.py
Executable file
185
contrib/abandon_stale.py
Executable file
@ -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())
|
Loading…
Reference in New Issue
Block a user