1 #!/usr/bin/env python |
1 # Author: Dmitriy Morozov <hg@foxcub.org>, 2007 |
2 |
2 |
3 from mercurial import hg |
3 """A very simple and lightweight issue tracker for Mercurial.""" |
|
4 |
|
5 from mercurial import hg, util |
4 from mercurial.i18n import _ |
6 from mercurial.i18n import _ |
5 import os, time, random |
7 import os, time, random, mailbox, glob, socket |
|
8 |
|
9 |
|
10 new_state = "new" |
|
11 default_state = new_state |
|
12 issues_dir = ".issues" |
|
13 filter_filename = ".filters" |
|
14 date_format = '%a, %d %b %Y %H:%M:%S %Z' |
|
15 |
6 |
16 |
7 def list(ui, repo, **opts): |
17 def list(ui, repo, **opts): |
8 """List issues associated with the project""" |
18 """List issues associated with the project""" |
|
19 |
|
20 # Process options |
|
21 show_all = False or opts['all'] |
|
22 properties = _get_properties(opts['property']) or [['state', new_state]] |
|
23 date_match = lambda x: True |
|
24 if opts['date']: |
|
25 date_match = util.matchdate(opts['date']) |
|
26 |
|
27 # Find issues |
|
28 issues_path = os.path.join(repo.root, issues_dir) |
|
29 if not os.path.exists(issues_path): return |
|
30 |
|
31 issues = glob.glob(os.path.join(issues_path, '*')) |
|
32 |
|
33 for issue in issues: |
|
34 mbox = mailbox.mbox(issue) |
|
35 property_match = True |
|
36 for property,value in properties: |
|
37 property_match = property_match and (mbox[0][property] == value) |
|
38 if not show_all and not property_match: continue |
|
39 if not date_match(util.parsedate(mbox[0]['date'], [date_format])[0]): continue |
|
40 print "%s [%s]: %s (%d)" % (issue[len(issues_path)+1:], # +1 for trailing / |
|
41 mbox[0]['State'], |
|
42 mbox[0]['Subject'], |
|
43 len(mbox)-1) # number of replies (-1 for self) |
|
44 |
9 |
45 |
10 def add(ui, repo): |
46 def add(ui, repo): |
11 """Adds a new issue""" |
47 """Adds a new issue""" |
12 |
48 |
13 # First, make sure issues have a directory |
49 # First, make sure issues have a directory |
14 issues_path = os.path.join(repo.root, '.issues') |
50 issues_path = os.path.join(repo.root, issues_dir) |
15 if not os.path.exists(issues_path): os.mkdir(issues_path) |
51 if not os.path.exists(issues_path): os.mkdir(issues_path) |
16 |
52 |
17 user = ui.username() |
53 user = ui.username() |
18 |
54 |
19 default_issue_text = "From: %s\nDate: %s\n" % (user, |
55 default_issue_text = "From: %s\nDate: %s\n" % (user, |
20 time.strftime('%a, %d %b %Y %H:%M:%S %Z')) |
56 time.strftime(date_format)) |
21 default_issue_text += "Status: new\nSubject: brief description\n\n" |
57 default_issue_text += "State: %s\nSubject: brief description\n\n" % default_state |
22 default_issue_text += "Detailed description." |
58 default_issue_text += "Detailed description." |
23 |
59 |
24 issue = ui.edit(default_issue_text, user) |
60 issue = ui.edit(default_issue_text, user) |
25 if issue.strip() == '': |
61 if issue.strip() == '': |
26 ui.warn('Empty issue, ignoring\n') |
62 ui.warn('Empty issue, ignoring\n') |
27 return |
63 return |
28 if issue.strip() == default_issue_text: |
64 if issue.strip() == default_issue_text: |
29 ui.warn('Unchanged issue text, ignoring\n') |
65 ui.warn('Unchanged issue text, ignoring\n') |
30 return |
66 return |
31 |
67 |
|
68 # Create the message |
|
69 msg = mailbox.mboxMessage(issue) |
|
70 msg.set_from('artemis', True) |
|
71 |
32 # Pick random filename |
72 # Pick random filename |
33 issue_fn = issues_path |
73 issue_fn = issues_path |
34 while os.path.exists(issue_fn): |
74 while os.path.exists(issue_fn): |
35 issue_fn = os.path.join(issues_path, "%x" % random.randint(2**32, 2**64-1)) |
75 issue_id = "%x" % random.randint(2**63, 2**64-1) |
|
76 issue_fn = os.path.join(issues_path, issue_id) |
|
77 msg.add_header('Message-Id', "%s-0-artemis@%s" % (issue_id, socket.gethostname())) |
36 |
78 |
37 # FIXME: replace with creating a mailbox |
79 # Add message to the mailbox |
38 f = file(issue_fn, "w") |
80 mbox = mailbox.mbox(issue_fn) |
39 f.write(issue) |
81 mbox.add(msg) |
40 f.close() |
82 mbox.close() |
41 |
83 |
|
84 # Add the new mailbox to the repository |
42 repo.add([issue_fn[(len(repo.root)+1):]]) # +1 for the trailing / |
85 repo.add([issue_fn[(len(repo.root)+1):]]) # +1 for the trailing / |
43 |
86 |
44 def show(ui, repo, id): |
87 |
45 """Shows issue ID""" |
88 def show(ui, repo, id, comment = None): |
|
89 """Shows issue ID, or possibly its comment COMMENT""" |
|
90 |
|
91 issue = _find_issue(ui, repo, id) |
|
92 if not issue: return |
|
93 |
|
94 # Read the issue |
|
95 mbox = mailbox.mbox(issue) |
|
96 msg = mbox[0] |
|
97 ui.write(msg.as_string()) |
|
98 |
|
99 # Walk the mailbox, and output comments |
|
100 |
|
101 |
|
102 |
|
103 def update(ui, repo, id, **opts): |
|
104 """Update properties of issue ID, or add a comment to it or its comment COMMENT""" |
|
105 |
|
106 issue = _find_issue(ui, repo, id) |
|
107 if not issue: return |
|
108 |
|
109 properties = _get_properties(opts['property']) |
|
110 |
|
111 # Read the issue |
|
112 mbox = mailbox.mbox(issue) |
|
113 msg = mbox[0] |
|
114 |
|
115 # Fix the properties |
|
116 for property, value in properties: |
|
117 msg.replace_header(property, value) |
|
118 mbox[0] = msg |
|
119 mbox.flush() |
|
120 |
|
121 # Deal with comments |
|
122 |
|
123 # Show updated message |
|
124 ui.write(mbox[0].as_string()) |
|
125 |
|
126 |
|
127 def _find_issue(ui, repo, id): |
|
128 issues_path = os.path.join(repo.root, issues_dir) |
|
129 if not os.path.exists(issues_path): return False |
|
130 |
|
131 issues = glob.glob(os.path.join(issues_path, id + '*')) |
|
132 |
|
133 if len(issues) == 0: |
|
134 return False |
|
135 elif len(issues) > 1: |
|
136 ui.status("Multiple choices:\n") |
|
137 for i in issues: ui.status(' ', i[len(issues_path)+1:], '\n') |
|
138 return False |
|
139 |
|
140 return issues[0] |
|
141 |
|
142 def _get_properties(property_list): |
|
143 return [p.split('=') for p in property_list] |
|
144 |
|
145 |
46 |
146 |
47 cmdtable = { |
147 cmdtable = { |
48 'issues-list': (list, |
148 'ilist': (list, |
49 [('s', 'status', None, 'restrict status')], |
149 [('a', 'all', None, |
50 _('hg issues-list')), |
150 'list all issues (by default only those with state new)'), |
51 'issues-add': (add, |
151 ('p', 'property', [], |
52 [], |
152 'list issues with specific field values (e.g., -p state=fixed)'), |
53 _('hg issues-add')), |
153 ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'), |
54 'issues-show': (show, |
154 ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s)' % (issues_dir, filter_filename))], |
55 [], |
155 _('hg ilist [OPTIONS]')), |
56 _('hg issues-show ID')) |
156 'iadd': (add, |
|
157 [], |
|
158 _('hg iadd')), |
|
159 'ishow': (show, |
|
160 [('v', 'verbose', None, 'list the comments')], |
|
161 _('hg ishow ID [COMMENT]')), |
|
162 'iupdate': (update, |
|
163 [('p', 'property', [], |
|
164 'update properties (e.g., -p state=fixed)'), |
|
165 ('c', 'comment', 0, |
|
166 'add a comment to issue or its comment COMMENT')], |
|
167 _('hg iupdate [OPTIONS] ID [COMMENT]')) |
57 } |
168 } |