blob: 32edf84f611b19c3e7a6afc8a3d7ee5fc5e04ddd [file] [log] [blame]
David Pursehouse522e4f82014-04-09 15:05:16 +09001#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# The MIT License
5#
6# Copyright 2014 Sony Mobile Communications. All rights reserved.
7#
8# Permission is hereby granted, free of charge, to any person obtaining a copy
9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24# THE SOFTWARE.
25
26""" Script to abandon stale changes from the review server.
27
28Fetches a list of open changes that have not been updated since a
29given age in months or years (default 6 months), and then abandons them.
30
31Assumes that the user's credentials are in the .netrc file. Supports
32either basic or digest authentication.
33
34Example to abandon changes that have not been updated for 3 months:
35
36 ./abandon_stale --gerrit-url http://review.example.com/ --age 3months
37
38Supports dry-run mode to only list the stale changes but not actually
39abandon them.
40
41Requires pygerrit (https://github.com/sonyxperiadev/pygerrit).
42
43"""
44
45import logging
46import optparse
47import re
48import sys
49
50from pygerrit.rest import GerritRestAPI
51from pygerrit.rest.auth import HTTPBasicAuthFromNetrc, HTTPDigestAuthFromNetrc
52
53
54def _main():
55 parser = optparse.OptionParser()
56 parser.add_option('-g', '--gerrit-url', dest='gerrit_url',
57 metavar='URL',
58 default=None,
59 help='gerrit server URL')
60 parser.add_option('-b', '--basic-auth', dest='basic_auth',
61 action='store_true',
62 help='use HTTP basic authentication instead of digest')
63 parser.add_option('-n', '--dry-run', dest='dry_run',
64 action='store_true',
65 help='enable dry-run mode: show stale changes but do '
66 'not abandon them')
67 parser.add_option('-a', '--age', dest='age',
68 metavar='AGE',
69 default="6months",
70 help='age of change since last update '
71 '(default: %default)')
72 parser.add_option('-m', '--message', dest='message',
73 metavar='STRING', default=None,
74 help='Custom message to append to abandon message')
75 parser.add_option('--exclude-branch', dest='exclude_branches',
76 metavar='BRANCH_NAME',
77 default=[],
78 action='append',
79 help='Do not abandon changes on given branch')
80 parser.add_option('--exclude-project', dest='exclude_projects',
81 metavar='PROJECT_NAME',
82 default=[],
83 action='append',
84 help='Do not abandon changes on given project')
85 parser.add_option('--owner', dest='owner',
86 metavar='USERNAME',
87 default=None,
88 action='store',
89 help='Only abandon changes owned by the given user')
90 parser.add_option('-v', '--verbose', dest='verbose',
91 action='store_true',
92 help='enable verbose (debug) logging')
93
94 (options, _args) = parser.parse_args()
95
96 level = logging.DEBUG if options.verbose else logging.INFO
97 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
98 level=level)
99
100 if not options.gerrit_url:
101 logging.error("Gerrit URL is required")
102 return 1
103
David Pursehouse25bebb12015-07-06 12:12:28 +0900104 pattern = re.compile(r"^([\d]+)(month[s]?|year[s]?|week[s]?)")
David Pursehouse522e4f82014-04-09 15:05:16 +0900105 match = pattern.match(options.age)
106 if not match:
107 logging.error("Invalid age: %s", options.age)
108 return 1
109 message = "Abandoning after %s %s or more of inactivity." % \
110 (match.group(1), match.group(2))
111
112 if options.basic_auth:
113 auth_type = HTTPBasicAuthFromNetrc
114 else:
115 auth_type = HTTPDigestAuthFromNetrc
116
117 try:
118 auth = auth_type(url=options.gerrit_url)
119 gerrit = GerritRestAPI(url=options.gerrit_url, auth=auth)
120 except Exception as e:
121 logging.error(e)
122 return 1
123
124 logging.info(message)
125 try:
126 stale_changes = []
127 offset = 0
128 step = 500
129 query_terms = ["status:new", "age:%s" % options.age] + \
130 ["-branch:%s" % b for b in options.exclude_branches] + \
131 ["-project:%s" % p for p in options.exclude_projects]
132 if options.owner:
133 query_terms += ["owner:%s" % options.owner]
134 query = "%20".join(query_terms)
135 while True:
136 q = query + "&n=%d&S=%d" % (step, offset)
137 logging.debug("Query: %s", q)
138 url = "/changes/?q=" + q
139 result = gerrit.get(url)
140 logging.debug("%d changes", len(result))
141 if not result:
142 break
143 stale_changes += result
144 last = result[-1]
145 if "_more_changes" in last:
146 logging.debug("More...")
147 offset += step
148 else:
149 break
150 except Exception as e:
151 logging.error(e)
152 return 1
153
154 abandoned = 0
155 errors = 0
156 abandon_message = message
157 if options.message:
158 abandon_message += "\n\n" + options.message
159 for change in stale_changes:
160 number = change["_number"]
161 try:
162 owner = change["owner"]["name"]
163 except:
164 owner = "Unknown"
165 subject = change["subject"]
166 if len(subject) > 70:
167 subject = subject[:65] + " [...]"
168 change_id = change["id"]
169 logging.info("%s (%s): %s", number, owner, subject)
170 if options.dry_run:
171 continue
172
173 try:
174 gerrit.post("/changes/" + change_id + "/abandon",
175 data='{"message" : "%s"}' % abandon_message)
176 abandoned += 1
177 except Exception as e:
178 errors += 1
179 logging.error(e)
180 logging.info("Total %d stale open changes", len(stale_changes))
181 if not options.dry_run:
182 logging.info("Abandoned %d changes. %d errors.", abandoned, errors)
183
184if __name__ == "__main__":
185 sys.exit(_main())