This repository is private.
All pages are served over SSL and all pushing and pulling is done over SSH.
No one may fork, clone, or view it unless they are added as a member.
Every repository with this icon (
) is private.
Every repository with this icon (
This repository is public.
Anyone may fork, clone, or view it.
Every repository with this icon (
) is public.
Every repository with this icon (
git-issues / git-issues
| 5f44f6e0 » | jwiegley | 2008-05-12 | 1 | #!/usr/bin/env python | |
| badd2f0f » | jwiegley | 2008-05-12 | 2 | # coding: utf-8 | |
| 5f44f6e0 » | jwiegley | 2008-05-12 | 3 | ||
| 9561b193 » | jwiegley | 2008-05-12 | 4 | # git-issue, version 0.3 | |
| 5f44f6e0 » | jwiegley | 2008-05-12 | 5 | # | |
| 6 | # by John Wiegley <johnw@newartisans.com> | ||||
| 7 | |||||
| badd2f0f » | jwiegley | 2008-05-12 | 8 | # TODO: (until I can add these bugs to the repo itself!) | |
| 9 | # | ||||
| 10 | # 1. use utf-8 throughout | ||||
| 11 | # 2. use -z flag for ls-tree | ||||
| 12 | # 3. use UTC throughout | ||||
| 13 | |||||
| bcd9ce29 » | edrik | 2008-08-20 | 14 | import platform | |
| 15 | |||||
| c0607c42 » | Giulio Eulisse | 2008-05-22 | 16 | from os.path import split, join, exists, dirname | |
| bcd9ce29 » | edrik | 2008-08-20 | 17 | from os import getcwd, makedirs, execv | |
| c0607c42 » | Giulio Eulisse | 2008-05-22 | 18 | from shutil import copy | |
| 19 | from sys import argv | ||||
| 20 | |||||
| bcd9ce29 » | edrik | 2008-08-20 | 21 | if platform.system() == "Windows": | |
| d10f3556 » | sbohrer | 2008-06-30 | 22 | resolvedLink = None | |
| bcd9ce29 » | edrik | 2008-08-20 | 23 | else: | |
| 24 | from os import readlink | ||||
| 25 | try: | ||||
| 26 | resolvedLink = readlink(__file__) | ||||
| 27 | except: | ||||
| 28 | resolvedLink = None | ||||
| 29 | |||||
| 30 | if resolvedLink and resolvedLink[0] != "/": | ||||
| 31 | resolvedLink = join(dirname(__file__), resolvedLink) | ||||
| 32 | if resolvedLink: | ||||
| 33 | #print "Symlink found, using %s instead" % resolvedLink | ||||
| 34 | execv(resolvedLink, [resolvedLink] + argv[1:]) | ||||
| c0607c42 » | Giulio Eulisse | 2008-05-22 | 35 | ||
| 36 | path = getcwd() | ||||
| 37 | |||||
| bcd9ce29 » | edrik | 2008-08-20 | 38 | if ".gitissues" not in __file__: | |
| c0607c42 » | Giulio Eulisse | 2008-05-22 | 39 | while not exists(join(path,".gitissues")): | |
| 40 | path,extra = split(path) | ||||
| 41 | if not extra: | ||||
| 42 | break | ||||
| 43 | issuesExec = join(path,".gitissues/git-issues") | ||||
| 44 | if exists(issuesExec): | ||||
| 45 | #print "git-issues found in %s. Using it in place of the one in %s" % (issuesExec, __file__) | ||||
| 46 | execv(issuesExec, [issuesExec]+ argv[1:]) | ||||
| 47 | assert ("This should never be called" and False) | ||||
| e5a93a93 » | sbohrer | 2008-06-29 | 48 | ||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 49 | import sys | |
| 50 | import os | ||||
| 51 | import re | ||||
| 52 | import optparse | ||||
| 53 | |||||
| f3268125 » | jwiegley | 2008-05-14 | 54 | import gitshelve | |
| 55 | |||||
| badd2f0f » | jwiegley | 2008-05-12 | 56 | try: | |
| 57 | from cStringIO import StringIO | ||||
| 58 | except: | ||||
| 59 | from StringIO import StringIO | ||||
| 60 | |||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 61 | import cPickle | |
| 62 | |||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 63 | from datetime import datetime | |
| 64 | from subprocess import Popen, PIPE | ||||
| 47f1c6f9 » | ktf | 2008-06-02 | 65 | from os.path import isdir, isfile, join, basename | |
| d0bafd0b » | ktf | 2008-06-03 | 66 | from tempfile import mkstemp | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 67 | ###################################################################### | |
| 68 | |||||
| badd2f0f » | jwiegley | 2008-05-12 | 69 | iso_fmt = "%Y%m%dT%H%M%S" | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 70 | options = None | |
| 18f58232 » | jwiegley | 2008-05-17 | 71 | cache_version = 11 | |
| 5f44f6e0 » | jwiegley | 2008-05-12 | 72 | ||
| 73 | ###################################################################### | ||||
| 74 | |||||
| cab44d22 » | jwiegley | 2008-05-14 | 75 | # You may wonder what dirtiness means below. Here's the deal: | |
| 76 | # | ||||
| 77 | # An object is "self-dirty" if it itself has been changed, but possibly none | ||||
| 78 | # of its children. An object is "dirty" if its children have been changed, | ||||
| 79 | # but possibly not itself. Any dirtiness is cause for rewriting the object | ||||
| 80 | # cache; only self-dirtiness of specific objects will cause the repository to | ||||
| 81 | # be updated. | ||||
| 82 | |||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 83 | class Person: | |
| 84 | def __init__(self, name, email): | ||||
| 85 | self.name = name | ||||
| 86 | self.email = email | ||||
| 87 | |||||
| 88 | def __str__(self): | ||||
| 89 | return "%s <%s>" % (self.name, self.email) | ||||
| 90 | |||||
| cab44d22 » | jwiegley | 2008-05-14 | 91 | class Comment: | |
| 92 | def __init__(self, issue, author, comment): | ||||
| a71d32ca » | ktf | 2008-06-22 | 93 | self.name = None | |
| cab44d22 » | jwiegley | 2008-05-14 | 94 | self.issue = issue | |
| 95 | self.author = author | ||||
| 96 | self.comment = comment | ||||
| 97 | self.created = datetime.now() | ||||
| 98 | self.modified = None | ||||
| 99 | self.self_dirty = True | ||||
| 100 | self.attachments = [] # records filename and blob | ||||
| a71d32ca » | ktf | 2008-06-22 | 101 | self.issue.comments[self.get_name()] = self # register into issue | |
| cab44d22 » | jwiegley | 2008-05-14 | 102 | ||
| 103 | def mark_dirty(self): | ||||
| 104 | self.modified = datetime.now() | ||||
| 105 | self.self_dirty = True | ||||
| f3268125 » | jwiegley | 2008-05-14 | 106 | self.issue.mark_dirty(self_dirty = False) | |
| cab44d22 » | jwiegley | 2008-05-14 | 107 | ||
| 108 | def __getstate__(self): | ||||
| 109 | odict = self.__dict__.copy() # copy the dict since we change it | ||||
| 110 | del odict['self_dirty'] # remove self dirty flag | ||||
| 111 | return odict | ||||
| 112 | |||||
| a71d32ca » | ktf | 2008-06-22 | 113 | def __setstate__(self, dict): | |
| cab44d22 » | jwiegley | 2008-05-14 | 114 | self.__dict__.update(dict) # update attributes | |
| 115 | self.self_dirty = False | ||||
| 116 | |||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 117 | class Issue: | |
| badd2f0f » | jwiegley | 2008-05-12 | 118 | def __init__(self, issueSet, author, title, | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 119 | summary = None, | |
| 120 | description = None, | ||||
| 121 | reporters = [], | ||||
| 122 | owners = [], | ||||
| 123 | assigned = None, | ||||
| 124 | carbons = [], | ||||
| 125 | status = "new", | ||||
| 126 | resolution = None, | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 127 | issue_type = "defect", | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 128 | components = [], | |
| 129 | version = None, | ||||
| 130 | milestone = None, | ||||
| 131 | severity = "major", | ||||
| badd2f0f » | jwiegley | 2008-05-12 | 132 | priority = "medium", | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 133 | tags = []): | |
| badd2f0f » | jwiegley | 2008-05-12 | 134 | self.issueSet = issueSet | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 135 | self.name = None | |
| 136 | self.author = author | ||||
| 137 | self.title = title | ||||
| 138 | self.summary = summary | ||||
| 139 | self.description = description | ||||
| 140 | self.reporters = reporters | ||||
| 141 | self.owners = owners | ||||
| 142 | self.assigned = assigned | ||||
| 143 | self.carbons = carbons | ||||
| 144 | self.status = status | ||||
| 145 | self.resolution = resolution | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 146 | self.issue_type = issue_type | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 147 | self.components = components | |
| 148 | self.version = version | ||||
| 149 | self.milestone = milestone | ||||
| 150 | self.severity = severity | ||||
| 151 | self.priority = priority | ||||
| 152 | self.tags = tags | ||||
| badd2f0f » | jwiegley | 2008-05-12 | 153 | self.created = datetime.now() | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 154 | self.modified = None | |
| 9561b193 » | jwiegley | 2008-05-12 | 155 | self.changes = {} | |
| cab44d22 » | jwiegley | 2008-05-14 | 156 | self.dirty = False | |
| 157 | self.self_dirty = True | ||||
| 158 | self.comments = {} | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 159 | ||
| cab44d22 » | jwiegley | 2008-05-14 | 160 | def mark_dirty(self, self_dirty): | |
| 161 | self.dirty = True | ||||
| 162 | if self_dirty: | ||||
| 163 | self.self_dirty = True | ||||
| 164 | self.modified = datetime.now() | ||||
| f3268125 » | jwiegley | 2008-05-14 | 165 | self.issueSet.mark_dirty(self_dirty = False) | |
| 9561b193 » | jwiegley | 2008-05-12 | 166 | ||
| 3bb55535 » | jwiegley | 2008-05-14 | 167 | def get_name(self): | |
| 168 | assert False | ||||
| 169 | |||||
| 9561b193 » | jwiegley | 2008-05-12 | 170 | def note_change(self, field, before, after): | |
| 171 | if self.changes.has_key(field): | ||||
| 172 | data = self.changes[field] | ||||
| 173 | data[1] = after | ||||
| 174 | else: | ||||
| 175 | data = [before, after] | ||||
| 176 | self.changes[field] = data | ||||
| 177 | |||||
| 178 | def set_author(self, author): | ||||
| 179 | self.note_change('author', self.author, author) | ||||
| 180 | self.author = author | ||||
| 181 | |||||
| 182 | def set_title(self, title): | ||||
| 183 | self.note_change('title', self.title, title) | ||||
| 184 | self.title = title | ||||
| 185 | |||||
| 186 | def set_summary(self, summary): | ||||
| 187 | self.note_change('summary', self.summary, summary) | ||||
| 188 | self.summary = summary | ||||
| 189 | |||||
| 190 | def set_description(self, description): | ||||
| 191 | self.note_change('description', self.description, description) | ||||
| 192 | self.description = description | ||||
| 193 | |||||
| 194 | def set_reporters(self, reporters): | ||||
| 195 | self.note_change('reporters', self.reporters, reporters) | ||||
| 196 | self.reporters = reporters | ||||
| 197 | |||||
| 198 | def set_owners(self, owners): | ||||
| 199 | self.note_change('owners', self.owners, owners) | ||||
| 200 | self.owners = owners | ||||
| 201 | |||||
| 202 | def set_assigned(self, assigned): | ||||
| 203 | self.note_change('assigned', self.assigned, assigned) | ||||
| 204 | self.assigned = assigned | ||||
| 205 | |||||
| 206 | def set_carbons(self, carbons): | ||||
| 207 | self.note_change('carbons', self.carbons, carbons) | ||||
| 208 | self.carbons = carbons | ||||
| 209 | |||||
| 210 | def set_status(self, status): | ||||
| 211 | self.note_change('status', self.status, status) | ||||
| 212 | self.status = status | ||||
| 213 | |||||
| 214 | def set_resolution(self, resolution): | ||||
| 215 | self.note_change('resolution', self.resolution, resolution) | ||||
| 216 | self.resolution = resolution | ||||
| 217 | |||||
| 218 | def set_issue_type(self, issue_type): | ||||
| 219 | self.note_change('type', self.issue_type, issue_type) | ||||
| 220 | self.issue_type = issue_type | ||||
| 221 | |||||
| 222 | def set_components(self, components): | ||||
| 223 | self.note_change('components', self.components, components) | ||||
| 224 | self.components = components | ||||
| 225 | |||||
| 226 | def set_version(self, version): | ||||
| 227 | self.note_change('version', self.version, version) | ||||
| 228 | self.version = version | ||||
| 229 | |||||
| 230 | def set_milestone(self, milestone): | ||||
| 231 | self.note_change('milestone', self.milestone, milestone) | ||||
| 232 | self.milestone = milestone | ||||
| 233 | |||||
| 234 | def set_severity(self, severity): | ||||
| 235 | self.note_change('severity', self.severity, severity) | ||||
| 236 | self.severity = severity | ||||
| 237 | |||||
| 238 | def set_priority(self, priority): | ||||
| 239 | self.note_change('priority', self.priority, priority) | ||||
| 240 | self.priority = priority | ||||
| 241 | |||||
| 242 | def set_tags(self, tags): | ||||
| 243 | self.note_change('tags', self.tags, tags) | ||||
| 244 | self.tags = tags | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 245 | ||
| 9561b193 » | jwiegley | 2008-05-12 | 246 | def __getstate__(self): | |
| 247 | odict = self.__dict__.copy() # copy the dict since we change it | ||||
| 248 | del odict['changes'] # remove change log | ||||
| 249 | del odict['dirty'] # remove dirty flag | ||||
| cab44d22 » | jwiegley | 2008-05-14 | 250 | del odict['self_dirty'] # remove self dirty flag | |
| 9561b193 » | jwiegley | 2008-05-12 | 251 | return odict | |
| 252 | |||||
| 253 | def __setstate__(self,dict): | ||||
| 254 | self.__dict__.update(dict) # update attributes | ||||
| 255 | self.changes = {} | ||||
| cab44d22 » | jwiegley | 2008-05-14 | 256 | self.dirty = False | |
| 257 | self.self_dirty = False | ||||
| dc1b7f66 » | jwiegley | 2008-05-12 | 258 | ||
| bda8dbc9 » | jwiegley | 2008-05-12 | 259 | class IssueSet: | |
| 260 | """An IssueSet refers to a group of issues. There is always at least one | ||||
| 261 | IssueSet that refers to all of the issues which exist in a repository. | ||||
| 262 | Other IssueSet's can be generated from that one as "views" or queries into | ||||
| 263 | that data. | ||||
| 264 | |||||
| 265 | In essence, it contains both a set of Issue's which can be looked up by | ||||
| 266 | their unique identifier, and also certain global definition, like the | ||||
| 267 | allowable components, etc.""" | ||||
| f3268125 » | jwiegley | 2008-05-14 | 268 | def __init__(self, shelf): | |
| 269 | self.shelf = shelf | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 270 | self.statuses = [] | |
| 271 | self.resolutions = [] | ||||
| 272 | self.issue_types = [] | ||||
| 273 | self.components = [] | ||||
| 274 | self.versions = [] | ||||
| 275 | self.milestones = [] | ||||
| 276 | self.severities = [] | ||||
| 277 | self.priorities = [] | ||||
| 278 | self.dirty = False | ||||
| cab44d22 » | jwiegley | 2008-05-14 | 279 | self.self_dirty = True | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 280 | self.cache_version = cache_version | |
| 9561b193 » | jwiegley | 2008-05-12 | 281 | self.created = datetime.now() | |
| cab44d22 » | jwiegley | 2008-05-14 | 282 | self.modified = None | |
| 283 | |||||
| 284 | def mark_dirty(self, self_dirty): | ||||
| 285 | self.dirty = True | ||||
| 286 | if self_dirty: | ||||
| 287 | self.modified = datetime.now() | ||||
| 288 | self.self_dirty = True | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 289 | ||
| f3268125 » | jwiegley | 2008-05-14 | 290 | def current_author(self): | |
| badd2f0f » | jwiegley | 2008-05-12 | 291 | assert False | |
| 292 | |||||
| f3268125 » | jwiegley | 2008-05-14 | 293 | def allocate_issue(self, title): | |
| badd2f0f » | jwiegley | 2008-05-12 | 294 | assert False | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 295 | ||
| f3268125 » | jwiegley | 2008-05-14 | 296 | def new_issue(self, title): | |
| 297 | issue = self.allocate_issue(title) | ||||
| 298 | self.add_issue(issue) | ||||
| 299 | return issue | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 300 | ||
| 1efa9acd » | ktf | 2008-06-20 | 301 | def new_comment(self, issue, text): | |
| 302 | comment = self.allocate_comment(issue, text) | ||||
| 303 | self.add_comment(comment) | ||||
| 304 | return comment | ||||
| 305 | |||||
| 306 | def comment_path(self, comment): | ||||
| 307 | name = comment.issue.get_name() | ||||
| 258379ab » | ktf | 2008-06-22 | 308 | return "%s/%s/comment_%s_%s_%s.xml" %(name[:2], | |
| 1efa9acd » | ktf | 2008-06-20 | 309 | name[2:], | |
| 258379ab » | ktf | 2008-06-22 | 310 | comment.name, | |
| 1efa9acd » | ktf | 2008-06-20 | 311 | datetime.now().isoformat(), | |
| 312 | comment.comment) | ||||
| f3268125 » | jwiegley | 2008-05-14 | 313 | def issue_path(self, issue): | |
| 3bb55535 » | jwiegley | 2008-05-14 | 314 | name = issue.get_name() | |
| 315 | return '%s/%s/issue.xml' % (name[:2], name[2:]) | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 316 | ||
| 9561b193 » | jwiegley | 2008-05-12 | 317 | def add_issue(self, issue): | |
| f3268125 » | jwiegley | 2008-05-14 | 318 | self.shelf[self.issue_path(issue)] = issue | |
| 319 | self.mark_dirty(self_dirty = False) | ||||
| 320 | |||||
| 1efa9acd » | ktf | 2008-06-20 | 321 | def add_comment(self, comment): | |
| 322 | self.shelf[self.comment_path(comment)] = comment | ||||
| 323 | self.mark_dirty(self_dirty = False) | ||||
| 324 | |||||
| a71d32ca » | ktf | 2008-06-22 | 325 | def get_comment(self, idx_or_partial_hash): | |
| 326 | comment = None | ||||
| 327 | try: | ||||
| 328 | idx = int(idx_or_partial_hash) - 1 | ||||
| 329 | comment = self.shelf[self.shelf.keys()[idx]] | ||||
| 330 | except: | ||||
| 258379ab » | ktf | 2008-06-22 | 331 | print [key for key in self.shelf.iterkeys()] | |
| 332 | def getCommentId(x): | ||||
| 333 | if not 'comment_' in x: | ||||
| 334 | return "" | ||||
| 335 | x = x.split('comment_')[1] | ||||
| 336 | x = x.split('_')[0] | ||||
| 337 | return x | ||||
| 338 | clean = lambda x: getCommentId(x) | ||||
| a71d32ca » | ktf | 2008-06-22 | 339 | matching = [(clean(key), key) for key in self.shelf.iterkeys() | |
| 340 | if clean(key).startswith(idx_or_partial_hash) and not "issue.xml" in clean(key)] | ||||
| 341 | if len(matching) == 0: | ||||
| 342 | pass | ||||
| 343 | elif len(matching) == 1: | ||||
| 344 | comment = self.shelf[matching[0][1]] | ||||
| 345 | else: | ||||
| 346 | print ("Ambiguous hash matches:\n" + | ||||
| 347 | '\t\n'.join(a[0] for a in matching)) | ||||
| 348 | if not comment: | ||||
| 349 | raise Exception("There is no issue matching the identifier '%s'.\n" % | ||||
| 350 | idx_or_partial_hash) | ||||
| 351 | return comment | ||||
| 352 | |||||
| f3268125 » | jwiegley | 2008-05-14 | 353 | def __getitem__(self, idx_or_partial_hash): | |
| 354 | issue = None | ||||
| 57f66a2b » | Toby Moore | 2008-05-15 | 355 | try: | |
| 356 | idx = int(idx_or_partial_hash) - 1 | ||||
| 357 | issue = self.shelf[self.shelf.keys()[idx]] | ||||
| 358 | except: | ||||
| d602dfb9 » | jwiegley | 2008-05-17 | 359 | clean = lambda x: x.replace('issue.xml', '').replace('/','') | |
| 360 | matching = [(clean(key), key) for key in self.shelf.iterkeys() | ||||
| a71d32ca » | ktf | 2008-06-22 | 361 | if clean(key).startswith(idx_or_partial_hash) and not "comment" in clean(key)] | |
| d602dfb9 » | jwiegley | 2008-05-17 | 362 | if len(matching) == 0: | |
| 363 | pass | ||||
| 364 | elif len(matching) == 1: | ||||
| 365 | issue = self.shelf[matching[0][1]] | ||||
| 366 | else: | ||||
| 367 | print ("Ambiguous hash matches:\n" + | ||||
| 368 | '\t\n'.join(a[0] for a in matching)) | ||||
| f3268125 » | jwiegley | 2008-05-14 | 369 | ||
| 370 | if not issue: | ||||
| d602dfb9 » | jwiegley | 2008-05-17 | 371 | raise Exception("There is no issue matching the identifier '%s'.\n" % | |
| 372 | idx_or_partial_hash) | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 373 | ||
| f3268125 » | jwiegley | 2008-05-14 | 374 | return issue | |
| 375 | |||||
| 376 | def __delitem__(self, idx_or_partial_hash): | ||||
| 377 | del self.shelf[None] # jww (2008-05-14): NYI | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 378 | assert False | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 379 | ||
| 380 | def issues_cache_file(self): | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 381 | assert False | |
| 382 | |||||
| f3268125 » | jwiegley | 2008-05-14 | 383 | def load_state(self): | |
| 9561b193 » | jwiegley | 2008-05-12 | 384 | """Given a newly created IssueSet object as a template, see if we can | |
| 385 | restore the cached version of the data from disk, and then check whether | ||||
| 386 | it's still valid. This can _greatly_ speed up subsequent list and show | ||||
| 387 | operations. | ||||
| 388 | |||||
| 389 | The reason why a newly created template exists is to abstract | ||||
| 390 | DVCS-specific behavior, such as the location of the cache file. | ||||
| 391 | |||||
| 392 | Thus, a typical session looks like this: | ||||
| dc1b7f66 » | jwiegley | 2008-05-12 | 393 | ||
| f3268125 » | jwiegley | 2008-05-14 | 394 | issueSet = GitIssueSet() | |
| 9561b193 » | jwiegley | 2008-05-12 | 395 | ||
| 396 | if ... looking at issues list is required ...: | ||||
| f3268125 » | jwiegley | 2008-05-14 | 397 | issueSet = issueSet.load_state() | |
| 9561b193 » | jwiegley | 2008-05-12 | 398 | ... use the issue data ...""" | |
| f3268125 » | jwiegley | 2008-05-14 | 399 | cache_file = self.issues_cache_file() | |
| 9561b193 » | jwiegley | 2008-05-12 | 400 | if isfile(cache_file): | |
| 401 | fd = open(cache_file, 'rb') | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 402 | if options.verbose: | |
| 9561b193 » | jwiegley | 2008-05-12 | 403 | print "Cache: Loading saved issues data" | |
| 404 | try: | ||||
| 405 | cachedIssueSet = cPickle.load(fd) | ||||
| 406 | finally: | ||||
| 407 | fd.close() | ||||
| 408 | |||||
| f3268125 » | jwiegley | 2008-05-14 | 409 | if cachedIssueSet.cache_version == self.cache_version: | |
| 9561b193 » | jwiegley | 2008-05-12 | 410 | if options.verbose: | |
| 411 | print "Cache: It is valid and usable" | ||||
| 412 | return cachedIssueSet | ||||
| dc1b7f66 » | jwiegley | 2008-05-12 | 413 | ||
| 9561b193 » | jwiegley | 2008-05-12 | 414 | if options.verbose: | |
| 415 | print "Cache: No longer valid, throwing it away" | ||||
| dc1b7f66 » | jwiegley | 2008-05-12 | 416 | ||
| cab44d22 » | jwiegley | 2008-05-14 | 417 | # We can't use or rely on the cache, so read all details from disk and | |
| 418 | # then mark the IssueSet dirty so that it gets saved back again when | ||||
| 419 | # we exit. | ||||
| f3268125 » | jwiegley | 2008-05-14 | 420 | try: | |
| 421 | return object_from_string(self.shelf['project.xml']) | ||||
| 422 | except: | ||||
| 423 | return self | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 424 | ||
| 9561b193 » | jwiegley | 2008-05-12 | 425 | def save_state(self): | |
| 426 | """Write an IssueSet to disk in object form, for fast loading on the next | ||||
| 427 | iteration. This is only done if there are actual changes to write.""" | ||||
| f3268125 » | jwiegley | 2008-05-14 | 428 | if not self.dirty: | |
| 429 | return | ||||
| 430 | |||||
| 431 | self.shelf.sync() | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 432 | ||
| f3268125 » | jwiegley | 2008-05-14 | 433 | cache_file = self.issues_cache_file() | |
| 434 | cache_file_dir = os.path.dirname(cache_file) | ||||
| 435 | |||||
| 436 | if not isdir(cache_file_dir): | ||||
| 437 | os.makedirs(cache_file_dir) | ||||
| 438 | |||||
| 439 | fd = open(cache_file, 'wb') | ||||
| 440 | try: | ||||
| 441 | cPickle.dump(issueSet, fd) | ||||
| 442 | finally: | ||||
| 443 | fd.close() | ||||
| 444 | |||||
| 445 | self.dirty = False | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 446 | ||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 447 | ###################################################################### | |
| 448 | |||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 449 | import xml.dom.minidom | |
| 450 | |||||
| 451 | def read_object(obj, file_descriptor): | ||||
| 452 | return XmlReader.read(file_descriptor) | ||||
| 453 | |||||
| 454 | def object_from_string(str): | ||||
| 455 | return XmlReader.readString(str) | ||||
| 456 | |||||
| 457 | class XmlReader: | ||||
| 458 | def read(cls, fd): | ||||
| 459 | doc = xml.dom.minidom.parse(fd) | ||||
| 460 | data = XmlRipper.rip(doc.firstChild) | ||||
| 461 | doc.unlink() | ||||
| 462 | return data | ||||
| 463 | |||||
| 464 | read = classmethod(read) | ||||
| 465 | |||||
| 466 | def readString(cls, data): | ||||
| 467 | doc = xml.dom.minidom.parseString(data) | ||||
| 468 | data = XmlRipper.rip(doc.firstChild) | ||||
| 469 | doc.unlink() | ||||
| 470 | return data | ||||
| 471 | |||||
| 472 | readString = classmethod(readString) | ||||
| 473 | |||||
| 474 | class XmlStringRipper: | ||||
| 475 | def rip(cls, node): | ||||
| dd629ab5 » | jwiegley | 2008-05-14 | 476 | return node.data[1:-1] | |
| dc95c9f3 » | jwiegley | 2008-05-13 | 477 | ||
| 478 | rip = classmethod(rip) | ||||
| 479 | |||||
| 480 | class XmlListRipper: | ||||
| 481 | def rip(cls, node): | ||||
| 482 | assert False | ||||
| 483 | |||||
| 484 | rip = classmethod(rip) | ||||
| 485 | |||||
| 486 | class XmlDateTimeRipper: | ||||
| 487 | def rip(cls, node): | ||||
| dd629ab5 » | jwiegley | 2008-05-14 | 488 | return datetime.strptime(node.childNodes[0].data[1:-1], iso_fmt) | |
| dc95c9f3 » | jwiegley | 2008-05-13 | 489 | ||
| 490 | rip = classmethod(rip) | ||||
| 491 | |||||
| 492 | class XmlPersonRipper: | ||||
| 493 | def rip(cls, node): | ||||
| dd629ab5 » | jwiegley | 2008-05-14 | 494 | person = Person(node.childNodes[1].childNodes[0].data[1:-1], | |
| 495 | node.childNodes[3].childNodes[0].data[1:-1]) | ||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 496 | return person | |
| 497 | |||||
| 498 | rip = classmethod(rip) | ||||
| 499 | |||||
| 500 | class XmlIssueRipper: | ||||
| 501 | def rip(cls, node): | ||||
| dd629ab5 » | jwiegley | 2008-05-14 | 502 | created = XmlRipper.rip(node.childNodes[1].childNodes[1]) | |
| 503 | author = XmlRipper.rip(node.childNodes[3].childNodes[1]) | ||||
| 504 | title = XmlRipper.rip(node.childNodes[5].firstChild) | ||||
| 505 | |||||
| 506 | issue = Issue(None, author, title) | ||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 507 | issue.created = created | |
| cab44d22 » | jwiegley | 2008-05-14 | 508 | issue.dirty = False | |
| dc95c9f3 » | jwiegley | 2008-05-13 | 509 | ||
| 510 | return issue | ||||
| 511 | |||||
| 512 | rip = classmethod(rip) | ||||
| 513 | |||||
| 514 | class XmlIssueSetRipper: | ||||
| 515 | pass | ||||
| 516 | |||||
| 517 | class XmlRipper: | ||||
| 518 | def rip(cls, node): | ||||
| dd629ab5 » | jwiegley | 2008-05-14 | 519 | if node.nodeType == xml.dom.minidom.Node.TEXT_NODE: | |
| dc95c9f3 » | jwiegley | 2008-05-13 | 520 | return XmlStringRipper.rip(node) | |
| 521 | elif node.nodeName == 'datetime': | ||||
| 522 | return XmlDateTimeRipper.rip(node) | ||||
| 523 | elif node.nodeName == 'person': | ||||
| 524 | return XmlPersonRipper.rip(node) | ||||
| 525 | elif node.nodeName == 'list': | ||||
| 526 | return XmlListRipper.rip(node) | ||||
| 527 | elif node.nodeName == 'issue': | ||||
| 528 | return XmlIssueRipper.rip(node) | ||||
| 529 | elif node.nodeName == 'issue-set': | ||||
| 530 | return XmlIssueSetRipper.rip(node) | ||||
| 531 | else: | ||||
| dd629ab5 » | jwiegley | 2008-05-14 | 532 | print node.nodeType | |
| 533 | print node.nodeName | ||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 534 | assert False | |
| 535 | |||||
| 536 | rip = classmethod(rip) | ||||
| 537 | |||||
| 538 | ###################################################################### | ||||
| 539 | |||||
| dd629ab5 » | jwiegley | 2008-05-14 | 540 | def write_object(obj, file_descriptor = sys.stdout): | |
| dc95c9f3 » | jwiegley | 2008-05-13 | 541 | XmlWriter.write(XmlBuilder.build(obj), fd = file_descriptor) | |
| 542 | |||||
| 543 | def object_to_string(obj): | ||||
| 544 | buffer = StringIO() | ||||
| 545 | XmlWriter.write(XmlBuilder.build(obj), fd = buffer) | ||||
| 546 | return buffer.getvalue() | ||||
| 547 | |||||
| 548 | class XmlWriter: | ||||
| 549 | def write(cls, doc, no_header = False, fd = sys.stdout): | ||||
| 550 | if no_header: | ||||
| 551 | buffer = StringIO() | ||||
| 552 | buffer.write(doc.toprettyxml(indent = "", encoding = "utf-8")) | ||||
| 553 | fd.write(re.sub('^.+\n', '', buffer.getvalue())) | ||||
| 554 | else: | ||||
| 555 | fd.write(doc.toprettyxml(indent = "", encoding = "utf-8")) | ||||
| 556 | doc.unlink() | ||||
| 557 | |||||
| 558 | write = classmethod(write) | ||||
| 559 | |||||
| 560 | class XmlStringBuilder: | ||||
| 561 | def build(cls, data, node, doc): | ||||
| 562 | node.appendChild(doc.createTextNode(data)) | ||||
| 563 | |||||
| 564 | build = classmethod(build) | ||||
| 565 | |||||
| 566 | class XmlListBuilder: | ||||
| 567 | def build(cls, data, node, doc): | ||||
| 568 | element = doc.createElement("list") | ||||
| 569 | for child in data: | ||||
| 570 | XmlBuilder.build(doc, element, child) | ||||
| 571 | node.appendChild(element) | ||||
| 572 | |||||
| 573 | build = classmethod(build) | ||||
| 574 | |||||
| 575 | class XmlDateTimeBuilder: | ||||
| 576 | def build(cls, data, node, doc): | ||||
| 577 | element = doc.createElement("datetime") | ||||
| 578 | element.appendChild(doc.createTextNode(data.strftime(iso_fmt))) | ||||
| 579 | node.appendChild(element) | ||||
| 580 | |||||
| 581 | build = classmethod(build) | ||||
| 582 | |||||
| 583 | class XmlPersonBuilder: | ||||
| 584 | def build(cls, data, node, doc): | ||||
| 585 | person = doc.createElement("person") | ||||
| 586 | |||||
| 587 | name = doc.createElement("name") | ||||
| 588 | name.appendChild(doc.createTextNode(data.name)) | ||||
| 589 | person.appendChild(name) | ||||
| 590 | |||||
| 591 | email = doc.createElement("email") | ||||
| 592 | email.appendChild(doc.createTextNode(data.email)) | ||||
| 593 | person.appendChild(email) | ||||
| 594 | |||||
| 595 | node.appendChild(person) | ||||
| 596 | |||||
| 597 | build = classmethod(build) | ||||
| 598 | |||||
| 599 | class XmlIssueBuilder: | ||||
| 4d9fb480 » | jwiegley | 2008-05-13 | 600 | def build(cls, issue, node, doc): | |
| dc95c9f3 » | jwiegley | 2008-05-13 | 601 | issueNode = doc.createElement("issue") | |
| 602 | |||||
| 603 | created = doc.createElement("created") | ||||
| 604 | XmlBuilder.build(issue.created, created, doc) | ||||
| 605 | issueNode.appendChild(created) | ||||
| 606 | |||||
| 607 | author = doc.createElement("author") | ||||
| 608 | XmlBuilder.build(issue.author, author, doc) | ||||
| 609 | issueNode.appendChild(author) | ||||
| 610 | |||||
| 611 | title = doc.createElement("title") | ||||
| 612 | XmlBuilder.build(issue.title, title, doc) | ||||
| 613 | issueNode.appendChild(title) | ||||
| 614 | |||||
| dd629ab5 » | jwiegley | 2008-05-14 | 615 | summary = doc.createElement("summary") | |
| 616 | XmlBuilder.build(issue.summary, summary, doc) | ||||
| 617 | issueNode.appendChild(summary) | ||||
| 618 | |||||
| 619 | description = doc.createElement("description") | ||||
| 620 | XmlBuilder.build(issue.description, description, doc) | ||||
| 621 | issueNode.appendChild(description) | ||||
| 622 | |||||
| 623 | reporters = doc.createElement("reporters") | ||||
| 624 | XmlBuilder.build(issue.reporters, reporters, doc) | ||||
| 625 | issueNode.appendChild(reporters) | ||||
| 626 | |||||
| 627 | owners = doc.createElement("owners") | ||||
| 628 | XmlBuilder.build(issue.owners, owners, doc) | ||||
| 629 | issueNode.appendChild(owners) | ||||
| 630 | |||||
| 631 | assigned = doc.createElement("assigned") | ||||
| 632 | XmlBuilder.build(issue.assigned, assigned, doc) | ||||
| 633 | issueNode.appendChild(assigned) | ||||
| 634 | |||||
| 635 | carbons = doc.createElement("carbons") | ||||
| 636 | XmlBuilder.build(issue.carbons, carbons, doc) | ||||
| 637 | issueNode.appendChild(carbons) | ||||
| 638 | |||||
| 639 | status = doc.createElement("status") | ||||
| 640 | XmlBuilder.build(issue.status, status, doc) | ||||
| 641 | issueNode.appendChild(status) | ||||
| 642 | |||||
| 643 | resolution = doc.createElement("resolution") | ||||
| 644 | XmlBuilder.build(issue.resolution, resolution, doc) | ||||
| 645 | issueNode.appendChild(resolution) | ||||
| 646 | |||||
| 647 | issue_type = doc.createElement("type") | ||||
| 648 | XmlBuilder.build(issue.issue_type, issue_type, doc) | ||||
| 649 | issueNode.appendChild(issue_type) | ||||
| 650 | |||||
| 651 | components = doc.createElement("components") | ||||
| 652 | XmlBuilder.build(issue.components, components, doc) | ||||
| 653 | issueNode.appendChild(components) | ||||
| 654 | |||||
| 655 | version = doc.createElement("version") | ||||
| 656 | XmlBuilder.build(issue.version, version, doc) | ||||
| 657 | issueNode.appendChild(version) | ||||
| 658 | |||||
| 659 | milestone = doc.createElement("milestone") | ||||
| 660 | XmlBuilder.build(issue.milestone, milestone, doc) | ||||
| 661 | issueNode.appendChild(milestone) | ||||
| 662 | |||||
| 663 | severity = doc.createElement("severity") | ||||
| 664 | XmlBuilder.build(issue.severity, severity, doc) | ||||
| 665 | issueNode.appendChild(severity) | ||||
| 666 | |||||
| 667 | priority = doc.createElement("priority") | ||||
| 668 | XmlBuilder.build(issue.priority, priority, doc) | ||||
| 669 | issueNode.appendChild(priority) | ||||
| 670 | |||||
| 671 | tags = doc.createElement("tags") | ||||
| 672 | XmlBuilder.build(issue.tags, tags, doc) | ||||
| 673 | issueNode.appendChild(tags) | ||||
| 674 | |||||
| 675 | modified = doc.createElement("modified") | ||||
| 676 | XmlBuilder.build(issue.modified, modified, doc) | ||||
| 677 | issueNode.appendChild(modified) | ||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 678 | ||
| 679 | node.appendChild(issueNode) | ||||
| 680 | |||||
| 681 | build = classmethod(build) | ||||
| 682 | |||||
| a71d32ca » | ktf | 2008-06-22 | 683 | class XmlCommentBuilder: | |
| 684 | def build(cls, comment, node, doc): | ||||
| 685 | commentNode = doc.createElement("comment") | ||||
| 686 | |||||
| 687 | created = doc.createElement("created") | ||||
| 688 | XmlBuilder.build(comment.created, created, doc) | ||||
| 689 | commentNode.appendChild(created) | ||||
| 690 | |||||
| 691 | author = doc.createElement("author") | ||||
| 692 | XmlBuilder.build(comment.author, author, doc) | ||||
| 693 | commentNode.appendChild(author) | ||||
| 694 | |||||
| 695 | commentText = doc.createElement("comment") | ||||
| 696 | XmlBuilder.build(comment.comment, commentText, doc) | ||||
| 697 | commentNode.appendChild(commentText) | ||||
| 698 | node.appendChild(commentNode) | ||||
| 699 | |||||
| 700 | build = classmethod(build) | ||||
| 701 | |||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 702 | #class XmlIssueChangesBuilder: | |
| 703 | # def build(cls, data, node, doc): | ||||
| 704 | # changes = doc.createElement("changes") | ||||
| 705 | # doc.appendChild(changes) | ||||
| 706 | # | ||||
| 707 | # for field_name in self.changes.keys(): | ||||
| 708 | # field = doc.createElement("field") | ||||
| 709 | # field.setAttribute("name", field_name) | ||||
| 710 | # | ||||
| 711 | # data = self.changes[field_name] | ||||
| 712 | # | ||||
| 713 | # before = doc.createElement("before") | ||||
| 714 | # XmlBuilder.build(data[0], before, doc) | ||||
| 715 | # field.appendChild(before) | ||||
| 716 | # | ||||
| 717 | # after = doc.createElement("after") | ||||
| 718 | # XmlBuilder.build(data[1], after, doc) | ||||
| 719 | # field.appendChild(after) | ||||
| 720 | # | ||||
| 721 | # changes.appendChild(field) | ||||
| 722 | # | ||||
| 723 | # node.appendChild(changes) | ||||
| 724 | # | ||||
| 725 | # build = classmethod(build) | ||||
| 726 | |||||
| 727 | class XmlIssueSetBuilder: | ||||
| 4d9fb480 » | jwiegley | 2008-05-13 | 728 | def build(cls, issueSet, node, doc): | |
| dc95c9f3 » | jwiegley | 2008-05-13 | 729 | set = doc.createElement("issue-set") | |
| 730 | |||||
| 731 | created = doc.createElement("created") | ||||
| 732 | XmlBuilder.build(issueSet.created, created, doc) | ||||
| 733 | set.appendChild(created) | ||||
| 734 | |||||
| dd629ab5 » | jwiegley | 2008-05-14 | 735 | statuses = doc.createElement("statuses") | |
| 736 | XmlBuilder.build(issueSet.statuses, statuses, doc) | ||||
| 737 | set.appendChild(statuses) | ||||
| 738 | |||||
| 739 | resolutions = doc.createElement("resolutions") | ||||
| 740 | XmlBuilder.build(issueSet.resolutions, resolutions, doc) | ||||
| 741 | set.appendChild(resolutions) | ||||
| 742 | |||||
| 743 | issue_types = doc.createElement("types") | ||||
| 744 | XmlBuilder.build(issueSet.issue_types, issue_types, doc) | ||||
| 745 | set.appendChild(issue_types) | ||||
| 746 | |||||
| 747 | components = doc.createElement("components") | ||||
| 748 | XmlBuilder.build(issueSet.components, components, doc) | ||||
| 749 | set.appendChild(components) | ||||
| 750 | |||||
| 751 | versions = doc.createElement("versions") | ||||
| 752 | XmlBuilder.build(issueSet.versions, versions, doc) | ||||
| 753 | set.appendChild(versions) | ||||
| 754 | |||||
| 755 | milestones = doc.createElement("milestones") | ||||
| 756 | XmlBuilder.build(issueSet.milestones, milestones, doc) | ||||
| 757 | set.appendChild(milestones) | ||||
| 758 | |||||
| 759 | severities = doc.createElement("severities") | ||||
| 760 | XmlBuilder.build(issueSet.severities, severities, doc) | ||||
| 761 | set.appendChild(severities) | ||||
| 762 | |||||
| 763 | priorities = doc.createElement("priorities") | ||||
| 764 | XmlBuilder.build(issueSet.priorities, priorities, doc) | ||||
| 765 | set.appendChild(priorities) | ||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 766 | ||
| cab44d22 » | jwiegley | 2008-05-14 | 767 | modified = doc.createElement("modified") | |
| 768 | XmlBuilder.build(issueSet.modified, modified, doc) | ||||
| 769 | set.appendChild(modified) | ||||
| 770 | |||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 771 | node.appendChild(set) | |
| 772 | |||||
| 773 | build = classmethod(build) | ||||
| 774 | |||||
| 775 | class XmlBuilder: | ||||
| 776 | def build(cls, data, node = None, doc = None): | ||||
| dd629ab5 » | jwiegley | 2008-05-14 | 777 | if data is None: | |
| 778 | pass | ||||
| 779 | elif isinstance(data, datetime): | ||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 780 | assert doc | |
| 781 | XmlDateTimeBuilder.build(data, node, doc) | ||||
| 782 | elif isinstance(data, Person): | ||||
| 783 | assert doc | ||||
| 784 | XmlPersonBuilder.build(data, node, doc) | ||||
| 785 | elif isinstance(data, list): | ||||
| 786 | assert doc | ||||
| 787 | XmlListBuilder.build(data, node, doc) | ||||
| 788 | elif isinstance(data, str): | ||||
| 789 | assert doc | ||||
| 790 | XmlStringBuilder.build(data, node, doc) | ||||
| 791 | elif isinstance(data, Issue): | ||||
| 792 | assert not doc | ||||
| 793 | doc = xml.dom.minidom.Document() | ||||
| 794 | XmlIssueBuilder.build(data, doc, doc) | ||||
| 795 | elif isinstance(data, IssueSet): | ||||
| 796 | assert not doc | ||||
| 797 | doc = xml.dom.minidom.Document() | ||||
| 798 | XmlIssueSetBuilder.build(data, doc, doc) | ||||
| a71d32ca » | ktf | 2008-06-22 | 799 | elif isinstance(data, Comment): | |
| 800 | assert not doc | ||||
| 801 | doc = xml.dom.minidom.Document() | ||||
| 802 | XmlCommentBuilder.build(data, doc, doc) | ||||
| dc95c9f3 » | jwiegley | 2008-05-13 | 803 | else: | |
| a71d32ca » | ktf | 2008-06-22 | 804 | print "Unknown type %s" % data | |
| dc95c9f3 » | jwiegley | 2008-05-13 | 805 | assert False | |
| 806 | |||||
| 807 | return doc | ||||
| 808 | |||||
| 809 | build = classmethod(build) | ||||
| 810 | |||||
| 811 | ###################################################################### | ||||
| 812 | |||||
| 3bb55535 » | jwiegley | 2008-05-14 | 813 | class GitIssue(Issue): | |
| 814 | def get_name(self): | ||||
| 815 | if not self.name: | ||||
| 816 | hash_func = self.issueSet.shelf.hash_blob | ||||
| 817 | name = hash_func(str(self.created) + str(self.author) + | ||||
| 818 | self.title) | ||||
| 819 | self.name = name | ||||
| 820 | return self.name | ||||
| 821 | |||||
| 1efa9acd » | ktf | 2008-06-20 | 822 | class GitComment(Comment): | |
| 823 | def get_name(self): | ||||
| 824 | if not self.name: | ||||
| 825 | hash_func = self.issue.issueSet.shelf.hash_blob | ||||
| a71d32ca » | ktf | 2008-06-22 | 826 | name = hash_func(str(self.created) | |
| 1efa9acd » | ktf | 2008-06-20 | 827 | + str(self.author) | |
| 828 | + self.comment) | ||||
| 829 | self.name = name | ||||
| 830 | return self.name | ||||
| 831 | |||||
| 3bb55535 » | jwiegley | 2008-05-14 | 832 | class xml_gitbook(gitshelve.gitbook): | |
| 31c9ebdc » | jwiegley | 2008-05-14 | 833 | def serialize_data(self, data): | |
| 834 | return object_to_string(data) | ||||
| b6d2d87d » | jwiegley | 2008-05-14 | 835 | ||
| f3268125 » | jwiegley | 2008-05-14 | 836 | def deserialize_data(self, data): | |
| 31c9ebdc » | jwiegley | 2008-05-14 | 837 | return object_from_string(data) | |
| b6d2d87d » | jwiegley | 2008-05-14 | 838 | ||
| bda8dbc9 » | jwiegley | 2008-05-12 | 839 | class GitIssueSet(IssueSet): | |
| 840 | """This object implements all the command necessary to interact with Git | ||||
| 841 | for the purpose of storing and distributing issues.""" | ||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 842 | def __init__(self): | |
| badd2f0f » | jwiegley | 2008-05-12 | 843 | self.GIT_DIR = None | |
| 844 | self.GIT_AUTHOR = None | ||||
| 3bb55535 » | jwiegley | 2008-05-14 | 845 | IssueSet.__init__(self, gitshelve.open('issues', | |
| 846 | book_type = xml_gitbook)) | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 847 | ||
| badd2f0f » | jwiegley | 2008-05-12 | 848 | def git_directory(self): | |
| 849 | if self.GIT_DIR is None: | ||||
| 19081aaf » | jwiegley | 2008-05-14 | 850 | self.GIT_DIR = gitshelve.git('rev-parse', '--git-dir') | |
| badd2f0f » | jwiegley | 2008-05-12 | 851 | return self.GIT_DIR | |
| 852 | |||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 853 | def issues_cache_file(self): | |
| badd2f0f » | jwiegley | 2008-05-12 | 854 | return join(self.git_directory(), "issues") | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 855 | ||
| badd2f0f » | jwiegley | 2008-05-12 | 856 | def current_author(self): | |
| 857 | if self.GIT_AUTHOR is None: | ||||
| 19081aaf » | jwiegley | 2008-05-14 | 858 | self.GIT_AUTHOR = Person(gitshelve.git('config', 'user.name'), | |
| 859 | gitshelve.git('config', 'user.email')) | ||||
| badd2f0f » | jwiegley | 2008-05-12 | 860 | return self.GIT_AUTHOR | |
| 861 | |||||
| f3268125 » | jwiegley | 2008-05-14 | 862 | def allocate_issue(self, title): | |
| 863 | return GitIssue(self, self.current_author(), title) | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 864 | ||
| 1efa9acd » | ktf | 2008-06-20 | 865 | def allocate_comment(self, issue, commentText): | |
| 866 | return GitComment(issue, self.current_author(), commentText) | ||||
| 867 | |||||
| f3268125 » | jwiegley | 2008-05-14 | 868 | ###################################################################### | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 869 | ||
| f3268125 » | jwiegley | 2008-05-14 | 870 | def format_long_text(text, indent = 13): | |
| 871 | if not text: | ||||
| 872 | return "<none>" | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 873 | ||
| d602dfb9 » | jwiegley | 2008-05-17 | 874 | lines = text.split('\n') | |
| dd629ab5 » | jwiegley | 2008-05-14 | 875 | ||
| f3268125 » | jwiegley | 2008-05-14 | 876 | buffer = StringIO() | |
| 877 | first = True | ||||
| 878 | for line in lines: | ||||
| 879 | if not first: | ||||
| 880 | buffer.write("\n%s" % (" " * indent)) | ||||
| 881 | else: | ||||
| 882 | first = False | ||||
| 883 | buffer.write(line) | ||||
| dd629ab5 » | jwiegley | 2008-05-14 | 884 | ||
| f3268125 » | jwiegley | 2008-05-14 | 885 | return buffer.getvalue() | |
| 5f44f6e0 » | jwiegley | 2008-05-12 | 886 | ||
| f3268125 » | jwiegley | 2008-05-14 | 887 | def format_people_list(people, indent = 13): | |
| 888 | if not people: | ||||
| 889 | return "<no one yet>" | ||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 890 | ||
| f3268125 » | jwiegley | 2008-05-14 | 891 | buffer = StringIO() | |
| 892 | first = True | ||||
| 893 | for person in people: | ||||
| 894 | if not first: | ||||
| 895 | buffer.write(",\n%s" % (" " * indent)) | ||||
| 896 | else: | ||||
| 897 | first = False | ||||
| 898 | buffer.write(person) | ||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 899 | ||
| f3268125 » | jwiegley | 2008-05-14 | 900 | return buffer.getvalue() | |
| 5f44f6e0 » | jwiegley | 2008-05-12 | 901 | ||
| 5be27b86 » | sbohrer | 2008-07-01 | 902 | def terminal_width(): | |
| 903 | """Return terminal width.""" | ||||
| 904 | width = 0 | ||||
| 905 | try: | ||||
| 906 | import struct, fcntl, termios | ||||
| 907 | s = struct.pack('HHHH', 0, 0, 0, 0) | ||||
| 908 | x = fcntl.ioctl(1, termios.TIOCGWINSZ, s) | ||||
| 909 | width = struct.unpack('HHHH', x)[1] | ||||
| 910 | except: | ||||
| 911 | pass | ||||
| 912 | if width <= 0: | ||||
| 913 | if os.environ.has_key("COLUMNS"): | ||||
| 914 | width = int(os.getenv("COLUMNS")) | ||||
| 915 | if width <= 0: | ||||
| 916 | width = 80 | ||||
| 917 | return width | ||||
| 918 | |||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 919 | ###################################################################### | |
| 920 | |||||
| e0788547 » | sbohrer | 2008-07-01 | 921 | parser = optparse.OptionParser(usage="""Usage: git-issues [options] <command> [command-options] | |
| 9814d6b7 » | Giulio Eulisse | 2008-05-22 | 922 | ||
| 923 | Commands: | ||||
| e0788547 » | sbohrer | 2008-07-01 | 924 | init Creates a copy of git-issues repository in .gitissues in the | |
| 925 | current git repository. | ||||
| 9814d6b7 » | Giulio Eulisse | 2008-05-22 | 926 | list Lists tickets for this repository | |
| 927 | new Creates a new ticket for this repository | ||||
| 928 | show/dump Shows the given ticket | ||||
| e0788547 » | sbohrer | 2008-07-01 | 929 | change Change options for the given ticket | |
| 930 | edit edit options for the given ticket in text editor | ||||
| 931 | comment Add a comment to the given ticket | ||||
| 932 | close Close the given ticket""") | ||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 933 | parser.add_option("-v", "--verbose", | |
| 934 | action = "store_true", | ||||
| 935 | dest = "verbose", | ||||
| 936 | default = False, | ||||
| 937 | help = "report activity options.verbosely") | ||||
| 938 | |||||
| 919b0a9e » | ktf | 2008-05-22 | 939 | parser.add_option("--print-new-bugs", | |
| 940 | action = "store_true", | ||||
| 941 | dest = "printNewBugs", | ||||
| 942 | default = False, | ||||
| 943 | help = "prints out a formatted string with the bug summary and id. Usuful for in editor usage.") | ||||
| 944 | |||||
| 401019fa » | ktf | 2008-05-27 | 945 | parser.add_option("--filter-status", | |
| 946 | dest="filterStatus", | ||||
| 947 | default="closed", | ||||
| 57281f8b » | ktf | 2008-09-12 | 948 | help = """do not print a issue if it is in one of | |
| 401019fa » | ktf | 2008-05-27 | 949 | the stati specified (column separated) by this option.""".replace("\n","")) | |
| 950 | |||||
| d352c9e8 » | ktf | 2008-09-12 | 951 | parser.add_option("--filter-tags", | |
| 952 | dest="filterTags", | ||||
| 953 | default="", | ||||
| 954 | help = """Prints only the issues with one of the following | ||||
| 955 | tags (column separated) associated to it.""") | ||||
| 956 | |||||
| 4aedf47c » | ktf | 2008-05-27 | 957 | parser.add_option("--screen-width", | |
| 958 | dest="screenWidth", | ||||
| 5be27b86 » | sbohrer | 2008-07-01 | 959 | default=terminal_width(), | |
| 4aedf47c » | ktf | 2008-05-27 | 960 | help = "Width of the terminal we are printing to.") | |
| 961 | |||||
| 81b89b3d » | ktf | 2008-06-01 | 962 | parser.add_option("--status", | |
| 963 | dest="status", | ||||
| 964 | default=None, | ||||
| 965 | metavar="STATUS", | ||||
| 966 | help="Set the status of the issue to STATUS when creating it.") | ||||
| 967 | |||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 968 | (options, args) = parser.parse_args() | |
| 969 | |||||
| f3268125 » | jwiegley | 2008-05-14 | 970 | gitshelve.verbose = options.verbose | |
| 9561b193 » | jwiegley | 2008-05-12 | 971 | ||
| 4aedf47c » | ktf | 2008-05-27 | 972 | ||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 973 | ###################################################################### | |
| 974 | |||||
| 4df5a51d » | ktf | 2008-06-20 | 975 | def inputFromEditor(originalText): | |
| d10f3556 » | sbohrer | 2008-06-30 | 976 | fd, tempFile = mkstemp() | |
| 977 | f = open(tempFile,"w") | ||||
| 978 | f.write(originalText or "") | ||||
| 979 | f.close() | ||||
| 980 | |||||
| 981 | defaultEditor = "vi" | ||||
| 982 | if platform.system() == "Windows": | ||||
| 983 | defaultEditor = "notepad" | ||||
| 984 | if os.environ.has_key("VISUAL"): | ||||
| 985 | defaultEditor = os.getenv("VISUAL") | ||||
| 986 | elif os.environ.has_key("EDITOR"): | ||||
| 987 | defualtEditor = os.getenv("EDITOR") | ||||
| 988 | editCommand = "%s %s" % (defaultEditor, tempFile) | ||||
| 989 | if os.system(editCommand) != 0: | ||||
| 990 | os.unlink(tempFile) | ||||
| 991 | print "Error while executing %s" % editCommand | ||||
| 992 | sys.exit(1) | ||||
| 993 | contents = open(tempFile).read() | ||||
| 994 | os.unlink(tempFile) | ||||
| 995 | return contents | ||||
| 4aedf47c » | ktf | 2008-05-27 | 996 | ||
| b6d2d87d » | jwiegley | 2008-05-14 | 997 | if __name__ == '__main__': | |
| 5f44f6e0 » | jwiegley | 2008-05-12 | 998 | ||
| b6d2d87d » | jwiegley | 2008-05-14 | 999 | if len(args) == 0: | |
| 9814d6b7 » | Giulio Eulisse | 2008-05-22 | 1000 | parser.print_help() | |
| b6d2d87d » | jwiegley | 2008-05-14 | 1001 | sys.exit(1) | |
| 1002 | |||||
| 1003 | command = args[0] | ||||
| 1004 | args = args[1:] | ||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 1005 | ||
| 9561b193 » | jwiegley | 2008-05-12 | 1006 | ###################################################################### | |
| 1007 | |||||
| 7eba73b6 » | jwiegley | 2008-07-31 | 1008 | # jww (2008-05-12): Pick the appropriate IssueSet to use based on the | |
| 1009 | # environment. | ||||
| bda8dbc9 » | jwiegley | 2008-05-12 | 1010 | ||
| f3268125 » | jwiegley | 2008-05-14 | 1011 | issueSet = GitIssueSet().load_state() | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 1012 | ||
| 9561b193 » | jwiegley | 2008-05-12 | 1013 | ###################################################################### | |
| bda8dbc9 » | jwiegley | 2008-05-12 | 1014 | ||
| 7173d82a » | Giulio Eulisse | 2008-05-21 | 1015 | if command == "init": | |
| 1016 | from os.path import split, join, exists, dirname | ||||
| 1017 | from os import getcwd, makedirs | ||||
| 1018 | from shutil import copy | ||||
| 1019 | path = getcwd() | ||||
| 1020 | while not exists(join(path,".git")): | ||||
| 1021 | path,extra = split(path) | ||||
| 1022 | if not extra: | ||||
| 1023 | print "Unable to find a git repository. " | ||||
| 1024 | print "Make sure you ran `git init` at some point." | ||||
| 1025 | sys.exit(1) | ||||
| 1026 | issuesdir = join(path,".gitissues") | ||||
| 1027 | if exists(issuesdir): | ||||
| 1028 | print "git-issues helper directory %s already exists." % issuesdir | ||||
| 1029 | print "Doing nothing." | ||||
| 1030 | sys.exit(1) | ||||
| 1031 | makedirs(issuesdir) | ||||
| 1032 | copy(__file__, issuesdir) | ||||
| 1033 | copy(join(dirname(__file__), "gitshelve.py"), issuesdir) | ||||
| 1034 | copy(join(dirname(__file__), "t_gitshelve.py"), issuesdir) | ||||
| 1035 | copy(join(dirname(__file__), "README"), issuesdir) | ||||
| 1036 | copy(join(dirname(__file__), "LICENSE"), issuesdir) | ||||
| f3b7babf » | edrik | 2008-08-20 | 1037 | sys.exit(0) | |
| 1038 | |||||
| 1039 | ###################################################################### | ||||
| 1040 | |||||
| 1041 | elif command == "list": | ||||
| 4aedf47c » | ktf | 2008-05-27 | 1042 | header = " # Id Title%sState Date Assign Tags" | |
| 1043 | width = int(options.screenWidth) | ||||
| 1044 | titleWidth = width - len(header) + 2 | ||||
| 1045 | print header % "".join([" " for x in xrange(titleWidth)]) | ||||
| 1046 | print "".join (["-" for x in xrange(width)]) | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 1047 | ||
| b6d2d87d » | jwiegley | 2008-05-14 | 1048 | index = 1 | |
| 401019fa » | ktf | 2008-05-27 | 1049 | filteredStati = options.filterStatus.split(":") | |
| d352c9e8 » | ktf | 2008-09-12 | 1050 | wantedTags = dict([(tag,1) for tag in options.filterTags.split(":") if tag]) | |
| f3b7babf » | edrik | 2008-08-20 | 1051 | ||
| f3268125 » | jwiegley | 2008-05-14 | 1052 | for item in issueSet.shelf.iteritems(): | |
| a71d32ca » | ktf | 2008-06-22 | 1053 | if "comment" in item[0]: | |
| 1054 | continue | ||||
| 3bb55535 » | jwiegley | 2008-05-14 | 1055 | issue = item[1].get_data() | |
| 401019fa » | ktf | 2008-05-27 | 1056 | if issue.status in filteredStati: | |
| 1057 | continue | ||||
| d352c9e8 » | ktf | 2008-09-12 | 1058 | if wantedTags and not issue.tags: | |
| 1059 | continue | ||||
| 1060 | if wantedTags: | ||||
| 1061 | matchingTags = [tag for tag in issue.tags.split(", ") if wantedTags.has_key(tag)] | ||||
| 1062 | if not matchingTags: | ||||
| 1063 | continue | ||||
| 4aedf47c » | ktf | 2008-05-27 | 1064 | formatString = "%4d %s %-" + str(titleWidth+len("Title")-1) + "s %-6s %5s %6s %s" | |
| 1065 | print formatString % \ | ||||
| b6d2d87d » | jwiegley | 2008-05-14 | 1066 | (index, issue.name[:7], issue.title, issue.status, | |
| 1067 | issue.created and issue.created.strftime('%m/%d'), | ||||
| 1068 | str(issue.author)[:6], '') | ||||
| 1069 | index += 1 | ||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 1070 | ||
| b6d2d87d » | jwiegley | 2008-05-14 | 1071 | ||
| f3b7babf » | edrik | 2008-08-20 | 1072 | ||
| 9561b193 » | jwiegley | 2008-05-12 | 1073 | ###################################################################### | |
| 1074 | |||||
| b6d2d87d » | jwiegley | 2008-05-14 | 1075 | elif command == "show" or command == "dump": | |
| 1076 | if len(args) == 0: | ||||
| e0788547 » | sbohrer | 2008-07-01 | 1077 | print "Usage: %s %s <issue-id | index>" % (sys.argv[0], command) | |
| b6d2d87d » | jwiegley | 2008-05-14 | 1078 | else: | |
| 73bc61c1 » | sbohrer | 2008-07-02 | 1079 | issue = issueSet[args[0]] | |
| 1080 | comments = "\n ".join(["Comment (%s): %s" % (comment[0:7], | ||||
| 1081 | issueSet.get_comment(comment[0:7]).comment) | ||||
| 1082 | for comment in issue.comments]) | ||||
| 1083 | if command == "show": | ||||
| 7eba73b6 » | jwiegley | 2008-07-31 | 1084 | if issue.title: | |
| 1085 | print " Title:", issue.title | ||||
| 1086 | if issue.summary: | ||||
| 1087 | print " Summary:", format_long_text(issue.summary) | ||||
| 1088 | |||||
| 1089 | if issue.description: | ||||
| 1090 | print " Description:", format_long_text(issue.description) | ||||
| 1091 | |||||
| 1092 | if issue.author: | ||||
| 1093 | print " Author:", issue.author | ||||
| 1094 | if issue.reporters: | ||||
| 1095 | print " Reporter(s):", format_people_list(issue.reporters) | ||||
| 1096 | if issue.owners: | ||||
| 1097 | print " Owner(s):", format_people_list(issue.owners) | ||||
| 1098 | if issue.assigned: | ||||
| 1099 | print " Assigned:", format_people_list(issue.assigned) | ||||
| 1100 | if issue.carbons: | ||||
| 1101 | print " Cc:", format_people_list(issue.carbons) | ||||
| 1102 | |||||
| 1103 | if issue.issue_type: | ||||
| 1104 | print " Type:", issue.issue_type | ||||
| 1105 | if issue.status: | ||||
| 1106 | print " Status:", issue.status | ||||
| 1107 | if issue.resolution: | ||||
| 1108 | print " Resolution:", issue.resolution | ||||
| 1109 | if issue.components: | ||||
| 1110 | print " Components:", issue.components | ||||
| 1111 | if issue.version: | ||||
| 1112 | print " Version:", issue.version | ||||
| 1113 | if issue.milestone: | ||||
| 1114 | print " Milestone:", issue.milestone | ||||
| 1115 | if issue.severity: | ||||
| 1116 | print " Severity:", issue.severity | ||||
| 1117 | if issue.priority: | ||||
| 1118 | print " Priority:", issue.priority | ||||
| 1119 | if issue.tags: | ||||
| 1120 | print " Tags:", issue.tags | ||||
| 1121 | |||||
| 1122 | print " Created:", issue.created | ||||
| 1123 | if issue.modified: | ||||
| 1124 | print " Modified:", issue.modified | ||||
| 73bc61c1 » | sbohrer | 2008-07-02 | 1125 | else: | |
| 1126 | write_object(issue) | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 1127 | ||
| 1128 | ###################################################################### | ||||
| 1129 | |||||
| b6d2d87d » | jwiegley | 2008-05-14 | 1130 | elif command == "change": | |
| 1131 | if len(args) == 0: | ||||
| e0788547 » | sbohrer | 2008-07-01 | 1132 | print "Usage: %s change <issue-id> <field> <value>" % sys.argv[0] | |
| addd4b0f » | ktf | 2008-05-22 | 1133 | else: | |
| 1134 | issue = issueSet[args[0]] | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 1135 | ||
| addd4b0f » | ktf | 2008-05-22 | 1136 | # jww (2008-05-13): Need to parse datetime, lists, and people | |
| f5850934 » | ktf | 2008-06-01 | 1137 | method = getattr(issue, "set_" + args[1]) | |
| ccd521d9 » | ktf | 2008-08-07 | 1138 | try: | |
| 1139 | method(args[2]) | ||||
| 1140 | except IndexError,e: | ||||
| 1141 | print "Index error." | ||||
| 1142 | print args | ||||
| 1143 | sys.exit(1) | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 1144 | ###################################################################### | |
| f3b7babf » | edrik | 2008-08-20 | 1145 | ||
| d0bafd0b » | ktf | 2008-06-03 | 1146 | elif command == "edit": | |
| 1147 | if len(args) == 0: | ||||
| e0788547 » | sbohrer | 2008-07-01 | 1148 | print "Usage: %s edit <issue-id> <field>" % sys.argv[0] | |
| d0bafd0b » | ktf | 2008-06-03 | 1149 | sys.exit(1) | |
| 1150 | else: | ||||
| 1151 | issue = issueSet[args[0]] | ||||
| bbf90ce3 » | ktf | 2008-06-10 | 1152 | if len(args) != 2: | |
| e0788547 » | sbohrer | 2008-07-01 | 1153 | print "Usage: %s edit <issue-id> <field>" % sys.argv[0] | |
| 4df5a51d » | ktf | 2008-06-20 | 1154 | sys.exit(1) | |
| bf0368e2 » | ktf | 2008-07-20 | 1155 | if not sys.stdin.isatty(): | |
| 1156 | contents = sys.stdin.read() | ||||
| 1157 | else: | ||||
| 4c5e4bd9 » | davglass | 2009-02-20 | 1158 | cmd = args[1].lower() | |
| 1159 | contents = inputFromEditor(getattr(issue, cmd)) | ||||
| 1160 | getattr(issue, "set_"+cmd)(contents) | ||||
| d0bafd0b » | ktf | 2008-06-03 | 1161 | ||
| 1162 | ###################################################################### | ||||
| 5f44f6e0 » | jwiegley | 2008-05-12 | 1163 | ||
| 47f1c6f9 » | ktf | 2008-06-02 | 1164 | elif command == "close": | |
| 1165 | if len(args) == 0: | ||||
| e0788547 » | sbohrer | 2008-07-01 | 1166 | print "Usage: %s close <issue-id>" % basename(sys.argv[0]) | |
| 47f1c6f9 » | ktf | 2008-06-02 | 1167 | sys.exit(1) | |
| 1168 | issue = issueSet[args[0]] | ||||
| 1169 | issue.set_status("closed") | ||||
| 1170 | |||||
| 1171 | ###################################################################### | ||||
| 1172 | |||||
| b6d2d87d » | jwiegley | 2008-05-14 | 1173 | elif command == "new": | |
| 1174 | if len(args) == 0: | ||||
| e0788547 » | sbohrer | 2008-07-01 | 1175 | print "Usage: %s new <title>" % sys.argv[0] | |
| addd4b0f » | ktf | 2008-05-22 | 1176 | else: | |
| 57f66a2b » | Toby Moore | 2008-05-15 | 1177 | issue = issueSet.new_issue(args[0]) | |
| 81b89b3d » | ktf | 2008-06-01 | 1178 | if options.status: | |
| 1179 | issue.set_status(options.status) | ||||
| 1180 | else: | ||||
| 1181 | issue.set_status("TODO") | ||||
| f3b7babf » | edrik | 2008-08-20 | 1182 | ||
| 919b0a9e » | ktf | 2008-05-22 | 1183 | if options.printNewBugs: | |
| 81b89b3d » | ktf | 2008-06-01 | 1184 | print "%s: %s (%s)" % (issue.status, issue.title, issue.name[0:7]) | |
| a71d32ca » | ktf | 2008-06-22 | 1185 | elif command == "comment": | |
| 1186 | if len(args) == 0: | ||||
| 1187 | print "Usage: %s comment <issue-id> <comment-title>" % sys.argv[0] | ||||
| 1188 | sys.exit(1) | ||||
| 1189 | issue = issueSet[args[0]] | ||||
| 1190 | comment = issueSet.new_comment(issue, args[1]) | ||||
| 1191 | if options.printNewBugs: | ||||
| 1192 | print "### Comment(%s): %s" % (comment.name[0:7], comment.comment) | ||||
| badd2f0f » | jwiegley | 2008-05-12 | 1193 | ||
| 9561b193 » | jwiegley | 2008-05-12 | 1194 | ###################################################################### | |
| f3b7babf » | edrik | 2008-08-20 | 1195 | ||
| 8d73af63 » | ktf | 2008-06-20 | 1196 | else: | |
| 1197 | print "Unknown command %s" % command | ||||
| b6d2d87d » | jwiegley | 2008-05-14 | 1198 | # If any of the commands made the issueSet dirty, (possibly) update the | |
| 1199 | # repository and write out a new cache | ||||
| 9561b193 » | jwiegley | 2008-05-12 | 1200 | ||
| b6d2d87d » | jwiegley | 2008-05-14 | 1201 | issueSet.save_state() | |
| 9561b193 » | jwiegley | 2008-05-12 | 1202 | ||
| 1203 | ###################################################################### | ||||
| badd2f0f » | jwiegley | 2008-05-12 | 1204 | ||
| b6d2d87d » | jwiegley | 2008-05-14 | 1205 | sys.exit(0) | |
| 5f44f6e0 » | jwiegley | 2008-05-12 | 1206 | ||
| 1207 | # git-issue ends here | ||||








