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