Merge ~cjwatson/launchpadlib:tidy-contrib into launchpadlib:main

Proposed by Colin Watson
Status: Merged
Merged at revision: f7fc211d5fa3ff39ba11f3dcdb9346c12c4ca971
Proposed branch: ~cjwatson/launchpadlib:tidy-contrib
Merge into: launchpadlib:main
Diff against target: 1124 lines (+1/-8)
2 files modified
NEWS.rst (+1/-0)
dev/null (+0/-8)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+410882@code.launchpad.net

Commit message

Remove some obsolete scripts from contrib/

Description of the change

`editmoin` is a separate project that has nothing to do with launchpadlib.

`close_bugs_from_commits` and `update-milestone-progress` assumed that Launchpad's source is managed in bzr, which hasn't been true since 2019, and are no longer useful enough to justify maintaining them.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/NEWS.rst b/NEWS.rst
2index f9be974..4f3f24f 100644
3--- a/NEWS.rst
4+++ b/NEWS.rst
5@@ -5,6 +5,7 @@ NEWS for launchpadlib
6 1.10.16
7 =======
8 - Add ``pre-commit`` configuration.
9+- Remove some obsolete scripts from ``contrib/``.
10
11 1.10.15.1 (2021-10-27)
12 ======================
13diff --git a/contrib/close_bugs_from_commits.conf.sample b/contrib/close_bugs_from_commits.conf.sample
14deleted file mode 100644
15index 2fca4ae..0000000
16--- a/contrib/close_bugs_from_commits.conf.sample
17+++ /dev/null
18@@ -1,18 +0,0 @@
19-[Project]
20-# A comma separated list of Launchpad project names to close bugs for.
21-# Only bugs targeted to these projects are closed.
22-name = malone,launchpad
23-
24-[Branch States]
25-# Which revnos were last looked through for each branch? The next time
26-# the script run, it will start looking at revno+1.
27-db = 7753
28-devel = 7845
29-
30-[Branch Locations]
31-# Which branches should be looked through for bug fixes? The left-hand
32-# column is the branch name, and will be used in the comment saying that a
33-# bug was fixed (e.g. "Fixed in db r7000.").
34-db = bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/db-devel
35-devel = bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel
36-
37diff --git a/contrib/close_bugs_from_commits.py b/contrib/close_bugs_from_commits.py
38deleted file mode 100755
39index cd92d95..0000000
40--- a/contrib/close_bugs_from_commits.py
41+++ /dev/null
42@@ -1,119 +0,0 @@
43-#!/usr/bin/env python
44-
45-# Copyright (C) 2009 Canonical Ltd.
46-#
47-# This file is part of launchpadlib.
48-#
49-# launchpadlib is free software: you can redistribute it and/or modify it
50-# under the terms of the GNU Lesser General Public License as published by the
51-# Free Software Foundation, version 3 of the License.
52-#
53-# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
54-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
55-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
56-# for more details.
57-#
58-# You should have received a copy of the GNU Lesser General Public License
59-# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
60-
61-from __future__ import with_statement
62-
63-import re
64-import os
65-import sys
66-from ConfigParser import RawConfigParser
67-
68-from bzrlib.branch import Branch
69-
70-from launchpadlib.launchpad import Launchpad
71-
72-fixed_bug_freeform_re = re.compile(
73- r'\(fixes bugs{0,1} (\d+(?:,\s*\d+)*)\)', re.IGNORECASE)
74-fixed_bug_structured_re = re.compile(
75- r'\[bugs{0,1}[= ](\d+(?:,\s*\d+)*)\]', re.IGNORECASE)
76-
77-
78-def get_fixed_bug_ids(revision):
79- """Try to extract which bugs were fixed by this revision."""
80- fixed_bugs = set()
81- commit_message = ' '.join(revision.message.split())
82- match = fixed_bug_freeform_re.search(commit_message)
83- if match is None:
84- match = fixed_bug_structured_re.search(commit_message)
85- if match is not None:
86- fixed_bugs.update(int(bug_id) for bug_id in match.group(1).split(','))
87- # TODO: Search revision properties.
88- return fixed_bugs
89-
90-
91-def get_config(config_filepath):
92- config_parser = RawConfigParser()
93- config_parser.read([config_filepath])
94- branches = dict(
95- (name, dict(location=location))
96- for name, location in config_parser.items('Branch Locations'))
97- for name in branches:
98- branches[name]['last_revno'] = config_parser.getint(
99- 'Branch States', name)
100- projects = config_parser.get('Project', 'name').split(',')
101- return branches, projects
102-
103-
104-def set_last_revno(config_filepath, branch_name, revno):
105- config_parser = RawConfigParser()
106- config_parser.read([config_filepath])
107- config_parser.set('Branch States', branch_name, revno)
108- with open(config_filepath, 'w') as config_file:
109- config_parser.write(config_file)
110-
111-
112-def main():
113- launchpad = Launchpad.login_with(os.path.basename(sys.argv[0]),
114- 'production')
115-
116- branches, project_names = get_config('close_bugs_from_commits.conf')
117-
118- projects_links = []
119- for project_name in project_names:
120- projects_links.append(launchpad.projects[project_name].self_link)
121- for branch_name, branch_info in branches.items():
122- branch = Branch.open(branch_info['location'])
123- repository = branch.repository
124- start_revno = branch_info['last_revno'] + 1
125- for revno in range(start_revno, branch.revno()+1):
126- rev_id = branch.get_rev_id(revno)
127- revision = repository.get_revision(rev_id)
128- fixed_bugs = get_fixed_bug_ids(revision)
129- for fixed_bug in sorted(fixed_bugs):
130- try:
131- lp_bug = launchpad.bugs[int(fixed_bug)]
132- except KeyError:
133- # Invalid bug id specified, skip it.
134- continue
135- for bug_task in lp_bug.bug_tasks:
136- if bug_task.target.self_link in projects_links:
137- break
138- else:
139- # The bug wasn't targeted to our project.
140- continue
141- fixed_statuses = [u'Fix Committed', u'Fix Released']
142- if bug_task.status not in fixed_statuses:
143- print "Marking bug %s as fixed in r%s." % (
144- lp_bug.id, revno)
145- branch_location = branch_info['location'].replace(
146- 'bzr+ssh', 'http')
147- codebrowse_url = (
148- branch_location + '/revision/' + str(revno))
149- bug_task.transitionToStatus(status=u'Fix Committed')
150- bug_task.bug.newMessage(
151- subject=u'Bug fixed by a commit',
152- content=u'Fixed in %s r%s <%s>' % (
153- branch_name, revno, codebrowse_url))
154- set_last_revno(
155- 'close_bugs_from_commits.conf', branch_name, revno)
156- return 0
157-
158-
159-
160-if __name__ == '__main__':
161- sys.exit(main())
162diff --git a/contrib/editmoin.py b/contrib/editmoin.py
163deleted file mode 100755
164index 01b48e4..0000000
165--- a/contrib/editmoin.py
166+++ /dev/null
167@@ -1,423 +0,0 @@
168-#!/usr/bin/env python
169-
170-"""
171-Copyright (c) 2002-2006 Gustavo Niemeyer <gustavo@niemeyer.net>
172-
173-This program allows you to edit moin (see http://moin.sourceforge.net)
174-pages with your preferred editor. The default editor is vi. If you want
175-to use any other, just set the EDITOR environment variable.
176-
177-To define your moin id used when logging in in a specifc moin, edit a
178-file named ~/.moin_ids and include lines like "http://moin.url/etc myid".
179-
180-WARNING: This program expects information to be in a very specific
181- format. It will break if this format changes, so there are
182- no warranties of working at all. All I can say is that it
183- worked for me, at least once. ;-)
184-
185-Tested moin versions: 0.9, 0.11, 1.0, 1.1, 1.3.5, 1.5, 1.5.1, 1.5.4, 1.5.5,
186- 1.6, 1.6.1
187-"""
188-
189-__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
190-__version__ = "1.10.1"
191-__license__ = "GPL"
192-
193-import tempfile
194-import textwrap
195-import sys, os
196-import urllib
197-import shutil
198-import md5
199-import re
200-import subprocess
201-
202-
203-USAGE = "Usage: editmoin [-t <template page>] <moin page URL>\n"
204-
205-IDFILENAME = os.path.expanduser("~/.moin_ids")
206-ALIASFILENAME = os.path.expanduser("~/.moin_aliases")
207-
208-
209-BODYRE = re.compile('<textarea.*?name="savetext".*?>(.*)</textarea>',
210- re.M|re.DOTALL)
211-DATESTAMPRE = re.compile('<input.*?name="datestamp".*?value="(.*?)".*?>')
212-NOTIFYRE = re.compile('<input.*?name="notify".*?value="(.*?)".*?>')
213-COMMENTRE = re.compile('<input.*?name="comment".*>')
214-TRIVIALRE = re.compile('<input.*?name="trivial".*?value="(.*?)".*?>')
215-MESSAGERE1 = re.compile('^</table>(.*?)<a.*?>Clear message</a>',
216- re.M|re.DOTALL)
217-MESSAGERE2 = re.compile('<div class="message">(.*?)</div>', re.M|re.DOTALL)
218-MESSAGERE3 = re.compile('<div id="message">\s*<p>(.*?)</p>', re.M|re.DOTALL)
219-STATUSRE = re.compile('<p class="status">(.*?)</p>', re.M|re.DOTALL)
220-CANCELRE = re.compile('<input.*?type="submit" name="button_cancel" value="(.*?)">')
221-EDITORRE = re.compile('<input.*?type="hidden" name="editor" value="text">')
222-TICKETRE = re.compile('<input.*?type="hidden" name="ticket" value="(.*?)">')
223-REVRE = re.compile('<input.*?type="hidden" name="rev" value="(.*?)">')
224-CATEGORYRE = re.compile('<option value="(Category\w+?)">')
225-SELECTIONRE = re.compile("\(([^)]*)\)\s*([^(]*)")
226-EXTENDMSG = "Use the Preview button to extend the locking period."
227-
228-
229-marker = object()
230-
231-
232-class Error(Exception): pass
233-
234-
235-class MoinFile:
236-
237- multi_selection = ["notify", "trivial", "add_category"]
238-
239- def __init__(self, filename, id, has_moin_session):
240- self.filename = filename
241- self.id = id
242- self.has_moin_session = has_moin_session
243- self.data = open(filename).read()
244- self.body = self._get_data(BODYRE, "body")
245-
246- try:
247- self.datestamp = self._get_data(DATESTAMPRE, "datestamp")
248- except Error:
249- self.datestamp = None
250-
251- try:
252- self.notify = self._get_data(NOTIFYRE, "notify")
253- self.comment = "None"
254- except Error:
255- self.notify = None
256- if COMMENTRE.search(self.data):
257- self.comment = "None"
258- else:
259- self.comment = None
260-
261- try:
262- self.trivial = self._get_data(TRIVIALRE, "trivial")
263- except Error:
264- self.trivial = None
265-
266- self.categories = self._get_data_findall(CATEGORYRE, "category", [])
267- self.add_category = None
268-
269- match = STATUSRE.search(self.data)
270- if match:
271- self.status = strip_html(match.group(1))
272- else:
273- self.status = None
274-
275- self.rev = self._get_data(REVRE, "rev", None)
276- self.ticket = self._get_data(TICKETRE, "ticket", None)
277-
278- def _get_data(self, pattern, info, default=marker):
279- match = pattern.search(self.data)
280- if not match:
281- if default is not marker:
282- return default
283- message = get_message(self.data)
284- if message:
285- print message
286- raise Error, info+" information not found"
287- else:
288- return match.group(1)
289-
290- def _get_data_findall(self, pattern, info, default=marker):
291- groups = pattern.findall(self.data)
292- if not groups:
293- if default is not marker:
294- return default
295- raise Error, info+" information not found"
296- return groups
297-
298- def _get_selection(self, str):
299- for selected, option in SELECTIONRE.findall(str):
300- if selected.strip():
301- return option.strip()
302- return None
303-
304- def _unescape(self, data):
305- data = data.replace("&lt;", "<")
306- data = data.replace("&gt;", ">")
307- data = data.replace("&amp;", "&")
308- return data
309-
310- def has_cancel(self):
311- return (CANCELRE.search(self.data) is not None)
312-
313- def has_editor(self):
314- return (EDITORRE.search(self.data) is not None)
315-
316- def write_raw(self):
317- filename = tempfile.mktemp(".moin")
318- file = open(filename, "w")
319- if self.has_moin_session:
320- syntax_version = "1.6"
321- else:
322- syntax_version = "1.5"
323- file.write("@@ Syntax: %s\n" % syntax_version)
324- if not self.id:
325- file.write("@@ WARNING! You're NOT logged in!\n")
326- if self.status is not None:
327- text = self.status.replace(EXTENDMSG, "").strip()
328- lines = textwrap.wrap(text, 70,
329- initial_indent="@@ Message: ",
330- subsequent_indent="@ ")
331- for line in lines:
332- file.write(line+"\n")
333- if self.comment is not None:
334- file.write("@@ Comment: %s\n" % self.comment)
335- if self.trivial is not None:
336- file.write("@@ Trivial: ( ) Yes (x) No\n")
337- if self.notify is not None:
338- yes, no = (self.notify and ("x", " ") or (" ", "x"))
339- file.write("@@ Notify: (%s) Yes (%s) No\n" % (yes, no))
340- if self.categories:
341- file.write("@@ Add category: (x) None\n")
342- for category in self.categories:
343- file.write("@ ( ) %s\n" % category)
344- file.write(self._unescape(self.body))
345- file.close()
346- return filename
347-
348- def read_raw(self, filename):
349- file = open(filename)
350- lines = []
351- data = file.readline()
352- while data != "\n":
353- if data[0] != "@":
354- break
355- if len(data) < 2:
356- pass
357- elif data[1] == "@":
358- lines.append(data[2:].strip())
359- else:
360- if not lines:
361- lines.append("")
362- lines[-1] += " "
363- lines[-1] += data[2:].strip()
364- data = file.readline()
365- self.body = data+file.read()
366- file.close()
367- for line in lines:
368- sep = line.find(":")
369- if sep != -1:
370- attr = line[:sep].lower().replace(' ', '_')
371- value = line[sep+1:].strip()
372- if attr in self.multi_selection:
373- setattr(self, attr, self._get_selection(value))
374- else:
375- setattr(self, attr, value)
376-
377-def get_message(data):
378- match = MESSAGERE3.search(data)
379- if not match:
380- # Check for moin < 1.3.5 (not sure the precise version it changed).
381- match = MESSAGERE2.search(data)
382- if not match:
383- # Check for moin <= 0.9.
384- match = MESSAGERE1.search(data)
385- if match:
386- return strip_html(match.group(1))
387- return None
388-
389-def strip_html(data):
390- data = re.subn("\n", " ", data)[0]
391- data = re.subn("<p>|<br>", "\n", data)[0]
392- data = re.subn("<.*?>", "", data)[0]
393- data = re.subn("Clear data", "", data)[0]
394- data = re.subn("[ \t]+", " ", data)[0]
395- data = data.strip()
396- return data
397-
398-def get_id(moinurl):
399- if os.path.isfile(IDFILENAME):
400- file = open(IDFILENAME)
401- for line in file.readlines():
402- line = line.strip()
403- if line and line[0] != "#":
404- tokens = line.split()
405- if len(tokens) > 1:
406- url, id = tokens[:2]
407- else:
408- url, id = tokens[0], None
409- if moinurl.startswith(url):
410- return id
411- return None
412-
413-def translate_shortcut(moinurl):
414- if "://" in moinurl:
415- return moinurl
416- if "/" in moinurl:
417- shortcut, pathinfo = moinurl.split("/", 1)
418- else:
419- shortcut, pathinfo = moinurl, ""
420- if os.path.isfile(ALIASFILENAME):
421- file = open(ALIASFILENAME)
422- try:
423- for line in file.readlines():
424- line = line.strip()
425- if line and line[0] != "#":
426- alias, value = line.split(None, 1)
427- if pathinfo:
428- value = "%s/%s" % (value, pathinfo)
429- if shortcut == alias:
430- if "://" in value:
431- return value
432- if "/" in value:
433- shortcut, pathinfo = value.split("/", 1)
434- else:
435- shortcut, pathinfo = value, ""
436- finally:
437- file.close()
438- if os.path.isfile(IDFILENAME):
439- file = open(IDFILENAME)
440- try:
441- for line in file.readlines():
442- line = line.strip()
443- if line and line[0] != "#":
444- url = line.split()[0]
445- if shortcut in url:
446- if pathinfo:
447- return "%s/%s" % (url, pathinfo)
448- else:
449- return url
450- finally:
451- file.close()
452- raise Error, "no suitable url found for shortcut '%s'" % shortcut
453-
454-
455-def get_urlopener(moinurl, id=None):
456- urlopener = urllib.FancyURLopener()
457- proxy = os.environ.get("http_proxy")
458- if proxy:
459- urlopener.proxies.update({"http": proxy})
460- if id:
461- # moinmoin < 1.6
462- urlopener.addheader("Cookie", "MOIN_ID=\"%s\"" % id)
463- # moinmoin >= 1.6
464- urlopener.addheader("Cookie", "MOIN_SESSION=\"%s\"" % id)
465- return urlopener
466-
467-def fetchfile(urlopener, url, id, template):
468- geturl = url+"?action=edit"
469- if template:
470- geturl += "&template=" + urllib.quote(template)
471- filename, headers = urlopener.retrieve(geturl)
472- has_moin_session = "MOIN_SESSION" in headers.get("set-cookie", "")
473- return MoinFile(filename, id, has_moin_session)
474-
475-def editfile(moinfile):
476- edited = 0
477- filename = moinfile.write_raw()
478- editor = os.environ.get("EDITOR", "vi")
479- digest = md5.md5(open(filename).read()).digest()
480- subprocess.call([editor, filename])
481- if digest != md5.md5(open(filename).read()).digest():
482- shutil.copyfile(filename, os.path.expanduser("~/.moin_lastedit"))
483- edited = 1
484- moinfile.read_raw(filename)
485- os.unlink(filename)
486- return edited
487-
488-def sendfile(urlopener, url, moinfile):
489- if moinfile.comment is not None:
490- comment = "&comment="
491- if moinfile.comment.lower() != "none":
492- comment += urllib.quote(moinfile.comment)
493- else:
494- comment = ""
495- data = "button_save=1&savetext=%s%s" \
496- % (urllib.quote(moinfile.body), comment)
497- if moinfile.has_editor():
498- data += "&action=edit" # Moin >= 1.5
499- else:
500- data += "&action=savepage" # Moin < 1.5
501- if moinfile.datestamp:
502- data += "&datestamp=" + moinfile.datestamp
503- if moinfile.rev:
504- data += "&rev=" + moinfile.rev
505- if moinfile.ticket:
506- data += "&ticket=" + moinfile.ticket
507- if moinfile.notify == "Yes":
508- data += "&notify=1"
509- if moinfile.trivial == "Yes":
510- data += "&trivial=1"
511- if moinfile.add_category and moinfile.add_category != "None":
512- data += "&category=" + urllib.quote(moinfile.add_category)
513- url = urlopener.open(url, data)
514- answer = url.read()
515- url.close()
516- message = get_message(answer)
517- if message is None:
518- print answer
519- raise Error, "data submitted, but message information not found"
520- else:
521- print message
522-
523-def sendcancel(urlopener, url, moinfile):
524- if not moinfile.has_cancel():
525- return
526- data = "button_cancel=Cancel"
527- if moinfile.has_editor():
528- data += "&action=edit&savetext=dummy" # Moin >= 1.5
529- else:
530- data += "&action=savepage" # Moin < 1.5
531- if moinfile.datestamp:
532- data += "&datestamp=" + moinfile.datestamp
533- if moinfile.rev:
534- data += "&rev=" + moinfile.rev
535- if moinfile.ticket:
536- data += "&ticket=" + moinfile.ticket
537- url = urlopener.open(url, data)
538- answer = url.read()
539- url.close()
540- message = get_message(answer)
541- if not message:
542- print answer
543- raise Error, "cancel submitted, but message information not found"
544- else:
545- print message
546-
547-
548-def editshortcut(shortcut, template=None, editfile_func=editfile):
549- """Edit a Moin page at a shortcut path.
550-
551- By default the editfile() function is used to actually edit the
552- page, but a custom one can be passed in as the editfile_func
553- parameter.
554-
555- Return True if the page as edited, otherwise False.
556- """
557- url = translate_shortcut(shortcut)
558- id = get_id(url)
559- urlopener = get_urlopener(url, id)
560- moinfile = fetchfile(urlopener, url, id, template)
561- try:
562- page_edited = editfile_func(moinfile)
563- if page_edited:
564- sendfile(urlopener, url, moinfile)
565- else:
566- sendcancel(urlopener, url, moinfile)
567- finally:
568- os.unlink(moinfile.filename)
569- return page_edited
570-
571-
572-def main():
573- argv = sys.argv[1:]
574- template = None
575- if len(argv) > 2 and argv[0] == "-t":
576- template = argv[1]
577- argv = argv[2:]
578- if len(argv) != 1 or argv[0] in ("-h", "--help"):
579- sys.stderr.write(USAGE)
580- sys.exit(1)
581- try:
582- editshortcut(argv[0], template)
583- except (IOError, OSError, Error), e:
584- sys.stderr.write("error: %s\n" % str(e))
585- sys.exit(1)
586-
587-if __name__ == "__main__":
588- main()
589-
590-# vim:et:ts=4:sw=4
591diff --git a/contrib/test_close_bugs_from_commits.py b/contrib/test_close_bugs_from_commits.py
592deleted file mode 100644
593index 11daf2e..0000000
594--- a/contrib/test_close_bugs_from_commits.py
595+++ /dev/null
596@@ -1,100 +0,0 @@
597-# Copyright (C) 2009 Canonical Ltd.
598-#
599-# This file is part of launchpadlib.
600-#
601-# launchpadlib is free software: you can redistribute it and/or modify it
602-# under the terms of the GNU Lesser General Public License as published by the
603-# Free Software Foundation, version 3 of the License.
604-#
605-# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
606-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
607-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
608-# for more details.
609-#
610-# You should have received a copy of the GNU Lesser General Public License
611-# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
612-
613-"""Test for close_bugs_from_commits.get_fixed_bug_ids()."""
614-
615-import unittest
616-
617-from close_bugs_from_commits import get_fixed_bug_ids
618-
619-
620-class FakeRevision:
621-
622- def __init__(self, message):
623- self.message = message
624-
625-
626-class TestGetFixedBugIds(unittest.TestCase):
627-
628- def test_one_freeform(self):
629- # One bug specified in the format: (fixes bug 42)
630- self.assertEqual(
631- get_fixed_bug_ids(FakeRevision("before (fixes bug 42) after")),
632- set([42]))
633-
634- def test_multiple_freeform(self):
635- # Multiple bugs specified in the format: (fixes bugs 42, 84, 168)
636- self.assertEqual(
637- get_fixed_bug_ids(
638- FakeRevision("before (fixes bug 42, 84, 168) after")),
639- set([42, 84, 168]))
640- self.assertEqual(
641- get_fixed_bug_ids(
642- FakeRevision("before (fixes bug 42,84,168) after")),
643- set([42, 84, 168]))
644-
645- def test_freeform_case_insensitve(self):
646- # The freeform format isn't case sensitive.
647- self.assertEqual(
648- get_fixed_bug_ids(FakeRevision("before (FIXES BUG 42) after")),
649- set([42]))
650-
651- def test_one_structured(self):
652- # One bug specified in the format: [bug=42]
653- self.assertEqual(
654- get_fixed_bug_ids(FakeRevision("before [bug=42]) after")),
655- set([42]))
656-
657- def test_one_structured_no_equal_sign(self):
658- # One bug specified in the format: [bug 42]
659- self.assertEqual(
660- get_fixed_bug_ids(FakeRevision("before [bug 42]) after")),
661- set([42]))
662-
663- def test_multiple_structured(self):
664- # Multiple bugs specified in the format: [bug=42, 84, 168]
665- self.assertEqual(
666- get_fixed_bug_ids(
667- FakeRevision("before [bug=42, 84, 168]) after")),
668- set([42, 84, 168]))
669-
670- def test_multiple_structured_bugs(self):
671- # Multiple bugs specified in the format: [bugs=42, 84, 168]
672- self.assertEqual(
673- get_fixed_bug_ids(
674- FakeRevision("before [bugs=42, 84, 168]) after")),
675- set([42, 84, 168]))
676- self.assertEqual(
677- get_fixed_bug_ids(
678- FakeRevision("before [bugs=42,84,168]) after")),
679- set([42, 84, 168]))
680-
681- def test_mention_bug(self):
682- # Bugs simply mentioned among the commit message shouldn't be
683- # considered being fixed.
684- self.assertEqual(
685- get_fixed_bug_ids(
686- FakeRevision("find bug 4231 after")),
687- set([]))
688-
689- def test_mention_multiple_bugs(self):
690- # Bugs simply mentioned among the commit message shouldn't be
691- # considered being fixed.
692- self.assertEqual(
693- get_fixed_bug_ids(
694- FakeRevision("find bug 123456 after 2345 and also 1234, end.")),
695- set([]))
696-
697diff --git a/contrib/update-milestone-progress.conf.sample b/contrib/update-milestone-progress.conf.sample
698deleted file mode 100644
699index feaff5b..0000000
700--- a/contrib/update-milestone-progress.conf.sample
701+++ /dev/null
702@@ -1,43 +0,0 @@
703-[Branch]
704-# The location of the branch that should be scanned for commits. It's
705-# best to keep a mirror locally, and point to the local mirror. Going
706-# through each commit on a remote branch might take quite a while.
707-location=bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel
708-# The revision number where we should start scanning for commits. This
709-# should be the revision where release-critical is turned off.
710-start_revno=7677
711-
712-[Milestone]
713-# The name of the current milestone.
714-name=2.2.2
715-# The project where bugs should be search in.
716-project=launchpad-project
717-# The day the milestone starts.
718-start=2009-02-02
719-# The day the milestone ends.
720-end=2009-02-25
721-# The day PQM gets set to release-critical mode.
722-release-critical=2009-02-20
723-# The name of the template for the progress wiki page.
724-template=update-milestone.template.sample
725-# The base URL where the wiki page live. The page URL will be this URL
726-# plus the milestone name appended to it. This should be an editmoin
727-# shortcut URL.
728-base_wiki_location=dev.launchpad.net/VersionThreeDotO/Project/Progress
729-# The page where the story definitions are. The story titles on the
730-# progress page will link to this URL, together with an anchor, which is
731-# the story tag name with the 'story-' part removed.
732-stories_page=https://dev.launchpad.net/VersionThreeDotO/Project/StoryCards
733-
734-[Stories]
735-# The list of stories that are scheduled for this milestone.
736-# story_tag_name=story_name_for_display
737-story-hwdb-affected-users=HWDB Affected Users
738-story-hwdb-dmi-lspci=Include DMI and lspci information in HWDB submissions
739-story-hwdb-filter-by-device-and-system=Find out which computers have a certain device
740-story-link-to-upstream-filebug=Link to file bugs upstream
741-story-link-to-upstream-searchbugs=Link to search bugs upstream
742-story-set-remote-product-manually=Enable search and filebug links for a project manually
743-story-set-remote-product-automatically=Enable search and filebug links for a project automatically
744-story-inline-edit-bug-summary-description=Inline editing of bug description and summary
745-story-hwdb-users-using-driver=Get number of users using a driver
746diff --git a/contrib/update-milestone-progress.py b/contrib/update-milestone-progress.py
747deleted file mode 100755
748index 1c207fc..0000000
749--- a/contrib/update-milestone-progress.py
750+++ /dev/null
751@@ -1,359 +0,0 @@
752-#!/usr/bin/env python
753-
754-# Copyright (C) 2009 Canonical Ltd.
755-#
756-# This file is part of launchpadlib.
757-#
758-# launchpadlib is free software: you can redistribute it and/or modify it
759-# under the terms of the GNU Lesser General Public License as published by the
760-# Free Software Foundation, version 3 of the License.
761-#
762-# launchpadlib is distributed in the hope that it will be useful, but WITHOUT
763-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
764-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
765-# for more details.
766-#
767-# You should have received a copy of the GNU Lesser General Public License
768-# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
769-
770-from __future__ import with_statement
771-from ConfigParser import RawConfigParser
772-from datetime import date, timedelta
773-import os
774-import sys
775-
776-from bzrlib.branch import Branch
777-
778-from close_bugs_from_commits import get_fixed_bug_ids
779-from editmoin import editshortcut
780-from launchpadlib.launchpad import Launchpad
781-
782-
783-class Config:
784- def __init__(self):
785- self.tag_to_story_name = {}
786- self.milestone_name = None
787- self.start = None
788- self.end = None
789- self.project_name = None
790- self.template_filename = None
791- self.branch_location = None
792- self.branch_start_revno = None
793- self.base_wiki_location = None
794- self.stories_page = None
795-
796- @property
797- def story_tags(self):
798- return self.tag_to_story_name.keys()
799-
800-
801-def get_assignee_name(bugtask):
802- """Get the name of the person assigned to the bug task.
803-
804- Instead of accessing bugtask.assignee.name, the name is extracted
805- from the assignee_link, which avoids fetching the assignee object
806- from the web service.
807- """
808- if bugtask.assignee_link is None:
809- return None
810- url_parts = bugtask.assignee_link.split('/')
811- return url_parts[-1].lstrip('~')
812-
813-
814-def string_isodate_to_date(string_isodate):
815- if 'T' in string_isodate:
816- string_isodate, rest = string_isodate.split('T', 1)
817- year, month, day = string_isodate.split('-')
818- return date(int(year), int(month), int(day))
819-
820-
821-class MilestoneState:
822- """The current state of the milestone."""
823-
824- def __init__(self, start, end, today):
825- self.days = {}
826- self.today = today
827- self.start = start
828- self.end = end
829- one_day = timedelta(days=1)
830- current_day = start
831- while current_day <= end:
832- self.days[current_day] = {}
833- current_day += one_day
834-
835- def mark_done(self, bug_id, day):
836- if day < self.start:
837- day = self.start
838- if day > self.end:
839- # It was fixed after the milestone ended.
840- return
841- self.days[day][bug_id] = 'D'
842-
843- def mark_started(self, bug_id, day):
844- if day not in self.days:
845- day = self.start
846- self.days[day][bug_id] = 'P'
847-
848- def mark_added(self, bug_id, day):
849- self.days[day][bug_id] = 'N'
850-
851-
852-def get_config(config_filepath):
853- """Create a map between committers and their test pages.
854-
855- Read the config file, and return a map, mapping a committer to the
856- name of the wiki page where test plans are held.
857- """
858- config_parser = RawConfigParser()
859- config_parser.read([config_filepath])
860- config = Config()
861-
862- config.branch_location = config_parser.get('Branch', 'location')
863- config.branch_start_revno = config_parser.getint('Branch', 'start_revno')
864-
865- config.milestone_name = config_parser.get('Milestone', 'name')
866- config.project_name = config_parser.get('Milestone', 'project')
867- config.template_filename = config_parser.get('Milestone', 'template')
868- config.base_wiki_location = config_parser.get(
869- 'Milestone', 'base_wiki_location')
870- config.stories_page = config_parser.get(
871- 'Milestone', 'stories_page')
872- config.start = string_isodate_to_date(
873- config_parser.get('Milestone', 'start'))
874- config.end = string_isodate_to_date(
875- config_parser.get('Milestone', 'end'))
876- config.release_critical = string_isodate_to_date(
877- config_parser.get('Milestone', 'release_critical'))
878- config.story_tags = []
879- for tag_name, story_name in config_parser.items('Stories'):
880- config.story_tags.append(tag_name)
881- config.tag_to_story_name[unicode(tag_name)] = story_name
882- return config
883-
884-
885-class Story:
886-
887- def __init__(self, name, tag_name):
888- self.name = name
889- self.tag_name = tag_name
890- self.tasks = []
891-
892-
893-ALL_BUG_STATUSES = [
894- "New",
895- "Incomplete",
896- "Invalid",
897- "Won't Fix",
898- "Confirmed",
899- "Triaged",
900- "In Progress",
901- "Fix Committed",
902- "Fix Released",
903- ]
904-
905-content_color = {
906- 'P': '#FF8080',
907- 'N': '#FFFFE0',
908- 'D': '#80FF80',
909- }
910-
911-def get_cell_style(content, fallback_color=None):
912- color = content_color.get(content, fallback_color)
913- if color is not None:
914- return '<style="background-color: %s;">' % color
915- else:
916- return ''
917-
918-def generate_milestone_table(milestone_state, stories, config):
919- table_rows = []
920- row_items = [
921- "''Story/Task''",
922- "''Assignee''",
923- ]
924- for day in sorted(milestone_state.days.keys()):
925- row_items.append("''%s''" % str(day.day))
926- table_rows.append('|| %s ||' % ' || '.join(row_items))
927- for story in stories:
928- row_items = []
929- unimportant, story_anchor = story.tag_name.split('-', 1)
930- if story.tag_name == 'unrelated-bugs':
931- row_items.append(
932- '<rowstyle="background-color: #CC6633;">'
933- ' %s' % story.name)
934- else:
935- row_items.append(
936- '<rowstyle="background-color: #E0E0FF;">'
937- " '''[[%s#%s|%s]]'''" % (
938- config.stories_page, story_anchor, story.name))
939-
940- row_items.append('') # Assignee
941- task_state = ''
942- for day in sorted(milestone_state.days.keys()):
943- row_items.append(task_state)
944- table_rows.append('||%s ||' % ' ||'.join(row_items))
945- for bugtask in story.tasks:
946- row_items = []
947- row_items.append(
948- '[[https://launchpad.net/bugs/%(bug_id)s|#%(bug_id)s]]:'
949- ' %(title)s' % dict(
950- bug_id=bugtask.bug.id, title=bugtask.bug.title))
951- assignee_name = get_assignee_name(bugtask)
952- if assignee_name is not None:
953- row_items.append(" [[/%s|%s]] "% (
954- assignee_name, assignee_name))
955- else:
956- row_items.append('') # Assignee
957- task_state = ''
958- future_color = ''
959- for day, changes in sorted(milestone_state.days.items()):
960- task_state = changes.get(bugtask.bug.id, task_state)
961- if day > milestone_state.today:
962- task_state = ''
963- if day > config.release_critical:
964- future_color = '#C8BBBE'
965- row_items.append("%s %s" % (
966- get_cell_style(task_state, future_color), task_state))
967- table_rows.append('|| %s ||' % ' ||'.join(row_items))
968-
969- return '\n'.join(table_rows)
970-
971-
972-
973-def get_day_added(task, fallback=None):
974- return fallback
975-
976-
977-def get_day_started(task):
978- if task.date_in_progress is not None:
979- return string_isodate_to_date(task.date_in_progress)
980- return None
981-
982-
983-def get_day_done(task):
984- # We should be able to use date_closed, but it seems like it's not
985- # always set.
986- closed_date_attributes = ['date_fix_committed', 'date_fix_released']
987- for closed_date_attribute in closed_date_attributes:
988- date_closed = getattr(task, closed_date_attribute)
989- if date_closed is not None:
990- return string_isodate_to_date(date_closed)
991- return None
992-
993-
994-def get_associated_story_tags(bugtask):
995- return [tag for tag in bugtask.bug.tags if tag.startswith('story-')]
996-
997-
998-def main():
999- config = get_config('update-milestone-progress.conf')
1000- launchpad = Launchpad.login_with(os.path.basename(sys.argv[0]),
1001- 'production')
1002- stories = dict(
1003- (story_tag, Story(story_name, story_tag))
1004- for story_tag, story_name in sorted(config.tag_to_story_name.items()))
1005- # 'unrelated' isn't quite right, but I want to sort on the tag name
1006- # and have the bugs come last.
1007- stories['unrelated-bugs'] = Story(
1008- 'Bugs not related to a story', 'unrelated-bugs')
1009- project = launchpad.projects[config.project_name]
1010- for milestone in project.all_milestones:
1011- if milestone.name == config.milestone_name:
1012- break
1013- else:
1014- raise AssertionError("No milestone: %s" % config.milestone_name)
1015-
1016- today = date.today()
1017- milestone_state = MilestoneState(config.start, config.end, today)
1018-
1019- milestone_bugtasks = dict(
1020- (bugtask.bug.id, bugtask)
1021- for bugtask in project.searchTasks(status=ALL_BUG_STATUSES,
1022- milestone=milestone))
1023- assignees = set()
1024- for task in milestone_bugtasks.values():
1025- assignee_name = get_assignee_name(task)
1026- if assignee_name is not None:
1027- assignees.add(assignee_name)
1028- associated_story_tags = get_associated_story_tags(task)
1029- if len(associated_story_tags) == 0:
1030- # This bug isn't part of a story. Put it into the pseudo
1031- # story which is used for all such bug fixes.
1032- associated_story_tags = ['unrelated-bugs']
1033- for story_tag in associated_story_tags:
1034- if story_tag not in stories:
1035- stories[story_tag] = Story(
1036- story_tag[len('story-'):], story_tag)
1037- stories[story_tag].tasks.append(task)
1038- milestone_state.mark_added(
1039- task.bug.id, get_day_added(task, fallback=config.start))
1040- day_started = get_day_started(task)
1041- if day_started is not None:
1042- milestone_state.mark_started(task.bug.id, day_started)
1043- day_done = get_day_done(task)
1044- if day_done is not None:
1045- milestone_state.mark_done(task.bug.id, day_done)
1046-
1047- # Look through commits and marks tasks as done, if any bug fixes are
1048- # found.
1049- branch = Branch.open(config.branch_location)
1050- repository = branch.repository
1051- start_revno = config.branch_start_revno
1052- for revno in range(start_revno, branch.revno()+1):
1053- rev_id = branch.get_rev_id(revno)
1054- revision = repository.get_revision(rev_id)
1055- fixed_bugs = get_fixed_bug_ids(revision)
1056- for fixed_bug in sorted(fixed_bugs):
1057- try:
1058- lp_bug = launchpad.bugs[int(fixed_bug)]
1059- except KeyError:
1060- # Invalid bug id specified, skip it.
1061- continue
1062- if milestone_bugtasks.get(lp_bug.id) is not None:
1063- milestone_state.mark_done(
1064- lp_bug.id, date.fromtimestamp(revision.timestamp))
1065-
1066- # Order by the order in the config file first.
1067- sorted_stories = [stories[story_tag] for story_tag in config.story_tags]
1068- sorted_stories.extend(
1069- story for story_tag, story in sorted(stories.items())
1070- if story_tag not in config.story_tags)
1071- table = generate_milestone_table(milestone_state, sorted_stories, config)
1072- with open(config.template_filename, 'r') as template:
1073- page = template.read() % dict(
1074- milestone=config.milestone_name,
1075- progress_table=table,
1076- )
1077- def update_if_modified(moinfile):
1078- if moinfile._unescape(moinfile.body) == page:
1079- # Nothing has changed, cancel the edit.
1080- return 0
1081- else:
1082- moinfile.body = page
1083- return 1
1084-
1085- page_shortcut = config.base_wiki_location + config.milestone_name
1086- editshortcut(page_shortcut, editfile_func=update_if_modified)
1087-
1088- # Generate assignee pages.
1089- for assignee_name in assignees:
1090- print "Assignee: %s" % assignee_name
1091- assignee_stories = dict()
1092- for story in stories.values():
1093- assigned_bugs = [
1094- bug_task for bug_task in story.tasks
1095- if (bug_task.assignee is not None and
1096- bug_task.assignee.name == assignee_name)]
1097- if len(assigned_bugs) > 0:
1098- assignee_stories[story.tag_name] = Story(
1099- story.name, story.tag_name)
1100- assignee_stories[story.tag_name].tasks = assigned_bugs
1101- page = generate_milestone_table(
1102- milestone_state, assignee_stories.values(), config)
1103- assignee_page_shortcut = page_shortcut + '/' + assignee_name
1104- editshortcut(
1105- assignee_page_shortcut, editfile_func=update_if_modified)
1106-
1107- print "done."
1108-
1109-if __name__ == '__main__':
1110- main()
1111diff --git a/contrib/update-milestone-progress.template.sample b/contrib/update-milestone-progress.template.sample
1112deleted file mode 100644
1113index 0db2ae0..0000000
1114--- a/contrib/update-milestone-progress.template.sample
1115+++ /dev/null
1116@@ -1,8 +0,0 @@
1117-= %(milestone)s Feature Progress =
1118-
1119-All the features that are currently being worked on are listed here. Don't edit this page. This information is being generated by a script, and any manual edits will be overwritten the next time the script runs.
1120-
1121-'''This page is currently in a beta stage, and the information here might not be
1122-correct.'''
1123-
1124-%(progress_table)s

Subscribers

People subscribed via source and target branches