OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 | |
3 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. | |
4 # Use of this source code is governed by a BSD-style license that can be | |
5 # found in the LICENSE file. | |
6 | |
7 # For Spreadsheets: | |
8 try: | |
9 from xml.etree import ElementTree | |
10 except ImportError: | |
11 from elementtree import ElementTree | |
12 import gdata.spreadsheet.service | |
13 import gdata.service | |
14 import atom.service | |
15 import gdata.spreadsheet | |
16 import atom | |
17 | |
18 # For Issue Tracker: | |
19 import gdata.projecthosting.client | |
20 import gdata.projecthosting.data | |
21 import gdata.gauth | |
22 import gdata.client | |
23 import gdata.data | |
24 import atom.http_core | |
25 import atom.core | |
26 | |
27 # For this script: | |
28 import getpass | |
29 from optparse import OptionParser | |
30 import pickle | |
31 from sets import Set | |
32 | |
33 # Settings | |
34 credentials_store = 'creds.dat' | |
35 | |
36 class Merger(object): | |
37 def __init__(self, ss_key, ss_ws_key, tracker_message, tracker_project, | |
38 debug, pretend): | |
39 self.ss_key = ss_key | |
40 self.ss_ws_key = ss_ws_key | |
41 self.tracker_message = tracker_message | |
42 self.tracker_project = tracker_project | |
43 self.debug_enabled = debug | |
44 self.pretend = pretend | |
45 self.user_agent = 'adlr-tracker-spreadsheet-merger' | |
46 self.it_keys = ['id', 'owner', 'status', 'title'] | |
47 | |
48 def debug(self, message): | |
49 """Prints message if debug mode is set.""" | |
50 if self.debug_enabled: | |
51 print message | |
52 | |
53 def print_feed(self, feed): | |
54 'Handy for debugging' | |
55 for i, entry in enumerate(feed.entry): | |
56 print 'id:', entry.id | |
57 if isinstance(feed, gdata.spreadsheet.SpreadsheetsCellsFeed): | |
58 print '%s %s\n' % (entry.title.text, entry.content.text) | |
59 elif isinstance(feed, gdata.spreadsheet.SpreadsheetsListFeed): | |
60 print '%s %s %s' % (i, entry.title.text, entry.content.text) | |
61 # Print this row's value for each column (the custom dictionary is | |
62 # built using the gsx: elements in the entry.) | |
63 print 'Contents:' | |
64 for key in entry.custom: | |
65 print ' %s: %s' % (key, entry.custom[key].text) | |
66 print '\n', | |
67 else: | |
68 print '%s %s\n' % (i, entry.title.text) | |
69 | |
70 | |
71 def tracker_login(self): | |
72 """Logs user into Tracker, using cached credentials if possible. | |
73 Saves credentials after login.""" | |
74 self.it_client = gdata.projecthosting.client.ProjectHostingClient() | |
75 self.it_client.source = self.user_agent | |
76 | |
77 self.load_creds() | |
78 | |
79 if self.tracker_token and self.tracker_user: | |
80 print 'Using existing credential for tracker login' | |
81 self.it_client.auth_token = self.tracker_token | |
82 else: | |
83 self.tracker_user = raw_input('Issue Tracker Login:') | |
84 password = getpass.getpass('Password:') | |
85 self.it_client.ClientLogin(self.tracker_user, password, | |
86 source=self.user_agent, service='code', | |
87 account_type='GOOGLE') | |
88 self.tracker_token = self.it_client.auth_token | |
89 self.store_creds() | |
90 | |
91 def spreadsheet_login(self): | |
92 """Logs user into Google Spreadsheets, using cached credentials if possible. | |
93 Saves credentials after login.""" | |
94 self.gd_client = gdata.spreadsheet.service.SpreadsheetsService() | |
95 self.gd_client.source = self.user_agent | |
96 | |
97 self.load_creds() | |
98 if self.docs_token: | |
99 print 'Using existing credential for docs login' | |
100 self.gd_client.SetClientLoginToken(self.docs_token) | |
101 else: | |
102 self.gd_client.email = raw_input('Google Docs Login:') | |
103 self.gd_client.password = getpass.getpass('Password:') | |
104 self.gd_client.ProgrammaticLogin() | |
105 self.docs_token = self.gd_client.GetClientLoginToken() | |
106 self.store_creds() | |
107 | |
108 def fetch_spreadsheet_issues(self): | |
109 """Fetches all issues from the user-specified spreadsheet. Returns | |
110 them as an array or dictionaries.""" | |
111 feed = self.gd_client.GetListFeed(self.ss_key, self.ss_ws_key) | |
112 issues = [] | |
113 for entry in feed.entry: | |
114 issue = {} | |
115 for key in entry.custom: | |
116 issue[key] = entry.custom[key].text | |
117 issue['__raw_entry'] = entry | |
118 issues.append(issue) | |
119 return issues | |
120 | |
121 def ids_for_spreadsheet_issues(self, ss_issues): | |
122 """Returns a Set of strings, each string an id from ss_issues""" | |
123 ret = Set() | |
124 for ss_issue in ss_issues: | |
125 ret.add(ss_issue['id']) | |
126 return ret | |
127 | |
128 def tracker_issues_for_query_feed(self, feed): | |
129 """Converts a feed object from a query to a list of tracker issue | |
130 dictionaries.""" | |
131 issues = [] | |
132 for issue in feed.entry: | |
133 issue_dict = {} | |
134 issue_dict['labels'] = [label.text for label in issue.label] | |
135 issue_dict['id'] = issue.id.text.split('/')[-1] | |
136 issue_dict['title'] = issue.title.text | |
137 issue_dict['status'] = issue.status.text | |
138 if issue.owner: | |
139 issue_dict['owner'] = issue.owner.username.text | |
140 issues.append(issue_dict) | |
141 return issues | |
142 | |
143 def fetch_tracker_issues(self, ss_issues): | |
144 """Fetches all relevant issues from traacker and returns them as an array | |
145 of dictionaries. Relevance is: | |
146 - has an ID that's in ss_issues, OR | |
147 - (is Area=Installer AND status is open). | |
148 Open status is one of: Unconfirmed, Untriaged, Available, Assigned, | |
149 Started, Upstream""" | |
150 issues = [] | |
151 got_results = True | |
152 index = 1 | |
153 while got_results: | |
154 query = gdata.projecthosting.client.Query(label='Area-Installer', | |
155 max_results=50, | |
156 start_index=index) | |
157 feed = self.it_client.get_issues('chromium-os', query=query) | |
158 if not feed.entry: | |
159 got_results = False | |
160 index = index + len(feed.entry) | |
161 issues.extend(self.tracker_issues_for_query_feed(feed)) | |
162 # Now, remove issues that are open or in ss_issues. | |
163 ss_ids = self.ids_for_spreadsheet_issues(ss_issues) | |
164 open_statuses = ['Unconfirmed', 'Untriaged', 'Available', 'Assigned', | |
165 'Started', 'Upstream'] | |
166 new_issues = [] | |
167 for issue in issues: | |
168 if issue['status'] in open_statuses or issue['id'] in ss_ids: | |
169 new_issues.append(issue) | |
170 # Remove id from ss_ids, if it's there | |
171 ss_ids.discard(issue['id']) | |
172 issues = new_issues | |
173 | |
174 # Now, for each ss_id that didn't turn up in the query, explicitly add it | |
175 for id_ in ss_ids: | |
176 query = gdata.projecthosting.client.Query(issue_id=id_, | |
177 max_results=50, | |
178 start_index=index) | |
179 feed = self.it_client.get_issues('chromium-os', query=query) | |
180 if not feed.entry: | |
181 print 'No result for id', id_ | |
182 continue | |
183 issues.extend(self.tracker_issues_for_query_feed(feed)) | |
184 | |
185 return issues | |
186 | |
187 def store_creds(self): | |
188 """Stores login credentials to disk.""" | |
189 obj = {} | |
190 if self.docs_token: | |
191 obj['docs_token'] = self.docs_token | |
192 if self.tracker_token: | |
193 obj['tracker_token'] = self.tracker_token | |
194 if self.tracker_user: | |
195 obj['tracker_user'] = self.tracker_user | |
196 try: | |
197 f = open(credentials_store, 'w') | |
198 pickle.dump(obj, f) | |
199 f.close() | |
200 except IOError: | |
201 print 'Unable to store credentials' | |
202 | |
203 def load_creds(self): | |
204 """Loads login credentials from disk.""" | |
205 self.docs_token = None | |
206 self.tracker_token = None | |
207 self.tracker_user = None | |
208 try: | |
209 f = open(credentials_store, 'r') | |
210 obj = pickle.load(f) | |
211 f.close() | |
212 if obj.has_key('docs_token'): | |
213 self.docs_token = obj['docs_token'] | |
214 if obj.has_key('tracker_token'): | |
215 self.tracker_token = obj['tracker_token'] | |
216 if obj.has_key('tracker_user'): | |
217 self.tracker_user = obj['tracker_user'] | |
218 except IOError: | |
219 print 'Unable to load credentials' | |
220 | |
221 def browse(self): | |
222 """Browses Spreadsheets to help the user find the spreadsheet and | |
223 worksheet keys""" | |
224 print 'Browsing spreadsheets...' | |
225 | |
226 if self.ss_key and self.ss_ws_key: | |
227 print 'You already passed in --ss_key and --ss_ws_key. No need to browse.' | |
228 return | |
229 | |
230 print 'Logging in...' | |
231 self.spreadsheet_login() | |
232 | |
233 if not self.ss_key: | |
234 print 'Fetching spreadsheets...' | |
235 feed = self.gd_client.GetSpreadsheetsFeed() | |
236 print '' | |
237 print 'Spreadsheet key - Title' | |
238 for entry in feed.entry: | |
239 key = entry.id.text.split('/')[-1] | |
240 title = entry.title.text | |
241 print '"%s" - "%s"' % (key, title) | |
242 print '' | |
243 print 'Done. Rerun with --ss_key=KEY to browse a list of worksheet keys.' | |
244 else: | |
245 print 'Fetching worksheets for spreadsheet', self.ss_key | |
246 feed = self.gd_client.GetWorksheetsFeed(self.ss_key) | |
247 for entry in feed.entry: | |
248 key = entry.id.text.split('/')[-1] | |
249 title = entry.title.text | |
250 print '' | |
251 print 'Worksheet key - Title' | |
252 print '"%s" - "%s"' % (key, title) | |
253 print '' | |
254 print 'Done. You now have keys for --ss_key and --ss_ws_key.' | |
255 | |
256 def tracker_issue_for_id(self, issues, id_): | |
257 """Returns the element of issues which has id_ for the key 'id'""" | |
258 for issue in issues: | |
259 if issue['id'] == id_: | |
260 return issue | |
261 return None | |
262 | |
263 def spreadsheet_issue_to_tracker_dict(self, ss_issue): | |
264 """Converts a spreadsheet issue to the dict format that is used to | |
265 represent a tracker issue.""" | |
266 ret = {} | |
267 ret['project'] = self.tracker_project | |
268 ret['title'] = ss_issue['title'] | |
269 ret['summary'] = self.tracker_message | |
270 ret['owner'] = ss_issue['owner'] | |
271 if ss_issue.get('status') is not None: | |
272 ret['status'] = ss_issue['status'] | |
273 ret['labels'] = [] | |
274 for (key, value) in ss_issue.items(): | |
275 if key.endswith('-') and (value is not None): | |
276 ret['labels'].append(key.title() + value) | |
277 return ret | |
278 | |
279 def label_from_prefix(self, prefix, corpus): | |
280 """Given a corpus (array of lable strings), return the first label | |
281 that begins with the specified prefix.""" | |
282 for label in corpus: | |
283 if label.startswith(prefix): | |
284 return label | |
285 return None | |
286 | |
287 def update_spreadsheet_issue_to_tracker_dict(self, ss_issue, t_issue): | |
288 """Updates a given tracker issue with data from the spreadsheet issue.""" | |
289 ret = {} | |
290 ret['title'] = ss_issue['title'] | |
291 ret['id'] = ss_issue['id'] | |
292 ret['summary'] = self.tracker_message | |
293 if ss_issue['status'] != t_issue['status']: | |
294 ret['status'] = ss_issue['status'] | |
295 | |
296 if ss_issue.get('owner'): | |
297 if (not t_issue.has_key('owner')) or \ | |
298 (ss_issue['owner'] != t_issue['owner']): | |
299 ret['owner'] = ss_issue['owner'] | |
300 # labels | |
301 ret['labels'] = [] | |
302 for (key, value) in ss_issue.items(): | |
303 caps_key = key.title() | |
304 if not caps_key.endswith('-'): | |
305 continue | |
306 ss_label = None | |
307 if value: | |
308 ss_label = caps_key + value.title() | |
309 t_label = self.label_from_prefix(caps_key, t_issue['labels']) | |
310 | |
311 if t_label is None and ss_label is None: | |
312 # Nothing | |
313 continue | |
314 | |
315 if (t_label is not None) and \ | |
316 ((ss_label is None) or (ss_label != t_label)): | |
317 ret['labels'].append('-' + t_label) | |
318 | |
319 if (ss_label is not None) and \ | |
320 ((t_label is None) or (t_label != ss_label)): | |
321 ret['labels'].append(ss_label) | |
322 return ret | |
323 | |
324 def tracker_issue_has_changed(self, t_issue, ss_issue): | |
325 """Returns True iff ss_issue indicates changes in t_issue that need to be | |
326 committed up to the Issue Tracker.""" | |
327 if t_issue is None: | |
328 return True | |
329 potential_commit = \ | |
330 self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue) | |
331 | |
332 if potential_commit.has_key('status') or \ | |
333 potential_commit.has_key('owner') or \ | |
334 (len(potential_commit['labels']) > 0): | |
335 return True | |
336 if potential_commit['title'] != t_issue['title']: | |
337 return True | |
338 return False | |
339 | |
340 def spreadsheet_to_tracker_commits(self, ss_issues, t_issues): | |
341 """Given the current state of all spreadsheet issues and tracker issues, | |
342 returns a list of all commits that need to go to tracker to get it in | |
343 line with the spreadsheet.""" | |
344 ret = [] | |
345 for ss_issue in ss_issues: | |
346 t_issue = self.tracker_issue_for_id(t_issues, ss_issue['id']) | |
347 commit = {} | |
348 # TODO see if an update is needed at all | |
349 if t_issue is None: | |
350 commit['type'] = 'append' | |
351 commit['dict'] = self.spreadsheet_issue_to_tracker_dict(ss_issue) | |
352 commit['__ss_issue'] = ss_issue | |
353 else: | |
354 if not self.tracker_issue_has_changed(t_issue, ss_issue): | |
355 continue | |
356 commit['type'] = 'update' | |
357 commit['dict'] = \ | |
358 self.update_spreadsheet_issue_to_tracker_dict(ss_issue, t_issue) | |
359 ret.append(commit) | |
360 return ret | |
361 | |
362 def fetch_issues(self): | |
363 """Logs into Docs/Tracker, and fetches spreadsheet and tracker issues""" | |
364 print 'Logging into Docs...' | |
365 self.spreadsheet_login() | |
366 print 'Logging into Tracker...' | |
367 self.tracker_login() | |
368 | |
369 print 'Fetching spreadsheet issues...' | |
370 ss_issues = self.fetch_spreadsheet_issues() | |
371 self.debug('Spreadsheet issues: %s' % ss_issues) | |
372 print 'Fetching tracker issues...' | |
373 t_issues = self.fetch_tracker_issues(ss_issues) | |
374 self.debug('Tracker issues: %s' % t_issues) | |
375 return (t_issues, ss_issues) | |
376 | |
377 def spreadsheet_to_tracker(self): | |
378 """High-level function to manage migrating data from the spreadsheet | |
379 to Tracker.""" | |
380 (t_issues, ss_issues) = self.fetch_issues() | |
381 print 'Calculating deltas...' | |
382 commits = self.spreadsheet_to_tracker_commits(ss_issues, t_issues) | |
383 self.debug('got commits: %s' % commits) | |
384 if not commits: | |
385 print 'No deltas. Done.' | |
386 return | |
387 | |
388 for commit in commits: | |
389 dic = commit['dict'] | |
390 labels = dic.get('labels') | |
391 owner = dic.get('owner') | |
392 status = dic.get('status') | |
393 | |
394 if commit['type'] == 'append': | |
395 print 'Creating new tracker issue...' | |
396 if self.pretend: | |
397 print '(Skipping because --pretend is set)' | |
398 continue | |
399 created = self.it_client.add_issue(self.tracker_project, | |
400 dic['title'], | |
401 self.tracker_message, | |
402 self.tracker_user, | |
403 labels=labels, | |
404 owner=owner, | |
405 status=status) | |
406 issue_id = created.id.text.split('/')[-1] | |
407 print 'Created issue with id:', issue_id | |
408 print 'Write id back to spreadsheet row...' | |
409 raw_entry = commit['__ss_issue']['__raw_entry'] | |
410 ss_issue = commit['__ss_issue'] | |
411 del ss_issue['__raw_entry'] | |
412 ss_issue.update({'id': issue_id}) | |
413 self.gd_client.UpdateRow(raw_entry, ss_issue) | |
414 print 'Done.' | |
415 else: | |
416 print 'Updating issue with id:', dic['id'] | |
417 if self.pretend: | |
418 print '(Skipping because --pretend is set)' | |
419 continue | |
420 self.it_client.update_issue(self.tracker_project, | |
421 dic['id'], | |
422 self.tracker_user, | |
423 comment=self.tracker_message, | |
424 status=status, | |
425 owner=owner, | |
426 labels=labels) | |
427 print 'Done.' | |
428 | |
429 def spreadsheet_issue_for_id(self, issues, id_): | |
430 """Given the array of spreadsheet issues, return the first one that | |
431 has id_ for the key 'id'.""" | |
432 for issue in issues: | |
433 if issue['id'] == id_: | |
434 return issue | |
435 return None | |
436 | |
437 def value_for_key_in_labels(self, label_array, prefix): | |
438 """Given an array of labels and a prefix, return the non-prefix part | |
439 of the first label that has that prefix. E.g. if label_array is | |
440 ["Mstone-R7", "Area-Installer"] and prefix is "Area-", returns | |
441 "Installer".""" | |
442 for label in label_array: | |
443 if label.startswith(prefix): | |
444 return label[len(prefix):] | |
445 return None | |
446 | |
447 def tracker_issue_to_spreadsheet_issue(self, t_issue, ss_keys): | |
448 """Converts a tracker issue to the format used by spreadsheet, given | |
449 the row headings ss_keys.""" | |
450 new_row = {} | |
451 for key in ss_keys: | |
452 if key.endswith('-'): | |
453 # label | |
454 new_row[key] = self.value_for_key_in_labels(t_issue['labels'], | |
455 key.title()) | |
456 # Special cases | |
457 if key in self.it_keys and key in t_issue: | |
458 new_row[key] = t_issue[key] | |
459 return new_row | |
460 | |
461 def spreadsheet_row_needs_update(self, ss_issue, t_issue): | |
462 """Returns True iff the spreadsheet issue passed in needs to be updated | |
463 to match data in the tracker issue.""" | |
464 new_ss_issue = self.tracker_issue_to_spreadsheet_issue(t_issue, | |
465 ss_issue.keys()) | |
466 for key in new_ss_issue.keys(): | |
467 if not ss_issue.has_key(key): | |
468 continue | |
469 if new_ss_issue[key] != ss_issue[key]: | |
470 return True | |
471 return False | |
472 | |
473 def tracker_to_spreadsheet_commits(self, t_issues, ss_issues): | |
474 """Given the current set of spreadsheet and tracker issues, computes | |
475 commits needed to go to Spreadsheets to get the spreadsheet in line | |
476 with what's in Tracker.""" | |
477 ret = [] | |
478 keys = ss_issues[0].keys() | |
479 for t_issue in t_issues: | |
480 commit = {} | |
481 ss_issue = self.spreadsheet_issue_for_id(ss_issues, t_issue['id']) | |
482 if ss_issue is None: | |
483 # New issue | |
484 commit['new_row'] = self.tracker_issue_to_spreadsheet_issue(t_issue, | |
485 keys) | |
486 commit['type'] = 'append' | |
487 elif self.spreadsheet_row_needs_update(ss_issue, t_issue): | |
488 commit['__raw_entry'] = ss_issue['__raw_entry'] | |
489 del ss_issue['__raw_entry'] | |
490 ss_issue.update(self.tracker_issue_to_spreadsheet_issue(t_issue, keys)) | |
491 commit['dict'] = ss_issue | |
492 commit['type'] = 'update' | |
493 else: | |
494 continue | |
495 ret.append(commit) | |
496 return ret | |
497 | |
498 def tracker_to_spreadsheet(self): | |
499 """High-level function to migrate data from Tracker to the spreadsheet.""" | |
500 (t_issues, ss_issues) = self.fetch_issues() | |
501 if len(ss_issues) == 0: | |
502 raise Exception('Error: must have at least one non-header row in '\ | |
503 'spreadsheet') | |
504 return | |
505 ss_keys = ss_issues[0].keys() | |
506 | |
507 print 'Calculating deltas...' | |
508 ss_commits = self.tracker_to_spreadsheet_commits(t_issues, ss_issues) | |
509 self.debug('commits: %s' % ss_commits) | |
510 if not ss_commits: | |
511 print 'Nothing to commit.' | |
512 return | |
513 print 'Committing...' | |
514 for commit in ss_commits: | |
515 self.debug('Operating on commit: %s' % commit) | |
516 if commit['type'] == 'append': | |
517 print 'Appending new row...' | |
518 if not self.pretend: | |
519 self.gd_client.InsertRow(commit['new_row'], | |
520 self.ss_key, self.ss_ws_key) | |
521 else: | |
522 print '(Skipped because --pretend set)' | |
523 if commit['type'] == 'update': | |
524 print 'Updating row...' | |
525 if not self.pretend: | |
526 self.gd_client.UpdateRow(commit['__raw_entry'], commit['dict']) | |
527 else: | |
528 print '(Skipped because --pretend set)' | |
529 print 'Done.' | |
530 | |
531 def main(): | |
532 class PureEpilogOptionParser(OptionParser): | |
533 def format_epilog(self, formatter): | |
534 return self.epilog | |
535 | |
536 parser = PureEpilogOptionParser() | |
537 parser.add_option('-a', '--action', dest='action', metavar='ACTION', | |
538 help='Action to perform') | |
539 parser.add_option('-d', '--debug', action='store_true', dest='debug', | |
540 default=False, help='Print debug output.') | |
541 parser.add_option('-m', '--message', dest='message', metavar='TEXT', | |
542 help='Log message when updating Tracker issues') | |
543 parser.add_option('-p', '--pretend', action='store_true', dest='pretend', | |
544 default=False, help="Don't commit anything.") | |
545 parser.add_option('--ss_key', dest='ss_key', metavar='KEY', | |
546 help='Spreadsheets key (find with browse action)') | |
547 parser.add_option('--ss_ws_key', dest='ss_ws_key', metavar='KEY', | |
548 help='Spreadsheets worksheet key (find with browse action)') | |
549 parser.add_option('--tracker_project', dest='tracker_project', | |
550 metavar='PROJECT', | |
551 help='Tracker project (default: chromium-os)', | |
552 default='chromium-os') | |
553 parser.epilog = """Actions: | |
554 browse -- browse spreadsheets to find spreadsheet and worksheet keys. | |
555 ss_to_t -- for each entry in spreadsheet, apply its values to tracker. | |
556 If no ID is in the spreadsheet row, a new tracker item is created | |
557 and the spreadsheet is updated. | |
558 t_to_ss -- for each tracker entry, apply it or add it to the spreadsheet. | |
559 | |
560 | |
561 This script can be used to migrate Issue Tracker issues between Issue Tracker | |
562 and Google Spreadsheets. The spreadsheet should have certain columns in any | |
563 order: Id, Owner, Title, Status. The spreadsheet may have any label of the | |
564 form 'Key-'. For those labels that end in '-', this script assumes the cell | |
565 value and the header form a label that should be applied to the issue. E.g. | |
566 if the spredsheet has a column named 'Mstone-' and a cell under it called | |
567 'R8' that corresponds to the label 'Mstone-R8' in Issue Tracker. | |
568 | |
569 To migrate data, you must choose on each invocation of this script if you | |
570 wish to migrate data from Issue Tracker to a spreadsheet of vice-versa. | |
571 | |
572 When migrating from Tracker, all found issues based on the query | |
573 (which is currently hard-coded to "label=Area-Installer") will be inserted | |
574 into the spreadsheet (overwritng existing cells if a row with matching ID | |
575 is found). Custom columns in the spreadsheet won't be overwritten, so if | |
576 the spreadsheet contains extra columns about issues (e.g. time estimates) | |
577 they will be preserved. | |
578 | |
579 When migrating from spreadsheet to Tracker, each row in the spreadsheet | |
580 is compared to existing tracker issues that match the query | |
581 (which is currently hard-coded to "label=Area-Installer"). If the | |
582 spreadsheet row has no Id, a new Issue Tracker issue is created and the new | |
583 Id is written back to the spreadsheet. If an existing tracker issue exists, | |
584 it's updated with the data from the spreadsheet if anything has changed. | |
585 | |
586 Suggested usage: | |
587 - Create a spreadsheet with columns Id, Owner, Title, Status, and any label | |
588 prefixes as desired. | |
589 - Run this script with '-b' to browse your spreadsheet and get the | |
590 spreadsheet key. | |
591 - Run this script again with '-b' and the spreadsheet key to get the | |
592 worksheet key. | |
593 - Run this script with "-a t_to_ss" or "-a ss_to_t" to migrate data in either | |
594 direction. | |
595 | |
596 Known issues: | |
597 - query is currently hardcoded to label=Area-Installer. That should be | |
598 a command-line flag. | |
599 - When creating a new issue on tracker, the owner field isn't set. I (adlr) | |
600 am not sure why. Workaround: If you rerun this script, tho, it will detect | |
601 a delta and update the tracker issue with the owner, which seems to succeed. | |
602 """ | |
603 | |
604 (options, args) = parser.parse_args() | |
605 | |
606 merger = Merger(options.ss_key, options.ss_ws_key, | |
607 options.message, options.tracker_project, | |
608 options.debug, options.pretend) | |
609 if options.action == 'browse': | |
610 merger.browse() | |
611 elif options.action == 'ss_to_t': | |
612 if not options.message: | |
613 print 'Error: when updating tracker, -m MESSAGE required.' | |
614 return | |
615 merger.spreadsheet_to_tracker() | |
616 elif options.action == 't_to_ss': | |
617 merger.tracker_to_spreadsheet() | |
618 else: | |
619 raise Exception('Unknown action requested.') | |
620 | |
621 if __name__ == '__main__': | |
622 main() | |
OLD | NEW |