13 filter_prefix = ".filter" |
13 filter_prefix = ".filter" |
14 date_format = '%a, %d %b %Y %H:%M:%S' |
14 date_format = '%a, %d %b %Y %H:%M:%S' |
15 |
15 |
16 |
16 |
17 def ilist(ui, repo, **opts): |
17 def ilist(ui, repo, **opts): |
18 """List issues associated with the project""" |
18 """List issues associated with the project""" |
19 |
19 |
20 # Process options |
20 # Process options |
21 show_all = opts['all'] |
21 show_all = opts['all'] |
22 properties = [] |
22 properties = [] |
23 match_date, date_match = False, lambda x: True |
23 match_date, date_match = False, lambda x: True |
24 if opts['date']: |
24 if opts['date']: |
25 match_date, date_match = True, util.matchdate(opts['date']) |
25 match_date, date_match = True, util.matchdate(opts['date']) |
26 |
26 |
27 # Find issues |
27 # Find issues |
28 issues_path = os.path.join(repo.root, issues_dir) |
28 issues_path = os.path.join(repo.root, issues_dir) |
29 if not os.path.exists(issues_path): return |
29 if not os.path.exists(issues_path): return |
30 |
30 |
31 issues = glob.glob(os.path.join(issues_path, '*')) |
31 issues = glob.glob(os.path.join(issues_path, '*')) |
32 |
32 |
33 # Process filter |
33 # Process filter |
34 if opts['filter']: |
34 if opts['filter']: |
35 filters = glob.glob(os.path.join(issues_path, filter_prefix + '*')) |
35 filters = glob.glob(os.path.join(issues_path, filter_prefix + '*')) |
36 config = ConfigParser.SafeConfigParser() |
36 config = ConfigParser.SafeConfigParser() |
37 config.read(filters) |
37 config.read(filters) |
38 if not config.has_section(opts['filter']): |
38 if not config.has_section(opts['filter']): |
39 ui.warning('No filter %s defined\n', opts['filter']) |
39 ui.warning('No filter %s defined\n', opts['filter']) |
40 else: |
40 else: |
41 properties += config.items(opts['filter']) |
41 properties += config.items(opts['filter']) |
42 |
42 |
43 _get_properties(opts['property']) |
43 _get_properties(opts['property']) |
44 |
44 |
45 for issue in issues: |
45 for issue in issues: |
46 mbox = mailbox.mbox(issue) |
46 mbox = mailbox.mbox(issue) |
47 property_match = True |
47 property_match = True |
48 for property,value in properties: |
48 for property,value in properties: |
49 property_match = property_match and (mbox[0][property] == value) |
49 property_match = property_match and (mbox[0][property] == value) |
50 if not show_all and (not properties or not property_match) and (properties or mbox[0]['State'].upper() == state['fixed'].upper()): continue |
50 if not show_all and (not properties or not property_match) and (properties or mbox[0]['State'].upper() == state['fixed'].upper()): continue |
51 |
51 |
52 |
52 |
53 if match_date and not date_match(util.parsedate(mbox[0]['date'])[0]): continue |
53 if match_date and not date_match(util.parsedate(mbox[0]['date'])[0]): continue |
54 ui.write("%s (%d) [%s]: %s\n" % (issue[len(issues_path)+1:], # +1 for trailing / |
54 ui.write("%s (%d) [%s]: %s\n" % (issue[len(issues_path)+1:], # +1 for trailing / |
55 len(mbox)-1, # number of replies (-1 for self) |
55 len(mbox)-1, # number of replies (-1 for self) |
56 mbox[0]['State'], |
56 mbox[0]['State'], |
57 mbox[0]['Subject'])) |
57 mbox[0]['Subject'])) |
58 |
58 |
59 |
59 |
60 def iadd(ui, repo, id = None, comment = 0): |
60 def iadd(ui, repo, id = None, comment = 0): |
61 """Adds a new issue, or comment to an existing issue ID or its comment COMMENT""" |
61 """Adds a new issue, or comment to an existing issue ID or its comment COMMENT""" |
62 |
62 |
63 comment = int(comment) |
63 comment = int(comment) |
64 |
64 |
65 # First, make sure issues have a directory |
65 # First, make sure issues have a directory |
66 issues_path = os.path.join(repo.root, issues_dir) |
66 issues_path = os.path.join(repo.root, issues_dir) |
67 if not os.path.exists(issues_path): os.mkdir(issues_path) |
67 if not os.path.exists(issues_path): os.mkdir(issues_path) |
68 |
68 |
69 if id: |
69 if id: |
70 issue_fn, issue_id = _find_issue(ui, repo, id) |
70 issue_fn, issue_id = _find_issue(ui, repo, id) |
71 if not issue_fn: |
71 if not issue_fn: |
72 ui.warn('No such issue\n') |
72 ui.warn('No such issue\n') |
73 return |
73 return |
74 |
74 |
75 user = ui.username() |
75 user = ui.username() |
76 |
76 |
77 default_issue_text = "From: %s\nDate: %s\n" % (user, util.datestr(format = date_format)) |
77 default_issue_text = "From: %s\nDate: %s\n" % (user, util.datestr(format = date_format)) |
78 if not id: |
78 if not id: |
79 default_issue_text += "State: %s\n" % state['default'] |
79 default_issue_text += "State: %s\n" % state['default'] |
80 default_issue_text += "Subject: brief description\n\n" |
80 default_issue_text += "Subject: brief description\n\n" |
81 default_issue_text += "Detailed description." |
81 default_issue_text += "Detailed description." |
82 |
82 |
83 issue = ui.edit(default_issue_text, user) |
83 issue = ui.edit(default_issue_text, user) |
84 if issue.strip() == '': |
84 if issue.strip() == '': |
85 ui.warn('Empty issue, ignoring\n') |
85 ui.warn('Empty issue, ignoring\n') |
86 return |
86 return |
87 if issue.strip() == default_issue_text: |
87 if issue.strip() == default_issue_text: |
88 ui.warn('Unchanged issue text, ignoring\n') |
88 ui.warn('Unchanged issue text, ignoring\n') |
89 return |
89 return |
90 |
90 |
91 # Create the message |
91 # Create the message |
92 msg = mailbox.mboxMessage(issue) |
92 msg = mailbox.mboxMessage(issue) |
93 msg.set_from('artemis', True) |
93 msg.set_from('artemis', True) |
94 |
94 |
95 # Pick random filename |
95 # Pick random filename |
96 if not id: |
96 if not id: |
97 issue_fn = issues_path |
97 issue_fn = issues_path |
98 while os.path.exists(issue_fn): |
98 while os.path.exists(issue_fn): |
99 issue_id = _random_id() |
99 issue_id = _random_id() |
100 issue_fn = os.path.join(issues_path, issue_id) |
100 issue_fn = os.path.join(issues_path, issue_id) |
101 # else: issue_fn already set |
101 # else: issue_fn already set |
102 |
102 |
103 # Add message to the mailbox |
103 # Add message to the mailbox |
104 mbox = mailbox.mbox(issue_fn) |
104 mbox = mailbox.mbox(issue_fn) |
105 if id and comment not in mbox: |
105 if id and comment not in mbox: |
106 ui.warn('No such comment number in mailbox, commenting on the issue itself\n') |
106 ui.warn('No such comment number in mailbox, commenting on the issue itself\n') |
107 if not id: |
107 if not id: |
108 msg.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname())) |
108 msg.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname())) |
109 else: |
109 else: |
110 msg.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname())) |
110 msg.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname())) |
111 msg.add_header('References', mbox[(comment < len(mbox) and comment) or 0]['Message-Id']) |
111 msg.add_header('References', mbox[(comment < len(mbox) and comment) or 0]['Message-Id']) |
112 msg.add_header('In-Reply-To', mbox[(comment < len(mbox) and comment) or 0]['Message-Id']) |
112 msg.add_header('In-Reply-To', mbox[(comment < len(mbox) and comment) or 0]['Message-Id']) |
113 mbox.add(msg) |
113 mbox.add(msg) |
114 mbox.close() |
114 mbox.close() |
115 |
115 |
116 # If adding issue, add the new mailbox to the repository |
116 # If adding issue, add the new mailbox to the repository |
117 if not id: |
117 if not id: |
118 repo.add([issue_fn[(len(repo.root)+1):]]) # +1 for the trailing / |
118 repo.add([issue_fn[(len(repo.root)+1):]]) # +1 for the trailing / |
119 ui.status('Added new issue %s\n' % issue_id) |
119 ui.status('Added new issue %s\n' % issue_id) |
120 |
120 |
121 |
121 |
122 def ishow(ui, repo, id, comment = 0, **opts): |
122 def ishow(ui, repo, id, comment = 0, **opts): |
123 """Shows issue ID, or possibly its comment COMMENT""" |
123 """Shows issue ID, or possibly its comment COMMENT""" |
124 |
124 |
125 comment = int(comment) |
125 comment = int(comment) |
126 issue, id = _find_issue(ui, repo, id) |
126 issue, id = _find_issue(ui, repo, id) |
127 if not issue: return |
127 if not issue: return |
128 mbox = mailbox.mbox(issue) |
128 mbox = mailbox.mbox(issue) |
129 |
129 |
130 if opts['all']: |
130 if opts['all']: |
131 ui.write('='*70 + '\n') |
131 ui.write('='*70 + '\n') |
132 for i in xrange(len(mbox)): |
132 for i in xrange(len(mbox)): |
133 _write_message(ui, mbox[i], i) |
133 _write_message(ui, mbox[i], i) |
134 ui.write('-'*70 + '\n') |
134 ui.write('-'*70 + '\n') |
135 return |
135 return |
136 |
136 |
137 _show_mbox(ui, mbox, comment) |
137 _show_mbox(ui, mbox, comment) |
138 |
138 |
139 |
139 |
140 def iupdate(ui, repo, id, **opts): |
140 def iupdate(ui, repo, id, **opts): |
141 """Update properties of issue ID""" |
141 """Update properties of issue ID""" |
142 |
142 |
143 issue, id = _find_issue(ui, repo, id) |
143 issue, id = _find_issue(ui, repo, id) |
144 if not issue: return |
144 if not issue: return |
145 |
145 |
146 properties = _get_properties(opts['property']) |
146 properties = _get_properties(opts['property']) |
147 |
147 |
148 # Read the issue |
148 # Read the issue |
149 mbox = mailbox.mbox(issue) |
149 mbox = mailbox.mbox(issue) |
150 msg = mbox[0] |
150 msg = mbox[0] |
151 |
151 |
152 # Fix the properties |
152 # Fix the properties |
153 properties_text = '' |
153 properties_text = '' |
154 for property, value in properties: |
154 for property, value in properties: |
155 msg.replace_header(property, value) |
155 msg.replace_header(property, value) |
156 properties_text += '%s=%s\n' % (property, value) |
156 properties_text += '%s=%s\n' % (property, value) |
157 mbox[0] = msg |
157 mbox[0] = msg |
158 |
158 |
159 # Write down a comment about updated properties |
159 # Write down a comment about updated properties |
160 if properties and not opts['no_property_comment']: |
160 if properties and not opts['no_property_comment']: |
161 user = ui.username() |
161 user = ui.username() |
162 properties_text = "From: %s\nDate: %s\nSubject: properties changes (%s)\n\n%s" % \ |
162 properties_text = "From: %s\nDate: %s\nSubject: properties changes (%s)\n\n%s" % \ |
163 (user, util.datestr(format = date_format), |
163 (user, util.datestr(format = date_format), |
164 _pretty_list(list(set([property for property, value in properties]))), |
164 _pretty_list(list(set([property for property, value in properties]))), |
165 properties_text) |
165 properties_text) |
166 msg = mailbox.mboxMessage(properties_text) |
166 msg = mailbox.mboxMessage(properties_text) |
167 msg.add_header('Message-Id', "<%s-%s-artemis@%s>" % (id, _random_id(), socket.gethostname())) |
167 msg.add_header('Message-Id', "<%s-%s-artemis@%s>" % (id, _random_id(), socket.gethostname())) |
168 msg.add_header('References', mbox[0]['Message-Id']) |
168 msg.add_header('References', mbox[0]['Message-Id']) |
169 msg.add_header('In-Reply-To', mbox[0]['Message-Id']) |
169 msg.add_header('In-Reply-To', mbox[0]['Message-Id']) |
170 msg.set_from('artemis', True) |
170 msg.set_from('artemis', True) |
171 mbox.add(msg) |
171 mbox.add(msg) |
172 mbox.flush() |
172 mbox.flush() |
173 |
173 |
174 # Show updated message |
174 # Show updated message |
175 _show_mbox(ui, mbox, 0) |
175 _show_mbox(ui, mbox, 0) |
176 |
176 |
177 |
177 |
178 def _find_issue(ui, repo, id): |
178 def _find_issue(ui, repo, id): |
179 issues_path = os.path.join(repo.root, issues_dir) |
179 issues_path = os.path.join(repo.root, issues_dir) |
180 if not os.path.exists(issues_path): return False |
180 if not os.path.exists(issues_path): return False |
181 |
181 |
182 issues = glob.glob(os.path.join(issues_path, id + '*')) |
182 issues = glob.glob(os.path.join(issues_path, id + '*')) |
183 |
183 |
184 if len(issues) == 0: |
184 if len(issues) == 0: |
185 return False, 0 |
185 return False, 0 |
186 elif len(issues) > 1: |
186 elif len(issues) > 1: |
187 ui.status("Multiple choices:\n") |
187 ui.status("Multiple choices:\n") |
188 for i in issues: ui.status(' ', i[len(issues_path)+1:], '\n') |
188 for i in issues: ui.status(' ', i[len(issues_path)+1:], '\n') |
189 return False, 0 |
189 return False, 0 |
190 |
190 |
191 return issues[0], issues[0][len(issues_path)+1:] |
191 return issues[0], issues[0][len(issues_path)+1:] |
192 |
192 |
193 def _get_properties(property_list): |
193 def _get_properties(property_list): |
194 return [p.split('=') for p in property_list] |
194 return [p.split('=') for p in property_list] |
195 |
195 |
196 def _write_message(ui, message, index = 0): |
196 def _write_message(ui, message, index = 0): |
197 if index: ui.write("Comment: %d\n" % index) |
197 if index: ui.write("Comment: %d\n" % index) |
198 if ui.verbose: |
198 if ui.verbose: |
199 ui.write(message.as_string().strip() + '\n') |
199 ui.write(message.as_string().strip() + '\n') |
200 else: |
200 else: |
201 if 'From' in message: ui.write('From: %s\n' % message['From']) |
201 if 'From' in message: ui.write('From: %s\n' % message['From']) |
202 if 'Date' in message: ui.write('Date: %s\n' % message['Date']) |
202 if 'Date' in message: ui.write('Date: %s\n' % message['Date']) |
203 if 'Subject' in message: ui.write('Subject: %s\n' % message['Subject']) |
203 if 'Subject' in message: ui.write('Subject: %s\n' % message['Subject']) |
204 if 'State' in message: ui.write('State: %s\n' % message['State']) |
204 if 'State' in message: ui.write('State: %s\n' % message['State']) |
205 ui.write('\n' + message.get_payload().strip() + '\n') |
205 ui.write('\n' + message.get_payload().strip() + '\n') |
206 |
206 |
207 def _show_mbox(ui, mbox, comment): |
207 def _show_mbox(ui, mbox, comment): |
208 # Output the issue (or comment) |
208 # Output the issue (or comment) |
209 if comment >= len(mbox): |
209 if comment >= len(mbox): |
210 comment = 0 |
210 comment = 0 |
211 ui.warn('Comment out of range, showing the issue itself\n') |
211 ui.warn('Comment out of range, showing the issue itself\n') |
212 msg = mbox[comment] |
212 msg = mbox[comment] |
213 ui.write('='*70 + '\n') |
213 ui.write('='*70 + '\n') |
214 if comment: |
214 if comment: |
215 ui.write('Subject: %s\n' % mbox[0]['Subject']) |
215 ui.write('Subject: %s\n' % mbox[0]['Subject']) |
216 ui.write('State: %s\n' % mbox[0]['State']) |
216 ui.write('State: %s\n' % mbox[0]['State']) |
217 ui.write('-'*70 + '\n') |
217 ui.write('-'*70 + '\n') |
218 _write_message(ui, msg, comment) |
218 _write_message(ui, msg, comment) |
219 ui.write('-'*70 + '\n') |
219 ui.write('-'*70 + '\n') |
220 |
220 |
221 # Read the mailbox into the messages and children dictionaries |
221 # Read the mailbox into the messages and children dictionaries |
222 messages = {} |
222 messages = {} |
223 children = {} |
223 children = {} |
224 for i in xrange(len(mbox)): |
224 for i in xrange(len(mbox)): |
225 m = mbox[i] |
225 m = mbox[i] |
226 messages[m['Message-Id']] = (i,m) |
226 messages[m['Message-Id']] = (i,m) |
227 children.setdefault(m['In-Reply-To'], []).append(m['Message-Id']) |
227 children.setdefault(m['In-Reply-To'], []).append(m['Message-Id']) |
228 children[None] = [] # Safeguard against infinte loop on empty Message-Id |
228 children[None] = [] # Safeguard against infinte loop on empty Message-Id |
229 |
229 |
230 # Iterate over children |
230 # Iterate over children |
231 id = msg['Message-Id'] |
231 id = msg['Message-Id'] |
232 id_stack = (id in children and map(lambda x: (x, 1), reversed(children[id]))) or [] |
232 id_stack = (id in children and map(lambda x: (x, 1), reversed(children[id]))) or [] |
233 if not id_stack: return |
233 if not id_stack: return |
234 ui.write('Comments:\n') |
234 ui.write('Comments:\n') |
235 while id_stack: |
235 while id_stack: |
236 id,offset = id_stack.pop() |
236 id,offset = id_stack.pop() |
237 id_stack += (id in children and map(lambda x: (x, offset+1), reversed(children[id]))) or [] |
237 id_stack += (id in children and map(lambda x: (x, offset+1), reversed(children[id]))) or [] |
238 index, msg = messages[id] |
238 index, msg = messages[id] |
239 ui.write(' '*offset + ('%d: ' % index) + msg['Subject'] + '\n') |
239 ui.write(' '*offset + ('%d: ' % index) + msg['Subject'] + '\n') |
240 ui.write('-'*70 + '\n') |
240 ui.write('-'*70 + '\n') |
241 |
241 |
242 def _pretty_list(lst): |
242 def _pretty_list(lst): |
243 s = '' |
243 s = '' |
244 for i in lst: |
244 for i in lst: |
245 s += i + ', ' |
245 s += i + ', ' |
246 return s[:-2] |
246 return s[:-2] |
247 |
247 |
248 def _random_id(): |
248 def _random_id(): |
249 return "%x" % random.randint(2**63, 2**64-1) |
249 return "%x" % random.randint(2**63, 2**64-1) |
250 |
250 |
251 |
251 |
252 cmdtable = { |
252 cmdtable = { |
253 'ilist': (ilist, |
253 'ilist': (ilist, |
254 [('a', 'all', False, |
254 [('a', 'all', False, |
255 'list all issues (by default only those with state new)'), |
255 'list all issues (by default only those with state new)'), |
256 ('p', 'property', [], |
256 ('p', 'property', [], |
257 'list issues with specific field values (e.g., -p state=fixed)'), |
257 'list issues with specific field values (e.g., -p state=fixed)'), |
258 ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'), |
258 ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'), |
259 ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (issues_dir, filter_prefix))], |
259 ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (issues_dir, filter_prefix))], |
260 _('hg ilist [OPTIONS]')), |
260 _('hg ilist [OPTIONS]')), |
261 'iadd': (iadd, |
261 'iadd': (iadd, |
262 [], |
262 [], |
263 _('hg iadd [ID] [COMMENT]')), |
263 _('hg iadd [ID] [COMMENT]')), |
264 'ishow': (ishow, |
264 'ishow': (ishow, |
265 [('a', 'all', None, 'list all comments')], |
265 [('a', 'all', None, 'list all comments')], |
266 _('hg ishow [OPTIONS] ID [COMMENT]')), |
266 _('hg ishow [OPTIONS] ID [COMMENT]')), |
267 'iupdate': (iupdate, |
267 'iupdate': (iupdate, |
268 [('p', 'property', [], |
268 [('p', 'property', [], |
269 'update properties (e.g., -p state=fixed)'), |
269 'update properties (e.g., -p state=fixed)'), |
270 ('n', 'no-property-comment', None, |
270 ('n', 'no-property-comment', None, |
271 'do not add a comment about changed properties')], |
271 'do not add a comment about changed properties')], |
272 _('hg iupdate [OPTIONS] ID')) |
272 _('hg iupdate [OPTIONS] ID')) |
273 } |
273 } |