artemis.py
changeset 16 49645303d045
parent 15 e5b03298394e
child 17 f70d7e98eb21
equal deleted inserted replaced
15:e5b03298394e 16:49645303d045
    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 }