poudriere-runner: update jails
[freebsd-maintainer-scripts/.git] / getpatch
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 # SPDX-License-Identifier: BSD-2-Clause
5 #
6 # Copyright (c) 2012 Sofian Brabez <sbz@FreeBSD.org>
7 # All rights reserved.
8 #
9 # Redistribution and use in source and binary forms, with or without
10 # modification, are permitted provided that the following conditions
11 # are met:
12 # 1. Redistributions of source code must retain the above copyright
13 #    notice, this list of conditions and the following disclaimer
14 # 2. Redistributions in binary form must reproduce the above copyright
15 #    notice, this list of conditions and the following disclaimer in the
16 #    documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
19 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
22 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
24 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
27 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
28 # SUCH DAMAGE.
29 #
30 # MAINTAINER=   sbz@FreeBSD.org
31
32 import argparse
33 import codecs
34 import os
35 import re
36 import ssl
37 import sys
38 if sys.version_info.major == 3:
39     import urllib.request as urllib2
40 else:
41     import urllib2
42
43 """
44 FreeBSD getpatch handles Gnats and Bugzilla patch attachments
45 """
46
47
48 def create_ssl_context(cafile):
49     if os.path.exists(cafile):
50         return ssl.create_default_context(cafile=cafile)
51     else:
52         return ssl._create_unverified_context()
53
54
55 class GetPatch(object):
56
57     def __init__(self, pr, category='ports'):
58         self.pr = pr
59         self.category = category
60         self.patchs = []
61         self.url = ""
62         self.patch = ""
63         self.output_stdout = False
64         self.default_locale = sys.getdefaultencoding()
65         self.ssl_context = create_ssl_context('/usr/local/etc/ssl/cert.pem')
66
67     def fetch(self, *largs, **kwargs):
68         raise NotImplementedError()
69
70     def write(self, filename, data):
71         if filename.endswith(('.patch', '.txt')):
72             filename = "{}.diff".format(filename[:filename.rindex('.')])
73         f = codecs.open(filename, encoding=self.default_locale, mode='w')
74         f.write(data.decode(self.default_locale))
75         f.close()
76         self.out("[+] {} created".format(filename))
77
78     def get(self, only_last=False, output_stdout=False):
79         self.output_stdout = output_stdout
80         self.fetch(self.pr, category=self.category)
81
82         if len(self.patchs) == 0:
83             self.out("[-] No patch found")
84             sys.exit(os.EX_UNAVAILABLE)
85
86         if only_last:
87             self.patchs = [self.patchs.pop()]
88
89         for patch in self.patchs:
90             url = patch['url']
91             p = patch['name']
92
93             data = urllib2.urlopen(url, context=self.ssl_context).read()
94
95             if self.output_stdout:
96                 sys.stdout.write(data.decode(self.default_locale))
97             else:
98                 self.write(p, data)
99
100     def add_patch(self, url, name):
101         self.patchs.append({'url': url, 'name': name})
102
103     def out(self, s):
104         if not self.output_stdout:
105             print(s)
106
107
108 class GnatsGetPatch(GetPatch):
109
110     URL_BASE = 'https://www.freebsd.org/cgi'
111     URL = '{}/query-pr.cgi?pr='.format(URL_BASE)
112     REGEX = r'<b>Download <a href="([^"]*)">([^<]*)</a>'
113
114     def __init__(self, pr, category):
115         GetPatch.__init__(self, pr, category)
116
117     def fetch(self, *largs, **kwargs):
118         category = kwargs['category']
119         target = ("{}/{}".format(category, self.pr),
120                   "{}".format(self.pr))[category == '']
121         self.out("[+] Fetching patch for pr {}".format(target))
122         pattern = re.compile(self.REGEX)
123         u = urllib2.urlopen("{}{}".format(self.URL, target),
124                             context=self.ssl_context)
125         data = u.read()
126         if data is None:
127             self.out("[-] No patch found")
128             sys.exit(os.EX_UNAVAILABLE)
129
130         for patchs in re.findall(pattern, str(data)):
131             self.add_patch(patchs[0], patchs[1])
132
133
134 class BzGetPatch(GetPatch):
135
136     URL_BASE = 'https://bugs.freebsd.org/bugzilla/'
137     URL_SHOW = '{}/show_bug.cgi?id='.format(URL_BASE)
138     REGEX_ATTACHMENTS_TABLE = r'<table id="attachment_table">(.*?)</table>'
139     REGEX_ATTACHMENT_TR = r'(<tr id="a\d+"[^<]+>.*?</tr>)'
140     REGEX_URL = r'<a href="([^<]+)">Details</a>'
141     REGEX = r'<div class="details">([^ ]+) \(text/plain(?:; charset=[-\w]+)?\)'
142
143     def __init__(self, pr, category):
144         GetPatch.__init__(self, pr, category)
145
146     def _get_patch_name(self, url):
147         data = urllib2.urlopen(url, context=self.ssl_context).read()
148         match = re.search(self.REGEX, str(data))
149         if match is None:
150             return None
151         return match.group(1)
152
153     def _get_patch_url(self, data):
154         for url in re.findall(self.REGEX_URL, str(data)):
155             url = '{}{}'.format(self.URL_BASE, url)
156             file_name = self._get_patch_name(url)
157             if file_name is None:
158                 msg = "[-] Could not determine the patch file name in {}." \
159                     "Skipping."
160                 self.out(msg.format(url))
161                 continue
162             download_url = url[:url.find('&')]
163             return download_url, file_name
164
165     def _get_patch_urls(self, data):
166         patch_urls = {}
167         match = re.search(self.REGEX_ATTACHMENTS_TABLE, str(data), re.DOTALL)
168         if match is None:
169             return patch_urls
170         table = match.group(1)
171         for tr in re.findall(self.REGEX_ATTACHMENT_TR, str(data), re.DOTALL):
172             if (tr.find('bz_tr_obsolete') >= 0):
173                 continue
174             download_url, file_name = self._get_patch_url(tr)
175             patch_urls[download_url] = file_name
176
177         return patch_urls
178
179     def fetch(self, *largs, **kwargs):
180         category = kwargs['category']
181         target = ("{}/{}".format(category, self.pr),
182                   "{}".format(self.pr))[category == '']
183         self.out("[+] Fetching patch for pr {}".format(target))
184         u = urllib2.urlopen("{}{}".format(self.URL_SHOW, self.pr),
185                             context=self.ssl_context)
186         data = u.read()
187
188         if data is None:
189             self.out("[-] No patch found")
190             sys.exit(os.EX_UNAVAILABLE)
191
192         patch_urls = self._get_patch_urls(data)
193         if not patch_urls:
194             self.out("[-] No patch found")
195             sys.exit(os.EX_UNAVAILABLE)
196
197         for url, file_name in patch_urls.items():
198             self.add_patch(url, file_name)
199
200
201 def main():
202
203     parser = argparse.ArgumentParser(
204             description='Gets patch attachments from a Bug Tracking System'
205     )
206     parser.add_argument('pr', metavar='pr', type=str, nargs=1,
207                         help='Pr id number')
208     parser.add_argument('--mode', type=str, choices=['gnats', 'bz'],
209                         default='bz', help='available modes to retrieve patch')
210     parser.add_argument('--last', action='store_true',
211                         help='only retrieve the latest iteration of a patch')
212     parser.add_argument('--stdout', action='store_true',
213                         help='dump patch on stdout')
214
215     if len(sys.argv) == 1:
216         parser.print_help()
217         sys.exit(os.EX_USAGE)
218
219     args = parser.parse_args()
220
221     category = ""
222     pr = str(args.pr[0])
223
224     if pr and '/' in pr:
225         category, pr = pr.split('/')
226
227     Clazz = globals()['%sGetPatch' % args.mode.capitalize()]
228     gp = Clazz(pr, category)
229     gp.get(only_last=args.last, output_stdout=args.stdout)
230
231     return os.EX_OK
232
233 if __name__ == '__main__':
234     sys.exit(main())