blob: 5f5b9efc5b5bac96661285e98180e74d59205cf6 [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')
David Pursehouse01989782016-02-01 10:38:12 +090075 parser.add_option('--branch', dest='branches', metavar='BRANCH_NAME',
76 default=[], action='append',
77 help='Abandon changes only on the given branch')
David Pursehouse522e4f82014-04-09 15:05:16 +090078 parser.add_option('--exclude-branch', dest='exclude_branches',
79 metavar='BRANCH_NAME',
80 default=[],
81 action='append',
82 help='Do not abandon changes on given branch')
David Pursehouse01989782016-02-01 10:38:12 +090083 parser.add_option('--project', dest='projects', metavar='PROJECT_NAME',
84 default=[], action='append',
85 help='Abandon changes only on the given project')
David Pursehouse522e4f82014-04-09 15:05:16 +090086 parser.add_option('--exclude-project', dest='exclude_projects',
87 metavar='PROJECT_NAME',
88 default=[],
89 action='append',
90 help='Do not abandon changes on given project')
91 parser.add_option('--owner', dest='owner',
92 metavar='USERNAME',
93 default=None,
94 action='store',
95 help='Only abandon changes owned by the given user')
96 parser.add_option('-v', '--verbose', dest='verbose',
97 action='store_true',
98 help='enable verbose (debug) logging')
99
100 (options, _args) = parser.parse_args()
101
102 level = logging.DEBUG if options.verbose else logging.INFO
103 logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
104 level=level)
105
106 if not options.gerrit_url:
107 logging.error("Gerrit URL is required")
108 return 1
109
David Pursehouse25bebb12015-07-06 12:12:28 +0900110 pattern = re.compile(r"^([\d]+)(month[s]?|year[s]?|week[s]?)")
David Pursehouse522e4f82014-04-09 15:05:16 +0900111 match = pattern.match(options.age)
112 if not match:
113 logging.error("Invalid age: %s", options.age)
114 return 1
115 message = "Abandoning after %s %s or more of inactivity." % \
116 (match.group(1), match.group(2))
117
118 if options.basic_auth:
119 auth_type = HTTPBasicAuthFromNetrc
120 else:
121 auth_type = HTTPDigestAuthFromNetrc
122
123 try:
124 auth = auth_type(url=options.gerrit_url)
125 gerrit = GerritRestAPI(url=options.gerrit_url, auth=auth)
126 except Exception as e:
127 logging.error(e)
128 return 1
129
130 logging.info(message)
131 try:
132 stale_changes = []
133 offset = 0
134 step = 500
David Pursehouse01989782016-02-01 10:38:12 +0900135 query_terms = ["status:new", "age:%s" % options.age]
136 if options.branches:
137 query_terms += ["branch:%s" % b for b in options.branches]
David Pursehousea55ed3d2016-02-15 16:45:53 +0900138 elif options.exclude_branches:
David Pursehouse01989782016-02-01 10:38:12 +0900139 query_terms += ["-branch:%s" % b for b in options.exclude_branches]
140 if options.projects:
141 query_terms += ["project:%s" % p for p in options.projects]
142 elif options.exclude_projects:
143 query_terms = ["-project:%s" % p for p in options.exclude_projects]
David Pursehouse522e4f82014-04-09 15:05:16 +0900144 if options.owner:
145 query_terms += ["owner:%s" % options.owner]
146 query = "%20".join(query_terms)
147 while True:
148 q = query + "&n=%d&S=%d" % (step, offset)
149 logging.debug("Query: %s", q)
150 url = "/changes/?q=" + q
151 result = gerrit.get(url)
152 logging.debug("%d changes", len(result))
153 if not result:
154 break
155 stale_changes += result
156 last = result[-1]
157 if "_more_changes" in last:
158 logging.debug("More...")
159 offset += step
160 else:
161 break
162 except Exception as e:
163 logging.error(e)
164 return 1
165
166 abandoned = 0
167 errors = 0
168 abandon_message = message
169 if options.message:
170 abandon_message += "\n\n" + options.message
171 for change in stale_changes:
172 number = change["_number"]
173 try:
174 owner = change["owner"]["name"]
175 except:
176 owner = "Unknown"
177 subject = change["subject"]
178 if len(subject) > 70:
179 subject = subject[:65] + " [...]"
180 change_id = change["id"]
181 logging.info("%s (%s): %s", number, owner, subject)
182 if options.dry_run:
183 continue
184
185 try:
186 gerrit.post("/changes/" + change_id + "/abandon",
187 data='{"message" : "%s"}' % abandon_message)
188 abandoned += 1
189 except Exception as e:
190 errors += 1
191 logging.error(e)
192 logging.info("Total %d stale open changes", len(stale_changes))
193 if not options.dry_run:
194 logging.info("Abandoned %d changes. %d errors.", abandoned, errors)
195
196if __name__ == "__main__":
197 sys.exit(_main())