DITrack-0.8/0000755000076500007650000000000011032045407012320 5ustar vssvss00000000000000DITrack-0.8/AUTHORS0000644000076500007650000000024511032045302013363 0ustar vssvss00000000000000Vlad Skvortsov, vss@73rus.com, http://vss.73rus.com Oleg Sharov, o.sharov@gmail.com, http://oleg.sharov.name Ivan Glushkov, gli.work@gmail.com, http://gli.73rus.com DITrack-0.8/ChangeLog0000644000076500007650000001670011032045302014070 0ustar vssvss00000000000000DITrack Change Log $Id: ChangeLog 2614 2008-06-30 02:56:05Z vss $ $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/ChangeLog $ v.0.8, 29-Jun-2008 ================== * The speed of 'dt ls' greatly improved with the introduction of a cache (i#146). * 'dt ls' is now capable of filtering issues with a header absent or blank with 'dt ls header=' syntax (i#192). * 'dt act' now features a menu option to quote and reply to an existing comment. * New non-interactive option for 'dt act': 'change-header' to update a header to the specified value. * Overall speed improvements due to reimplementation of debug tracing facilities. * Now using a plain file (not a Subversion property) to store database format version (i#166) * Issue attributes are now sorted alphabetically in XML output ('dt ls --xml'). v.0.7, 06-Nov-2007 ================== * 'cat' and 'ls' commands can now produce output in XML ('--xml' option) (i#156). * 'act' is now able to update issues non-interactively with '-a', '-F' and '-m' options (i#162). * 'remove' and 'update' commands now accept '-u' option to specify the username (i#186). * 'act' for multiple issues now allows to change due to all common versions, even from different version sets (i#130). * 'commit' will now abort the whole transaction if any of the issues/comments can't be committed (i#135). * 'list' command now has option ('--list-formats') to display all defined output format (i#175). * Log messages for newly created issues now contain owner and due version (i#150). * Custom header changes are now mentioned in commit logs (i#159). v.0.6, 17-Sep-2007 ================== * Database format changed, a conversion utility is provided for databases created with DITrack version 0.5. * Significantly improved Windows compatibility. This includes reimplementation of the database creation script in Python (i#153). * A new CGI script (dubbed 'webui') is added to feature basic read-only web interface for DITrack (i#78). * Added basic support for file attachments handling: 'act' is now able to manage attachments and 'cat' is capable of dumping those (or their paths, if '--path' option is specified) (i#151). * 'list' command now accepts '-f' option to specify custom output format (i#20). * New command ('update') to update the working copy from the repository. Transparent updates are also available with '--maxage' option (i#125). * Special variable 'DT:USER' in predefined filters configuration will be replaced with the effective user name (e.g. set by '-u' option to 'act') in runtime (i#123). * Commit messages produced by DITrack now contain comment text and header field changes summary in them (i#114). * Improved output of issue titles in 'act', so that it's now visible what version sets the issues belong to (i#128). * Added basic locking capabilities to DITrack client to avoid concurrent changes to the same working copy (i#52). v.0.5, 06-Mar-2007 ================== * The database format has changed (see the README file for the upgrading instructions). * Support for disconnected operations added (new commands: 'commit', 'remove', 'status'; the 'act' and 'new' commands modified accordingly). * Installation script is now provided in the distributive. * Categories may now be marked as disabled (and thus won't appear in menus) (i#73). * Added the 'resolution' header for closed issues (e.g. 'dropped', 'fixed', 'invalid') (i#75). * It is now possible to specify user id through the '--user'/'-u' command line option or by setting the DITRACK_USER environment variable (i#98). * The 'act' command will now display titles of the issues worked on (i#74). * The 'cat' command now accepts arguments of the syntax X.Y, meaning "display comment Y of issue X". * A new command line option for the 'cat' command: '--headers-only'. * Duplicate user accounts in the configuration are now detected. * Fixed: dt-createdb doesn't set 'ditrack:format' property (i#72). * Fixed: unhandled exception if database root specified doesn't exist (i#76) * Fixed: the usage of filters in 'ls' cause unhandled exception if database path is invalid (i#81). * Fixed: referencing nonexistent header from a filter results in a crash (i#96). * Fixed: assertion fault on empty 'DT-New-*' header. * Fixed: 'cat' handles invalid entity ids ungracefully (i#88, i#94). * The debug output enabled by the DITRACK_DEBUG environment variable. * Added the test suite (48 testcases). v.0.4, 06-Oct-2006 ================== * Pre-0.3 issue databases are now longer supported. * Categories can no longer be named '-'. * It is now possible to refer to environment variables in filter definitions. * Commit log messages now reflect actual changes to issues worked on. * Identifier of just added issue/comment is now printed out after committing. * 'i#NN' notation is now consistently used to refer to an issue. * Database creation script now schedules newly created database addition to a repository. * User list is now sorted in 'reassign the issue owner' menu. * The '--version' option support resurrected (was broken since 0.3). * More helpful diagnostics when failed to open a database. * 'dt ls' invoked with nonexisting predefined filter now properly reports error. * Fixed: for positive timezone offsets the corresponding string in issue headers was printed like '+-400'. * Fixed: unhandled SVN error on propget for a database root. * Fixed: format version property was not set by database creation script. v.0.3.1, 24-Aug-2006 ==================== * Fixed: 'edit info headers' menu item didn't work in 'act'. v.0.3, 22-Aug-2006 ================== * DITrack is rewritten from scratch in Python. * Database format changed: issue descriptions and comments are now stored in RFC2822 message format. Conversion scripts are supplied (see the README file for conversion instructions). * 'edit-categories', 'edit-users' and 'edit-versions' commands removed. * The 'comment' command renamed to 'act'. * The 'act' command is now able to take action on multiple issues at once. * An issue header field changes are now recorded in structured way (with DT-Old-* and DT-New-* comment headers). * 'dt ls' is now able to accept multiple filter expressions (or predefined filter names) and display issues that match any of the filters. * Filter expressions (and predefined filters) used by 'dt ls' now may include repetitive header field names, e.g. 'Due-in!=0.2,Due-in!=0.3'. * Command line options now may be placed anywhere on a command line, provided they can be unambigously parsed out. * 'dt act' now has a menu option to change an issue owner. * It is now impossible to mess up comment headers from within the 'act' command. * Fixed: 'ls' aborted execution upon running into nonexistent/corrupted issue directory in the database. v.0.2, 18-Jul-2006 ================== * A draft of user manual added. * 'dt' now makes use of EDITOR environment variable to guess the editor to use. * 'dt' now recognizes DITRACK_ROOT environment variable specify a location of the issues database. * 'dt' now supports '--version' option to report its version information. * 'dt ls' now supports filters to selectively list issues. * 'dt comment' menu now has an option to change due version for the issue. * 'dt new' now copies just-entered issue title to the initial issue description (passed to a user to edit). * 'dt comment' no longer obligates the user to enter a comment text. * Timestamp presentation in issue and comment headers no longer depends on locale used. v.0.1, 26-Jun-2006 ================== Initial release. DITrack-0.8/DITrack/0000755000076500007650000000000011032045407013601 5ustar vssvss00000000000000DITrack-0.8/DITrack/__init__.py0000644000076500007650000000000011032045266015703 0ustar vssvss00000000000000DITrack-0.8/DITrack/Backend/0000755000076500007650000000000011032045407015130 5ustar vssvss00000000000000DITrack-0.8/DITrack/Backend/__init__.py0000644000076500007650000000000011032045265017231 0ustar vssvss00000000000000DITrack-0.8/DITrack/Backend/Common.py0000644000076500007650000000440311032045265016735 0ustar vssvss00000000000000# # Common.py - common DITrack backend declarations # # Copyright (c) 2007 The DITrack Project, www.ditrack.org. # # $Id: Common.py 1913 2007-08-17 20:14:53Z vss $ # $HeadURL: https://127.0.0.1/ditrack/src/trunk/DITrack/Common.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # # Generic backend exception class. # class Error(Exception): """ The following data members are available: message One-line description of a problem. No new lines, not terminated with a period. Suitable to be displayed in a pop-up window whatsoever. Always available. details A [possibly empty] list of strings describing the problem in a greater detail. Strings are not terminated with newlines. """ def __init__(self, message, details): self.message = message self.details = details class UpdateStatus: """ Abstract class representing the backend-specific result of an update. """ def __init__(self): raise NotImplementedError def __str__(self): raise NotImplementedError DITrack-0.8/DITrack/Client.py0000644000076500007650000001127211032045266015377 0ustar vssvss00000000000000# # Client.py - DITrack Client interface module # # Copyright (c) 2006-2008 The DITrack Project, www.ditrack.org. # # $Id: Client.py 2519 2008-05-28 06:27:14Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Client.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import DITrack.DB.Exceptions # # Generic client exception class visible to the application. # class Error(Exception): """ The following data members are available: message One-line description of a problem. No new lines, not terminated with a period. Suitable to be displayed in a pop-up window whatsoever. Always available. details A [possibly empty] list of strings describing the problem in a greater detail. Strings are not terminated with newlines. """ def __init__(self, message, details): self.message = message self.details = details class Client: """ A DITrack client class. Provides high-level API. """ def __init__(self, db): """ Initialize the client. DB is the DITrack.DB.Common.Database object. """ self.db = db def issues(self, filters=None): """ Returns a list of tuples (ID, ISSUE) for each issue in the database, filtering them with FILTERS (objects of DITrack.DB.Common.Filter). If any of the filters match, the issue matches. """ res = [] for id, issue in self.db.issues(): matches = None if filters: for f in filters: if f.matches(issue): matches = True if not matches: continue res.append((id, issue)) return res def update(self, maxage=None): """ Update the working copy (sync from the repository). If MAXAGE is None, the update takes place unconditionally. Otherwise if the database was updated less than MAXAGE seconds back, the update is skipped. On error raises DITrack.Client.Error(). On success may return: None No update took place. not None An instance of DITrack.Client.UpdateStatus containing the details of the update. """ try: us = self.db.update(maxage) except DITrack.DB.Exceptions.BackendError, e: raise Error( "Database update failed: %s" % e.backend_e.message, e.backend_e.details ) if us: return UpdateStatus(us) else: return None class UpdateStatus: """ Class representing the result of an update. Not to be instantiated by an application. """ def __init__(self, backend_us): """ Given backend-specific update status BACKEND_US create an object describing the update in a backend-agnostic way. """ assert backend_us is not None # XXX: we should ensure that backend_us is a descendant of # DITrack.Backend.Common.UpdateStatus. # For now we just store the backend-specific object. This will change # later when multiple backends come into play. self._status = backend_us def __str__(self): """ Return the summary of the update in a free form. XXX: To be changed later, the format of the output should not be relied upon. """ return str(self._status) DITrack-0.8/DITrack/Command/0000755000076500007650000000000011032045407015157 5ustar vssvss00000000000000DITrack-0.8/DITrack/Command/__init__.py0000644000076500007650000000000011032045266017261 0ustar vssvss00000000000000DITrack-0.8/DITrack/Command/act.py0000644000076500007650000006127111032045266016312 0ustar vssvss00000000000000# # act.py - DITrack 'act' command # # Copyright (c) 2006-2008 The DITrack Project, www.ditrack.org. # # $Id: act.py 2516 2008-05-26 14:25:52Z gli $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/act.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import copy import email import email.Message import os import sys # DITrack modules import DITrack.Command.generic import DITrack.Edit import DITrack.UI import DITrack.Util.common class _InconsistencyError(Exception): """ The action would create a database inconsistency. """ def __init__(self, issue_id, message): self.issue_id = issue_id self.message = message class _Driver: """ Class representing a driver: en entity that generates actions to be performed on the issue(s). """ def __init__(self, globals, dbcfg, issues, future_versions, vsets): """ Initializes the driver environment. Parameters are: DBCFG A database configuration object. FUTURE_VERSIONS A list of version strings that can be used as future versions. May be empty if the issues being dealt with don't have common future versions. GLOBALS Globals object. ISSUES A dictionary of issue objects. Keys are issue ids (strings). VSETS A list of version sets of the issues we deal with. No duplicates allowed. """ self._dbcfg = dbcfg self._future_versions = future_versions self._globals = globals self._issues = issues self._vsets = vsets # # Helper data # self._single_issue = (len(self._issues) == 1) self._issue_numbers = self._issues.keys() self._issue_numbers.sort(lambda x,y: cmp(int(x), int(y))) def _change_issues_due_version(self, version): """ Changes due version of all issues to VERSION. """ for i in self._issues.values(): i.change_due_version(version) def _close_issues(self, resolution): """ Close all issues with specified RESOLUTION. """ assert resolution for id in self._issues: try: self._issues[id].close(resolution) except DITrack.DB.Exceptions.InconsistentActionError, msg: raise _InconsistencyError(id, msg) def _reassign_issues(self, owner): """ Reassign all issues to OWNER. """ for i in self._issues.values(): i.reassign(owner) def _reopen_issues(self): """ Reopen all issues. """ for id in self._issues: try: self._issues[id].reopen() except DITrack.DB.Exceptions.InconsistentActionError, msg: raise _InconsistencyError(id, msg) def _change_issues_header(self, header): """ Update (or add) a single header of/to all issues. The header name and its new value are passed in HEADER, separated by '='. Raises ValueError if HEADER couldn't be parsed out. """ k, v = header.split("=", 1) for issue in self._issues.itervalues(): # XXX: should probably use internal method like replace_header(). issue.info[k] = v def run(self): """ Runs the driver. Returns a sorted list of issue numbers (XXX) to try saving. Empty list returned means "don't save the changes". """ raise NotImplementedError class _CmdlineDriver(_Driver): """ Action driver for noninteractive (command line) sessions. """ def __init__(self, actionlist, comment_text, **kv): """ ACTIONLIST is a parameter to '-a' option, as described in the usage note. COMMENT_TEXT is the comment text to add (may be ""). The rest parameters are the same as the base class constructor method accepts. """ _Driver.__init__(self, **kv) if actionlist: self._actions = actionlist.strip().split(",") else: self._actions = [] self.comment_text = comment_text def _invalid_action(self, action, issue, msg): """ Prints out diagnostic message MSG about ACTION on ISSUE (string) and returns an empty list. """ DITrack.Util.common.err( "Can't do '%s' on i#%s: %s" % (action, issue, msg) ) return [] def run(self): for a in self._actions: a = a.split(":") if len(a) == 1: action, arg = a[0], None elif len(a) == 2: action, arg = a else: DITrack.Util.common.err("Invalid action list syntax") if action == "change-due": if (not arg) or (arg not in self._future_versions): DITrack.Util.common.err( "'change-due' requires a valid future version number " "as the argument" ) self._change_issues_due_version(arg) elif action == "close": valid_resolutions = ("dropped", "fixed", "invalid") if (not arg) or (arg not in valid_resolutions): DITrack.Util.common.err( "'close' requires one of %s as the argument" % ", ".join(["'%s'" % x for x in valid_resolutions]) ) try: self._close_issues(arg) except _InconsistencyError, e: return self._invalid_action(action, e.issue_id, e.message) elif action == "reassign": # XXX: this check really belongs to the database layer if (not arg) or (arg not in self._dbcfg.users): DITrack.Util.common.err( "'reassign' requires a valid user name as the argument" ) self._reassign_issues(arg) elif action == "reopen": if arg: DITrack.Util.common.err( "'reopen' doesn't accept arguments" ) try: self._reopen_issues() except _InconsistencyError, e: return self._invalid_action(action, e.issue_id, e.message) elif action == "change-header": try: self._change_issues_header(arg) except ValueError: DITrack.Util.common.err( "'change-header' requires 'header=value' argument" ) else: DITrack.Util.common.err("Invalid action: %s" % action) return self._issue_numbers class _InteractiveDriver(_Driver): """ Action driver for interactive sessions. """ def _list_attaches(self, issue): attaches = issue.attachments() qty = len(attaches) sys.stdout.write( "\n%d file(s) currently attached\n" % qty ) for i in range(qty): flags = "" if attaches[i].is_local: flags = "L" sys.stdout.write( "%3d %-3s %s\n" % (i + 1, flags, attaches[i].name) ) sys.stdout.write("\n") def _manage_attaches(self, issue): mi_abort = DITrack.UI.MenuItem("a", "abandon this menu") mi_new = DITrack.UI.MenuItem("n", "new attach") mi_remove = DITrack.UI.MenuItem("r", "remove attach") menu = DITrack.UI.Menu( "Choose an action to manage attaches", [ mi_abort, mi_new, mi_remove, ]) while 1: # List the attaches self._list_attaches(issue) r = menu.run() if r == mi_abort: break elif r == mi_new: ti = DITrack.UI.TextInput("File to attach (blank to abort)") while 1: fname = ti.run() if not fname: break if not os.path.exists(fname): sys.stdout.write("File doesn't exist: %s\n" % fname) continue if not os.path.isfile(fname): sys.stdout.write("Not a file: %s\n" % fname) continue try: issue.add_attachment(fname) break except ValueError, name: sys.stdout.write( "Attachment named '%s' already exists\n" % name ) except DITrack.DB.Exceptions.BadAttachmentNameError, name: sys.stdout.write( "Attachment named '%s' has been removed within " "this session; can't add another one with the " "same name -- you need to save your changes first" "\n" % name ) elif r == mi_remove: # Create menu with a list of attachments removal_menu = DITrack.UI.EnumMenu( "Choose an attachment to remove", map(lambda x: x.name, issue.attachments()), abort_option=True ) fname = removal_menu.run() if fname is None: continue issue.remove_attachment(fname) # Go straight to the main menu from here return def _reply_comment(self): assert self._single_issue # XXX: shouldn't a 'human-redable' representation of dates be a member # of the Comment class? def rm_timestamp(text): return text.split(" ", 1)[1] issue = self._issues[self._issue_numbers[0]] # Get only "firm" comments comments = issue.comments(local=False) # Creating comment choice menu mi_reply_abort = DITrack.UI.MenuItem("a", "abort") mi_reply_comments = [ DITrack.UI.MenuItem( 0, "original description by %s, %s" % ( issue.info["Opened-by"], rm_timestamp(issue.info["Opened-on"]) ) ) ] for (id, c) in comments[1:]: mi_reply_comments.append( DITrack.UI.MenuItem( int(id), "comment #%s by %s, %s" % ( id, c.added_by, rm_timestamp(c.added_on) ) ) ) reply_menu = DITrack.UI.Menu( "Choose a comment to reply to", [mi_reply_abort] + mi_reply_comments ) comment_id = None while comment_id is None: r = reply_menu.run() if r == mi_reply_abort: break else: # Comment id to reply to id = "%d" % r.key c = issue[id] sys.stdout.write( "\n======\n" "\nComment #%s by %s, %s\n\n" % ( id, c.added_by, rm_timestamp(c.added_on) ) ) sys.stdout.write("".join(c.header_as_strings())) sys.stdout.write("\n" + c.text + "\n======\n") ti_confirmation = DITrack.UI.TextInput( "Is this the comment you'd like to reply to (y/n)?" ) while 1: choice = ti_confirmation.run() if choice == "y": comment_id = id break elif choice == "n": break if comment_id is not None: c = issue[comment_id] def _quote_string(str): if str and (str[0] != ">"): return "> " + str else: return ">" + str self.comment_text = DITrack.Edit.edit_text( self._globals, "\nQuoting c#%s by %s, %s\n\n%s" % ( id, c.added_by, rm_timestamp(c.added_on), "\n".join(map(_quote_string, c.text.split("\n"))) ) ) def run(self): """ If changes are to be saved (see the base class method description), the COMMENT_TEXT member contains the comment text for the action upon the return from this method. """ # Build up the menu. mi_abort = DITrack.UI.MenuItem("a", "abort, discarding changes") mi_attaches = DITrack.UI.MenuItem("f", "manage file attaches") mi_ch_due_in = DITrack.UI.MenuItem("d", "change due version") mi_close = DITrack.UI.MenuItem("c", "close the issue") mi_edit_info = DITrack.UI.MenuItem("h", "edit the issue header") mi_edit_text = DITrack.UI.MenuItem("e", "edit comment text") mi_quit = DITrack.UI.MenuItem("q", "quit, saving changes") mi_reassign = DITrack.UI.MenuItem("o", "reassign the issue owner") mi_reopen = DITrack.UI.MenuItem("r", "reopen the issue") mi_reply = DITrack.UI.MenuItem("re", "reply to a comment") menu = DITrack.UI.Menu("Choose an action for the issue(s)", [ mi_abort, mi_attaches, mi_ch_due_in, mi_close, mi_edit_info, mi_edit_text, mi_quit, mi_reassign, mi_reopen, mi_reply ]) save_changes = False self.comment_text = "" mi_ch_due_in.enabled = self._future_versions mi_attaches.enabled = mi_edit_info.enabled = self._single_issue while 1: # Conditionally enable/disable menu items. mi_close.enabled = filter(lambda x: x.info["Status"] == "open", self._issues.itervalues()) mi_reopen.enabled = filter( lambda x: x.info["Status"] == "closed", self._issues.itervalues() ) mi_reply.enabled = self._single_issue and self.comment_text == "" sys.stdout.write("\nActing on:\n") output = dict([(x, "") for x in self._vsets]) for id in self._issue_numbers: vset = self._dbcfg.category[ self._issues[id].info["Category"] ].version_set output[vset] += "i#%s: %s\n" % ( id, self._issues[id].info["Title"] ) sys.stdout.write("\n") for vset in self._vsets: sys.stdout.write("[%s]:\n%s\n" % (vset, output[vset])) sys.stdout.write("\n") r = menu.run() if r == mi_abort: break elif r == mi_attaches: assert len(self._issue_numbers) == 1 self._manage_attaches(self._issues[self._issue_numbers[0]]) elif r == mi_close: mi_c_abort = DITrack.UI.MenuItem("a", "abort closing") mi_c_dropped = DITrack.UI.MenuItem("d", "dropped") mi_c_fixed = DITrack.UI.MenuItem("f", "fixed") mi_c_invalid = DITrack.UI.MenuItem("i", "invalid") resolution_menu = DITrack.UI.Menu( "Choose the resolution", [ mi_c_abort, mi_c_dropped, mi_c_fixed, mi_c_invalid ]) r = resolution_menu.run() if r != mi_c_abort: if r == mi_c_dropped: resolution = "dropped" elif r == mi_c_fixed: resolution = "fixed" elif r == mi_c_invalid: resolution = "invalid" self._close_issues(resolution) elif r == mi_ch_due_in: assert self._future_versions any_issue = self._issues[self._issue_numbers[0]] if self._single_issue: sys.stdout.write("Current due version: %s\n" % any_issue.info["Due-in"]) due_menu = DITrack.UI.EnumMenu("Choose new due version", self._future_versions) v = due_menu.run() if not v: break self._change_issues_due_version(v) elif r == mi_edit_info: id = self._issue_numbers[0] info = email.Message.Message() keys = self._issues[id].info.keys() keys.sort() for k in keys: info.add_header(k, self._issues[id].info[k]) header = DITrack.Edit.edit_text( self._globals, info.as_string() ) self._issues[id].info = {} new_info = email.message_from_string(header) for h in new_info.keys(): self._issues[id].info[h] = new_info[h] elif r == mi_edit_text: self.comment_text = DITrack.Edit.edit_text(self._globals, self.comment_text ) elif r == mi_quit: save_changes = True break elif r == mi_reassign: if self._single_issue: sys.stdout.write("Current issue owner: %s\n" % self._issues[self._issue_numbers[0]].info["Owned-by"] ) users = self._dbcfg.users.keys() users.sort() owner_menu = DITrack.UI.EnumMenu("Choose new issue owner", users) v = owner_menu.run() if not v: break self._reassign_issues(v) elif r == mi_reopen: self._reopen_issues() elif r == mi_reply: self._reply_comment() else: raise NotImplementedError if save_changes: return self._issue_numbers else: return [] class Handler(DITrack.Command.generic.Handler): canonical_name = "act" # XXX: replace ISSUENUM with ISSUEID later description = """Perform actions on an issue (or multiple issues). usage: %s ISSUENUM [ISSUENUM...]""" % canonical_name description_ps = """ ACTIONLIST is a comma-separated list of one of more of the following actions: change-header:HEADER=VALUE - add/update issue(s) header HEADER with specified value VALUE; omitting VALUE removes the header. close:{dropped, fixed, invalid} - close the issue(s) with specified resolution. change-due:VERSION - change the due version to VERSION. reassign:USER - reassign the issue(s) to USER. reopen - reopen the issue(s). Each action can occur in the ACTIONLIST once at the most. Any of -a, -F or -m implies non-interactive mode. -F and -m are mutually exclusive. """ def run(self, opts, globals): self.check_options(opts) if len(opts.fixed) < 2: self.print_help(globals) sys.exit(1) # We'll need an editor. globals.get_editor() db = DITrack.Util.common.open_db(globals, opts, "w") present_vsets = {} issue = {} prev_issue = {} for id in opts.fixed[1:]: id = id.upper() try: issue[id] = db.issue_by_id(id) except (KeyError, ValueError): # Diagnostics printed by issue_by_id(). pass if db.is_valid_issue_name(id): DITrack.Util.common.err( "Non-local identifier expected '%s'" % id ) prev_issue[id] = copy.deepcopy(issue[id]) present_vsets[ db.cfg.category[ issue[id].info["Category"] ].version_set] = 1 # Scan through version sets we are dealing with and find an # intersection of all future versions. i = 0 common_versions = [] for vset in present_vsets: if not i: common_versions = db.cfg.versions[vset].future i += 1 else: intersected_versions = [] for version in db.cfg.versions[vset].future: if version in common_versions: intersected_versions.append(version) if len(intersected_versions) == 0: break common_versions = intersected_versions common_versions.sort() if ("actions" in opts.var) or ("comment_file" in opts.var) or \ ("comment_message" in opts.var): if "actions" in opts.var: actions = opts.var["actions"] else: actions = "" if "comment_file" in opts.var: comment_text = open(opts.var["comment_file"]).read() elif "comment_message" in opts.var: comment_text = "%s\n" % opts.var["comment_message"] else: comment_text = "" driver = _CmdlineDriver( actions, comment_text, globals=globals, dbcfg=db.cfg, issues=issue, future_versions=common_versions, vsets=present_vsets.keys() ) else: driver = _InteractiveDriver( globals, db.cfg, issues=issue, future_versions=common_versions, vsets=present_vsets.keys() ) issue_numbers = driver.run() if issue_numbers: # We need to save the changes local_names = [] for id in issue_numbers: try: name, comment = db.new_comment(id, prev_issue[id], issue[id], driver.comment_text, globals.username, globals.fmt_timestamp()) local_names.append((id, name)) # XXX: should embrace only db.new_comment() except DITrack.DB.Exceptions.NoDifferenceCondition: continue sys.stdout.write("Comment %s added to issue %s\n" % (name, id)) if not opts.var["no_commits"]: # Now commit newly added comments. We do it in a separate step # to simplify the solution for now. If something goes wrong # (like no disk space or connectivity issues), a user may # choose to commit the changes later. for issue_id, comment_name in local_names: # XXX: for now we don't deal with commenting local issues. assert db.is_valid_issue_number(issue_id) firm_id = db.commit_comment(issue_id, comment_name) # XXX: print 'Local ... in r234'. sys.stdout.write("Local i#%s.%s committed as i#%s.%s\n" % \ (issue_id, comment_name, issue_id, firm_id)) DITrack-0.8/DITrack/Command/cat.py0000644000076500007650000001446011032045266016310 0ustar vssvss00000000000000# # cat.py - DITrack 'cat' command # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: cat.py 2191 2007-10-15 22:34:10Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/cat.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import sys # DITrack modules import DITrack.Command.generic class Handler(DITrack.Command.generic.Handler): canonical_name = "cat" description = """Dump contents/headers of an issue or its part. usage: %s ISSUEID or: %s ISSUEID.COMMENTID or: %s [--path] ISSUEID.{ATTACHMENT_NAME} """ % (canonical_name, canonical_name, canonical_name) def run(self, opts, globals): self.check_options(opts) if len(opts.fixed) != 2: self.print_help(globals) sys.exit(1) # XXX: this double check '("X" in ...) and ...' is silly, need to # wrap it into something headers_only = opts.var.has_key("headers_only") \ and opts.var["headers_only"] path_option = ("path" in opts.var) and opts.var["path"] xml_option = ("xml" in opts.var) and opts.var["xml"] db = DITrack.Util.common.open_db(globals, opts) # XXX: add a wrapper function to err out? try: parsed_id = DITrack.Common.Identifier(opts.fixed[1]) except ValueError: DITrack.Util.common.err( "Invalid identifier: '%s'" % opts.fixed[1], fatal=True) issue_id = parsed_id.issue.id issue = db.issue_by_id(issue_id) if parsed_id.has_comment_part(): comment_id = parsed_id.comment.id elif parsed_id.is_attachment_id(): fname = parsed_id.attachment.fname try: af = issue.get_attachment(fname) except KeyError: DITrack.Util.common.err( "No such attachment: '%s'" % fname, fatal=True ) if path_option: sys.stdout.write("%s\n" % af.path) else: sys.stdout.write(open(af.path).read()) sys.exit(0) else: if headers_only: # dt cat --headers-only 1 # is a shortcut for # dt cat --headers-only 1.0 comment_id = "0" else: comment_id = None if path_option: DITrack.Util.common.err( "Can't print a path of non-attachment: '%s'" % opts.fixed[1], fatal=True ) if xml_option: attrs = [ ("issue", issue_id) ] if comment_id is not None: attrs.append(("comment", comment_id)) if headers_only: attrs.append(("headers-only", "true")) xo = DITrack.XML.Output("cat", attrs) match = False if (comment_id is None) or (comment_id == "0"): match = True if xml_option: xo.writer.opentag("header", { "issue-id": issue_id }, nl=True) for (k, v) in issue.info.items(): xo.writer.tag_enclose(k, {}, v, nl=True, indent=2) xo.writer.closetag("header", nl=True) xo.writer.text("\n") else: sys.stdout.write("Issue: %s\n" % issue_id) issue.write_info() if not headers_only: sys.stdout.write("\n") first = True for id, comment in issue.comments(): if (comment_id is None) or (id == comment_id): match = True if xml_option: xo.writer.opentag("comment", { "id": id }, nl=True) xo.writer.opentag("header", {}, nl=True, indent=2) for (k, v) in comment.headers(): xo.writer.tag_enclose(k, {}, v, nl=True, indent=4) xo.writer.closetag("header", nl=True, indent=2) if not headers_only: xo.writer.opentag("text", {}, nl=True, indent=2) xo.writer.text(comment.text) xo.writer.closetag("text", nl=True, indent=2) xo.writer.closetag("comment", nl=True) xo.writer.text("\n") else: if not first: sys.stdout.write("Comment: %s\n" % id) comment.write( headers_only=headers_only, display_headers=( (not first) or (comment_id and (comment_id != "0")) ) ) if not headers_only: sys.stdout.write("\n") if not xml_option: if comment_id is None: sys.stdout.write("%s\n" % globals.text_delimiter) first = False if not match: DITrack.Util.common.err( "No such entity: '%s.%s'" % (issue_id, comment_id), fatal=True) if xml_option: xo.finish() DITrack-0.8/DITrack/Command/commit.py0000644000076500007650000001212711032045266017027 0ustar vssvss00000000000000# # commit.py - DITrack 'commit' command # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: commit.py 2221 2007-10-19 21:36:00Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/commit.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import string import sys # DITrack modules import DITrack.Command.generic class Handler(DITrack.Command.generic.Handler): canonical_name = "commit" description = """Commit local changes to the repository. usage: %s [ID...]""" % canonical_name def _ids_from_cmdline(self, opts): assert len(opts.fixed) > 1 # XXX: we should sort the entities to be committed ids = [] for id in opts.fixed[1:]: try: parsed_id = DITrack.Common.Identifier(id) except ValueError: sys.stderr.write( "Invalid entity name '%s', ignored\n" % id ) continue issue_id = parsed_id.issue.id if parsed_id.has_comment_part(): comment_id = parsed_id.comment.id if comment_id.isdigit(): sys.stderr.write( "Non-local comment id in '%s', ignored\n" % id ) continue else: if issue_id.isdigit(): sys.stderr.write( "Non-local issue id '%s', ignored\n" % id ) continue comment_id = None ids.append((issue_id, comment_id)) return ids def run(self, opts, globals): self.check_options(opts) db = DITrack.Util.common.open_db(globals, opts, "w") if len(opts.fixed) > 1: # We are told explicitly what to commit ids = self._ids_from_cmdline(opts) else: # Commit all local issues (not comments!) # See i#108: for now, let's just commit new issues since handling # comments is much more complex. ids = map(lambda x: (x[0], None), db.issues(from_wc=False)) # Hash of issues that user wants to commit issues_to_commit = {} # Fill keys in the hash for issue_id, comment_id in ids: assert issue_id # Work only with comments if comment_id is not None: issues_to_commit[issue_id] = None; # Get the local comments for each issue (fill values) for issue_id in issues_to_commit.keys(): issues_to_commit[issue_id] = db[issue_id].comments(firm=False); # Comments in each issues are sorted, so the first in the list # should be committed first. for issue_id, comment_id in ids: # work only with comments if comment_id is not None: if comment_id != issues_to_commit[issue_id][0][0]: DITrack.Util.common.err( "Can't commit %s.%s: %s.%s is in the way" % ( issue_id, comment_id, issue_id, issues_to_commit[issue_id][0][0] ) ) # Keep the list in the state that the first comment in it # should be committed first. del(issues_to_commit[issue_id][0]) for issue_id, comment_id in ids: if comment_id is None: firm_id = db.commit_issue(issue_id) id = "%s" % issue_id else: firm_id = db.commit_comment(issue_id, comment_id) firm_id = "%s.%s" % (issue_id, firm_id) id = "%s.%s" % (issue_id, comment_id) assert firm_id is not None # XXX: print 'Local i#ABC committed as i#123 in r234'. sys.stdout.write("Local i#%s committed as i#%s.\n" % (id, firm_id)) DITrack-0.8/DITrack/Command/generic.py0000644000076500007650000000564211032045266017157 0ustar vssvss00000000000000# # dt - DITrack commands base class # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: generic.py 2257 2007-10-22 22:49:59Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/generic.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import sys class SyntaxError(Exception): "Syntax error detected by a handler" class Handler: canonical_name = "generic command" description = "generic description" def __init__(self, *options): self.options = options def check_options(self, passed): "Check that options passed are all valid for this command" for o in passed.var.keys(): if len(filter(lambda x: x.name == o, self.options)) == 0: # If no alias is set, then the value is a # default one. if passed.var_alias.has_key(o): sys.stdout.write( "Command '%s' doesn't accept option '%s'\n" % (self.canonical_name, passed.var_alias[o]) ) sys.exit(1) def print_help(self, globals): name = self.canonical_name cmds = globals.command_table.map if len(cmds[name]) > 1: name = name + " (" + ", ".join(cmds[name][1:]) + ")" sys.stdout.write("%s: %s\n" % (name, self.description)) options = self.options if len(options): sys.stdout.write("\nValid options:\n") for o in options: o.print_description() if "description_ps" in self.__class__.__dict__: sys.stdout.write(self.__class__.description_ps) def run(self, opts, globals): raise NotImplementedError DITrack-0.8/DITrack/Command/help.py0000644000076500007650000000460411032045266016470 0ustar vssvss00000000000000# # dt - DITrack 'help' command # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: help.py 1696 2007-07-10 22:45:31Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/help.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import generic def print_command_help(globals, cmd): table = globals.command_table.table cmds = globals.command_table.map if table.has_key(cmd): table[cmd].print_help(globals) else: print '"' + cmd + '": unknown command.' print class Handler(generic.Handler): canonical_name = "help" description = """Describe the usage of this program or its commands. usage: %s [COMMAND...]""" % canonical_name def run(self, opts, globals): self.check_options(opts) if len(opts.fixed) > 1: for cmd in opts.fixed[1:]: print_command_help(globals, cmd) return print \ """%s General usage: %s [] [] Available commands:""" % (globals.dt_title, globals.binname) cmds = globals.command_table.map keys = cmds.keys() keys.sort() for k in keys: print "\t%s" % (", ".join(cmds[k])) print "\nType '%s help ' for help on specific " \ "command.\n" % globals.binname DITrack-0.8/DITrack/Command/list.py0000644000076500007650000001157111032045266016514 0ustar vssvss00000000000000# # list.py - DITrack 'list' command # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: list.py 2390 2007-11-24 17:07:13Z oleg $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/list.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import os import sys # DITrack modules import DITrack.DB.Common import DITrack.DB.Exceptions import DITrack.Client import DITrack.Command.generic import DITrack.XML class _FallbackDict: """ A wrapper (fallback) dictionary-like class allowing to fetch info header fields and fall back to default values if there is no such header present. """ def __getitem__(self, key): if key == "id": return "%s" % self.id elif key in self.info: return self.info[key] else: return "-"; def __init__(self, id, info): self.id = id self.info = info class Handler(DITrack.Command.generic.Handler): canonical_name = "list" description = """List issues, possibly filtering. usage: %s [FILTER...]""" % canonical_name def _xml_output(self, globals, issues, filters): input = [ ("user", globals.username) ] for f in filters: input.append(("filter", str(f))) xo = DITrack.XML.Output("list", input) for id, issue in issues: xo.writer.opentag("issue", { "id": str(id) }, nl=True) issue_keys = issue.info.keys() issue_keys.sort() for k in issue_keys: xo.writer.tag_enclose(k, {}, issue.info[k], nl=True, indent=2) xo.writer.closetag("issue") xo.writer.text("\n\n") xo.finish() def run(self, opts, globals): self.check_options(opts) db = DITrack.Util.common.open_db(globals, opts) if opts.var.has_key("listing_format_list") \ and opts.var["listing_format_list"]: format_list = db.cfg.listing_format.items.keys() format_list.sort() for f in format_list: sys.stdout.write("%s\n" % f) return xml = ("xml" in opts.var) and opts.var["xml"] if xml and ("listing_format" in opts.var): DITrack.Util.common.err( "Listing output format can't be specified when performing " "output in XML." ) client = DITrack.Client.Client(db) filters = [] for e in opts.fixed[1:]: try: f = DITrack.DB.Common.Filter(e, globals.username) except DITrack.DB.Exceptions.FilterIsPredefinedError: if e in db.cfg.filters: f = db.cfg.filters[e] else: DITrack.Util.common.err( "ERROR: '%s' is not a predefined filter" % e ) except DITrack.DB.Exceptions.FilterExpressionError: DITrack.Util.common.err( "ERROR: '%s' contains a syntax error" % e ) filters.append(f) try: if opts.var.has_key("listing_format"): format = db.cfg.listing_format[opts.var["listing_format"]] else: format = db.cfg.listing_format["default"] except DITrack.DB.Exceptions.InvalidListingFormatError, key: DITrack.Util.common.err( "ERROR: Unknown listing format '%s'" % key ) issues = client.issues(filters) if xml: self._xml_output(globals, issues, filters) else: for id, issue in issues: print format % _FallbackDict(id, issue.info) DITrack-0.8/DITrack/Command/new.py0000644000076500007650000000754411032045266016337 0ustar vssvss00000000000000# # new.py - DITrack 'new' command # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: new.py 1696 2007-07-10 22:45:31Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/new.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import sys # DITrack modules import DITrack.Command.generic class Handler(DITrack.Command.generic.Handler): canonical_name = "new" description = """Add new issue to a database. usage: %s""" % canonical_name def run(self, opts, globals): self.check_options(opts) globals.get_editor() db = DITrack.Util.common.open_db(globals, opts, "w") # Choose one of categories categories = filter( lambda x: db.cfg.category[x].enabled, db.cfg.category.keys() ) categories.sort() m_category = DITrack.UI.EnumMenu("Choose the issue category", categories, abort_option=True) category = m_category.run() if not category: return assert(category in db.cfg.category) # Now ask for a version the issue is reported against m_version = DITrack.UI.EnumMenu( "Choose the version the issue is reported against", db.cfg.category[category].versions.current, abort_option=True) version = m_version.run() if not version: return # Ask for a title ti_title = DITrack.UI.TextInput("Enter the issue title") title = ti_title.run() if not title: return # Now edit the issue description text descr = DITrack.Edit.edit_text(globals, """DITrack: Enter the issue description here. The line below is your issue title. %s """ % title) # Now ask for a version the issue is due m_version = DITrack.UI.EnumMenu( "Choose the version the issue is due", db.cfg.category[category].versions.future, abort_option=True) due_version = m_version.run() if not due_version: return local_id, issue = db.new_issue( title=title, opened_by=globals.username, opened_on=globals.fmt_timestamp(), owned_by=None, category=category, version_reported=version, version_due=due_version, description=descr) # Check if we should commit the new issue if opts.var["no_commits"]: sys.stdout.write("New local issue #%s added\n" % local_id) else: id = db.commit_issue(local_id) sys.stdout.write( "New local issue #%s committed as i#%s\n" % (local_id, id) ) DITrack-0.8/DITrack/Command/remove.py0000644000076500007650000001001611032045266017027 0ustar vssvss00000000000000# # remove.py - DITrack 'remove' command # # Copyright (c) 2007 The DITrack Project, www.ditrack.org. # # $Id: remove.py 1909 2007-08-16 23:46:45Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/remove.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import string import sys # DITrack modules import DITrack.Command.generic class Handler(DITrack.Command.generic.Handler): canonical_name = "remove" description = """Remove local comments. usage: %s ID...""" % canonical_name def run(self, opts, globals): self.check_options(opts) db = DITrack.Util.common.open_db(globals, opts, "w") if len(opts.fixed) < 2: raise DITrack.Command.generic.SyntaxError issue_comments = {} for id in opts.fixed[1:]: parsed_id = DITrack.Common.Identifier(id) if not parsed_id.has_comment_part(): DITrack.Util.common.err( "Comment identifier expected: '%s'" % id ) if parsed_id.comment.id.isdigit(): DITrack.Util.common.err( "Non-local comment identifier: '%s'" % id ) issue_id, comment_id = parsed_id.issue.id, parsed_id.comment.id if issue_id not in issue_comments: issue_comments[issue_id] = {} issue_comments[issue_id][comment_id] = True # Sort the issue numbers that were mentioned in arguments issues = issue_comments.keys() issues.sort() for issue_id in issues: # This is (was) essentially a set of mentioned comment names. comments_to_remove = issue_comments[issue_id].keys() comments_to_remove.sort() comments_to_remove.reverse() issue = db[issue_id] # Fetch IDs only (hence x[0]) existing_comments = [ x[0] for x in issue.comments(firm=False) ] existing_comments.sort existing_comments.reverse() for victim in comments_to_remove: if (not existing_comments) or (victim > existing_comments[0]): DITrack.Util.common.err( "No such comment: '%s.%s'" % (issue_id, victim) ) if victim < existing_comments[0]: DITrack.Util.common.err( "Can't remove '%s.%s': '%s.%s' is in the way" % (issue_id, victim, issue_id, existing_comments[0]) ) existing_comments.pop(0) # All correct. Remember the list. issue_comments[issue_id] = comments_to_remove # Now the removal loop itself. for issue_id in issues: for comment_id in issue_comments[issue_id]: db.remove_comment(issue_id, comment_id) DITrack-0.8/DITrack/Command/status.py0000644000076500007650000000412611032045266017062 0ustar vssvss00000000000000# # commit.py - DITrack 'commit' command # # Copyright (c) 2007 The DITrack Project, www.ditrack.org. # # $Id: status.py 1718 2007-07-16 16:02:21Z gli $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/status.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import string import sys # DITrack modules import DITrack.Command.generic class Handler(DITrack.Command.generic.Handler): canonical_name = "status" description = """List local changes to the issue database. usage: %s""" % canonical_name def run(self, opts, globals): self.check_options(opts) db = DITrack.Util.common.open_db(globals, opts) if len(opts.fixed) > 1: raise DITrack.Command.generic.SyntaxError for id, issue in db.lma_issues(): for cid, comment in issue.comments(firm=False): sys.stdout.write("%s.%s\n" % (id, cid)) DITrack-0.8/DITrack/Command/update.py0000644000076500007650000000501111032045266017013 0ustar vssvss00000000000000# # update.py - DITrack 'update' command # # Copyright (c) 2007 The DITrack Project, www.ditrack.org. # # $Id: update.py 1945 2007-08-24 23:39:43Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/update.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import string import sys # DITrack modules import DITrack.Command.generic class Handler(DITrack.Command.generic.Handler): canonical_name = "update" description = """Update firm entities. usage: %s""" % canonical_name def run(self, opts, globals): if len(opts.fixed) > 1: raise DITrack.Command.generic.SyntaxError self.check_options(opts) maxage = None if "maxage" in opts.var: try: maxage = int(opts.var["maxage"]) except ValueError: DITrack.Util.common.err( "Non-integer max age value: '%s'" % opts.var["maxage"], fatal=True ) db = DITrack.Util.common.open_db(globals, opts, "w") client = DITrack.Client.Client(db) try: status = client.update(maxage) except DITrack.Client.Error, e: DITrack.Util.common.err(e.message, fatal=True) # Report the status. if status: sys.stdout.write(str(status)) DITrack-0.8/DITrack/Common.py0000644000076500007650000001156311032045266015414 0ustar vssvss00000000000000# # Common.py - DITrack common functions # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: Common.py 1913 2007-08-17 20:14:53Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Common.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import string import re import urllib attachment_id_re = re.compile(r"^{[-_.%a-zA-Z0-9 ]+}$") class Identifier: """ Class representing an identifier (issue, comment or attachment). The following members are available: comment Available only if has_comment_part() returns True. An object of CommentIdentifier type which contains the comment component of the identifier. issue An object of IssueIdentifier containing the issue part of the identifier. """ def __init__(self, str): """ Parses out the identifier STR. Raises ValueError if the parsing fails. """ s = str.strip().split(".") # Attachment names can have '.' inside s = [s[0].upper(), ".".join(s[1:])] if len(s) == 0: raise ValueError, "Blank identifier passed" elif len(s) == 1: self.issue = IssueIdentifier(s[0]) elif len(s) == 2: self.issue = IssueIdentifier(s[0]) if s[1]: if s[1][0] == "{": self.attachment = AttachmentIdentifier(self.issue, s[1]) else: self.comment = CommentIdentifier(self.issue, s[1]) else: raise ValueError, "Can't parse out the identifier" def has_comment_part(self): """ Returns True if the identifier has a comment component. """ return "comment" in self.__dict__ def is_attachment_id(self): """ Returns true if it is an attachment identifier. """ return "attachment" in self.__dict__ class AttachmentIdentifier(Identifier): def __init__(self, issue_id, str): s = str.strip() if attachment_id_re.match(s): self.fname = urllib.unquote(s[1:-1]) else: raise ValueError, "Invalid attachment identifier" class CommentIdentifier(Identifier): def __init__(self, issue_id, str): s = str.strip().upper() if not s: raise ValueError, "Blank string passed as a comment identifier" if s[0] in string.digits: if not issue_id.is_numeric: # Case of 'A.1': we cannot have committed comment to # uncommitted issue. raise ValueError, \ "Numeric comment identifier of a non-numeric issue " \ "identifier" if not s.isdigit(): raise ValueError, "Expected numeric comment identifier" else: # Name if not (s.isalpha() and s.isupper()): raise ValueError, "Expected alphabetic comment identifier" self.id = s class IssueIdentifier(Identifier): def __init__(self, str): s = str.strip().upper() if not s: raise ValueError, "Empty string is not a valid issue id" self.is_numeric = False if s[0] in string.digits: # Numeric id if not s.isdigit(): raise ValueError, "Issue identifier is mixed alphanumeric" if int(s) == 0: raise ValueError, "0 is not a valid issue number" self.id = s self.is_numeric = True else: # Name if not (s.isupper() and s.isalpha()): raise ValueError, "Expected alphabetic issue identifier" self.id = s DITrack-0.8/DITrack/DB/0000755000076500007650000000000011032045407014066 5ustar vssvss00000000000000DITrack-0.8/DITrack/DB/__init__.py0000644000076500007650000000006011032045264016174 0ustar vssvss00000000000000__all__ = [ 'Configuration', 'Issue', ] DITrack-0.8/DITrack/DB/Cache.py0000644000076500007650000000452111032045264015446 0ustar vssvss00000000000000# # Cache.py - cache interface # # Copyright (c) 2006-2008 The DITrack Project, www.ditrack.org. # # $Id: Cache.py 2605 2008-06-23 03:07:37Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/DB/Cache.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import time # DITrack modules import DITrack.dt.globals import DITrack.DB.Common CACHE_FILE = "cache" # # Classes # class Cache: """ Class encapsulating cache operations. """ def __del__(self): self.cache.close() def __init__(self, path): """ PATH is a path to the issue database. """ self.cache = DITrack.DB.Common.open_local_dt_shelve(path, CACHE_FILE) self.version = DITrack.dt.globals.VERSION def __len__(self): return len(self.get()) def get(self): if ("data" in self.cache) and ("version" in self.cache) and \ (self.cache["version"] == self.version): return self.cache["data"] self.set([]) return [] def set(self, issues): self.cache["data"] = issues self.cache["version"] = self.version self.cache.sync() DITrack-0.8/DITrack/DB/Common.py0000644000076500007650000004660711032045264015706 0ustar vssvss00000000000000# # Common.py - common database classes and functions # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: Common.py 2439 2008-01-21 16:17:21Z gli $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/DB/Common.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import copy import os.path import pprint import re import string import errno import shelve import sys # DITrack modules import DITrack.DB.Configuration import DITrack.DB.Issue import DITrack.DB.Exceptions import DITrack.DB.LMA import DITrack.DB.WC from DITrack.Logging import DEBUG, get_caller import DITrack.SVN import DITrack.ThirdParty.Python.string import DITrack.Util.Locking # Root database directory property containing format version. VERSION_PROP = "ditrack:format" # Current database format version. FORMAT_VERSION = "4" # File with database format version DATABASE_VERSION_FILE = "format" # Local modifications area directory and file name LOCAL_DT_DIR = ".ditrack" # Lock file LOCK_FILE = "LOCK" def next_entity_name(names): """ Generates next entity name, given the set of existing ones. The sequence is A, B .. Z, AA, AB, .. AZ, BA, BB, ... ZZ """ if names: names = copy.copy(names) names.sort() prev = names[-1] else: return "A" assert(len(prev)) res = "" carry = True for c in prev[::-1]: assert(c >= "A") assert(c <= "Z") was_carry = carry carry = False if was_carry: if c == "Z": res = "A" + res carry = True else: res = chr(ord(c) + 1) + res else: res = c + res if carry: res = "A" + res return res def open_local_dt_shelve(path, filename): """ Opens a shelve in local DITrack directory (LOCAL_DT_DIR) which is created as needed. The name of the shelve to open is passed in FILENAME. Returns the shelve handle. """ datadir = os.path.join(path, LOCAL_DT_DIR) if not os.path.exists(datadir): os.mkdir(datadir, 0755) return shelve.open(os.path.join(datadir, filename)) # # Classes # class Database: """ Class representing complete database object. """ def __init__(self, path, username, svn_path, mode="r", timeout=0): """ XXX: rework this description, document the 'timeout' parameter. Opens a DITrack database at PATH. USERNAME is a username which is used to expand variables in filter definitions. SVN_PATH is a path to the Subversion command line client executable. MODE is the open mode, either "r" (for read-only operations) or "w" (for modifications). May raise the following exceptions: DITrack.DB.Exceptions.InvalidVersionError Invalid database format version. DITrack.DB.Exceptions.NotDatabaseError The directory pointed to by PATH doesn't look like a DITrack database. DITrack.DB.Exceptions.NotDirectoryError The PATH is not a directory. """ self.lock = None assert (mode == "r") or (mode == "w"), mode # Check that it's a directory. if not os.path.isdir(path): raise DITrack.DB.Exceptions.NotDirectoryError # Get database version try: v = open(os.path.join(path, DATABASE_VERSION_FILE), "r").readline().split()[0] except IOError: v = None if not v or not len(v): raise DITrack.DB.Exceptions.NotDatabaseError # Check if this is supported version. if v != FORMAT_VERSION: raise DITrack.DB.Exceptions.InvalidVersionError # Creating local ditrack dir datadir = os.path.join(path, LOCAL_DT_DIR) if not os.path.exists(datadir): os.mkdir(datadir, 0755) # Lock database try: self.lock = DITrack.Util.Locking.lock( open(os.path.join(path, LOCAL_DT_DIR, LOCK_FILE), "w"), mode, timeout) except DITrack.Util.Locking.FileIsLockedError: raise DITrack.DB.Exceptions.DBIsLockedError # Read up the configuration. self.cfg = DITrack.DB.Configuration.Configuration(path, username) if mode != "r": if username not in self.cfg.users: raise DITrack.DB.Exceptions.InvalidUserError # Set up local modifications area. self.lma = DITrack.DB.LMA.LocalModsArea(path) # Working copy interface. self.wc = DITrack.DB.WC.WorkingCopy(path, svn_path) def __getitem__(self, key): """ Return an issue by KEY (string id). Raises KeyError is the issue is neither in the WC, nor in the LMA. """ # XXX: make sure the key is a string. DEBUG("Retrieving issue '%s' (called from %s)" % (key, get_caller())) try: issue = self.wc[key] except KeyError: issue = None local = None try: local = self.lma[key] except KeyError: if not issue: raise assert(issue or local) if issue: if local: issue.merge_local_comments(local) issue.update_info() else: issue = local return issue def commit_comment(self, issue_number, comment_name): """ Commits comment COMMENT_NAME of issue ISSUE_NUMBER from the LMA. An assertion will fail if the ISSUE_NUMBER is not a firm one. Returns simple newly assigned comment number as a string. """ DEBUG("Committing comment '%s' of issue '%s' (called from %s)" % (comment_name, issue_number, get_caller())) # XXX: use is_valid_*() assert issue_number.isdigit() lma_issue = self.lma[issue_number] comment = lma_issue[comment_name] number, changes = self.wc.new_comment(issue_number, comment) # XXX: if the commit has failed, we need to revert the changes back. self.wc.commit(changes, "i#%s: %s\n%s" % (issue_number, self.wc[issue_number].info["Title"], comment.logmsg)) # Now, when the changes are committed, remove the comment from the LMA. self.lma.remove_comment(issue_number, comment_name) return number def commit_issue(self, name): """ Commits issue NAME from LMA. Returns newly assigned issue number. """ DEBUG("Committing issue '%s' (called from %s)" % (name, get_caller())) for x in name: assert(x in string.uppercase) issue = self.lma[name] number, changes = self.wc.new_issue(issue) # XXX: if the commit has failed, we need to revert the changes back. self.wc.commit( changes, "i#%d added: %s\n" "(assigned to %s, due in %s)" % ( number, issue.info["Title"], issue.info["Owned-by"], issue.info["Due-in"] ) ) # Now, when the changes are committed, remove the issue from the LMA. self.lma.remove_issue(name) return number def issue_by_id(self, id, err=True): """ Fetches an issue by ID from the database, checking if 1) the identifier is valid; 2) the issue actually exists. If either of these checks fails, raises DITrack.DB.Exceptions.IssueIdSyntaxError or KeyError respectively. If ERR is true, prints out diagnostics before raising the exception. """ if not self.is_valid_issue_id(id): if err: DITrack.Util.common.err("Invalid identifier: '%s'" % id) raise DITrack.DB.Exceptions.IssueIdSyntaxError, id try: issue = self[id] except KeyError: if err: DITrack.Util.common.err("No such entity: '%s'" % id) raise KeyError, id return issue def issues(self, from_wc=True, from_lma=True): """ Return a list of tuples (ID, ISSUE) for issues present in the database. Issues from working copy and LMA are included as prescribed by FROM_WC and FROM_LMA parameters respectively. The list returned is sorted. All WC issues precede LMA issues. """ DEBUG("from_wc=%s, from_lma=%s" % (from_wc, from_lma)) res = {} if from_wc: for id, issue in self.wc.issues(): res[id] = issue DEBUG("Firm issues: %s" % pprint.pformat(res)) if from_lma: for id, issue in self.lma.issues(): DEBUG("LMA issue entity: %s" % pprint.pformat(id)) if self.is_valid_issue_name(id): # It's a local issue, just store it. res[id] = issue else: # We are asked to return local issues only, don't bother # merging comments to firm ones. if not from_wc: continue # It's a local comment to a firm issue. We need to merge # issue headers. id = int(id) assert id in res, pprint.pformat(id) DEBUG("Merging local comments for issue '%s'" % id) res[id].merge_local_comments(issue) res[id].update_info() keys = res.keys() keys.sort() return [(k, res[k]) for k in keys] def is_valid_issue_number(self, id): issue_number_re = re.compile("^\\d+$") if issue_number_re.match(id): return int(id) != 0 else: return False def is_valid_issue_name(self, id): """ XXX: rename into valid_simple_name() XXX: move out of the class Checks if the ID passed is a syntactically valid simple entity name. 'Simple' means 'not compound', e.g. "A" is simple, "A.B" is not. """ issue_name_re = re.compile("^[A-Z]+$") return issue_name_re.match(id) def is_valid_issue_id(self, id): """ Checks if the passed identifier ID is a syntactically correct identifier (i.e. a valid number or name). """ return self.is_valid_issue_number(id) or self.is_valid_issue_name(id) def lma_issues(self, firm=True, local=True): """ Returns a list of tuples (ID, ISSUE) for all issue entities present in the LMA. The FIRM and LOCAL parameters control which kind of issues to include into the resulting list (either FIRM or LOCAL should be True; or both). The returned list is sorted, firm issues always precede local ones. """ assert (firm or local), "firm=%s, local=%s" % (firm, local) return self.lma.issues(firm=firm, local=local) def new_comment(self, issue_num, issue_before, issue_after, text, added_by, added_on): """ Add a new comment reflecting the change in issue ISSUE_NUM from ISSUE_BEFORE to ISSUE_AFTER with specified TEXT to the LMA. Returns tuple (NAME, COMMENT). ADDED_BY and ADDED_ON are the comment's author and addition date respectively. If there is no difference and the TEXT passed is empty, raises DITrack.DB.Exceptions.NoDifferenceCondition. XXX: reflect attachments logic """ delta = issue_before.diff(issue_after) attachment_delta = issue_before.diff_attachments(issue_after) if (not delta) and (not text) and (not attachment_delta): raise DITrack.DB.Exceptions.NoDifferenceCondition delta_map = {} for h, o, n in delta: delta_map[h] = (o, n) logmsg = [] for (name, (old_value, new_value)) in delta_map.iteritems(): if name == "Status": if new_value == "closed": s = "closed" if "Resolution" in delta_map: s += " as %s" % delta_map["Resolution"][1] logmsg.append(s) elif new_value == "open": logmsg.append("reopened") elif name == "Owned-by": logmsg.append("reassigned to %s" % new_value) elif name == "Due-in": logmsg.append("moved to %s" % new_value) elif name == "Resolution": # Used in "Status" condition. continue elif name == "Attachments": # Handled below (see attachment_delta). continue else: if old_value is None: old_value = "" if new_value is None: new_value = "" logmsg.append("headers changed: '%s': '%s' -> '%s'" % (name, old_value, new_value)) if attachment_delta: for action in ("added", "removed"): if (action in attachment_delta) and attachment_delta[action]: logmsg.append( "attachment(s) %s: %s" % ( action, ", ".join( map( lambda x: x.name, attachment_delta[action] ) ) ) ) logmsg_str = "".join ([" * %s\n" % k for k in logmsg]) if len(text): logmsg_str += "\n%s\n" % text comment = DITrack.DB.Issue.Comment.create(text, added_on=added_on, added_by=added_by, delta=delta, logmsg=logmsg_str, attachment_delta=attachment_delta) name = self.lma.new_comment(issue_num, comment) return name, comment def new_issue(self, title, opened_by, opened_on, owned_by, category, version_reported, version_due, description): """ Add a new issue to LMA of the database. Returns tuple (name, issue), where NAME is newly assigned name and ISSUE is newly created issue object. """ if not owned_by: owned_by = self.cfg.category[category].default_owner issue = DITrack.DB.Issue.Issue.create( title=title, opened_by=opened_by, opened_on=opened_on, owned_by=owned_by, category=category, version_reported=version_reported, version_due=version_due, description=description ) name = self.lma.new_issue(issue) return name, issue def remove_comment(self, issue_id, comment_name): """ Removes local comment from the database (e.g. from the LMA). ValueError is raised if the ISSUE_ID is not a syntactically valid issue number of if COMMENT_NAME is not a syntactically valid name. The COMMENT_NAME parameter should refer to an existing LMA entity (e.g. a comment of an existing issue), otherwise KeyError is raised. """ if not self.is_valid_issue_id(issue_id): raise ValueError if not self.is_valid_issue_name(comment_name): raise ValueError if not issue_id in self.lma: raise KeyError # XXX: check that KeyError will be raised if COMMENT_NAME is not # contained in the LMA issue self.lma.remove_comment(issue_id, comment_name) def update(self, maxage=None): """ Update the working copy (sync from the repository). If MAXAGE is None, update the database. If the database was updated more than MAXAGE seconds back, update the database. Otherwise return taking no action. XXX: return values are described in WC.update() """ return self.wc.update(maxage) class Filter: def __init__(self, str, username=None): """ Initialize the filter by parsing string expression STR. USERNAME is the user name to be set as the value for 'DT:USER' variable. """ # Remember the way the filter was defined self._expression = str # Is it a predefined filter name? inplace_re = re.compile("[,=]") if not inplace_re.search(str): # This looks like a predefined filter name raise DITrack.DB.Exceptions.FilterIsPredefinedError(str) vars = dict(os.environ) if username is not None: vars["DT:USER"] = username # Split into subclauses. clauses = filter(lambda x: len(x), str.split(",")) self.conditions = [] condition_re = re.compile( "^(?P[^!=]*)(?P!?=)(?P.*)$" ) for c in clauses: m = condition_re.match(c) if not m: raise DITrack.DB.Exceptions.FilterExpressionError(c) condition = m.groupdict() if len(condition) != 3: raise DITrack.DB.Exceptions.FilterExpressionError(c) if condition['value'] == '""' or condition['value'] == "''": condition['value'] = "" # Perform substitutions. t = DITrack.ThirdParty.Python.string.Template(condition['value']) try: condition['value'] = t.substitute(vars) except KeyError, var: sys.stderr.write( "Warning. Unknown variable in filters: %s\n" % var) condition['value'] = t.safe_substitute(vars) self.conditions.append(condition) def __str__(self): return self._expression def matches(self, issue): for c in self.conditions: assert len(c) == 3 if c['param'] not in issue.info: if c['value'] != "": return False else: info_value = "" else: info_value = issue.info[c['param']] if c['condition'] == "=": if info_value != c['value']: return False elif c['condition'] == "!=": if info_value == c['value']: return False else: raise DITrack.DB.Exceptions.NotImplemented, c['condition'] return True DITrack-0.8/DITrack/DB/Configuration.py0000644000076500007650000003050611032045264017254 0ustar vssvss00000000000000# # Configuration.py - database configuration # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: Configuration.py 1846 2007-08-04 11:25:09Z gli $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/DB/Configuration.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import email import email.Message import os.path import ConfigParser # DITrack modules import DITrack.DB.Common import DITrack.DB.Exceptions # # Classes # class VersionSet: """ A representation of a single version set. """ def __init__(self, past, current, future): self.past = past self.current = current self.future = future class VersionCfg: """ A representation of version sets configuration. """ def __contains__(self, key): return self.items.has_key(key) def __getitem__(self, key): return self.items[key] def __init__(self, path): f = open(path) versions = email.message_from_file(f) f.close() if len(versions.get_payload()): raise DITrack.DB.Exceptions.CorruptedDB_UnparseableVersionsError( "Empty line in version set configuration file") self.items = {} for s in versions.keys(): if s in self.items: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableVersionsError( "Duplicat version set '" + s + "' definition") v = versions[s].strip().split("/") if len(v) != 3: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableVersionsError( "Invalid version set '" + s + "' definition") self.items[s] = VersionSet( v[0].strip().split(), v[1].strip().split(), v[2].strip().split() ) class Category: def __init__(self, version_set, versions, default_owner, enabled): """ VERSION_SET Version set name. XXX: do we really need to pass both version_set and versions? """ self.version_set = version_set self.versions = versions self.default_owner = default_owner self.enabled = enabled class CategoryCfg(object): """ Representation of per-database category configuration. """ def __getitem__(self, key): return self.items[key] def __init__(self, cfgfile, users, versions): """ Parse category configuration file 'cfgfile'. Defined versions and users are passed in 'versions' and 'users' parameters respectively. They are used only for consistency checks and are not stored anywhere inside the object. """ f = open(cfgfile) sections = f.read().split("\n\n") f.close() category = {} for s in sections: if not len(s): continue c = email.message_from_string(s) if "Category" not in c: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableCategoriesError( "Invalid category definition (no 'Category' field)" ) if not len(c["Category"]): raise DITrack.DB.Exceptions.CorruptedDB_UnparseableCategoriesError( "Invalid category definition (blank 'Category' field)" ) if " " in c["Category"]: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableCategoriesError( "Invalid category '" + c["Category"] + "' definition: " "blank characters in name" ) if c["Category"] in category: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableCategoriesError( "Duplicate category '" + c["Category"] + "' definition") if "Version-set" not in c: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableCategoriesError( "Invalid category '" + c["Category"] + "' definition: " "no version set defined" ) if c["Version-set"] not in versions: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableCategoriesError( "Invalid category '" + c["Category"] + "' definition: " "unknown version set" ) if "Default-owner" not in c: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableCategoriesError( "Invalid category '" + c["Category"] + "' definition: " "no default owner defined" ) if c["Default-owner"] not in users: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableCategoriesError( "Invalid category '" + c["Category"] + "' definition: " "unknown user assigned as default owner" ) if c.has_key("Status"): status = c["Status"].strip() if status == "enabled": enabled = True elif status == "disabled": enabled = False else: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableCategoriesError( "Invalid category '" + c["Category"] + "' definition: " "invalid status" ) else: enabled = True category[c["Category"]] = Category( c["Version-set"], versions[c["Version-set"]], c["Default-owner"], enabled ) self.items = category def __iter__(self): return self.items.__iter__() def __len__(self): return len(self.items) def keys(self): return self.items.keys() class ListingFormatCfg: """ A representation of version sets configuration. """ def __contains__(self, key): return self.items.has_key(key) def __getitem__(self, key): try: return self.items[key] except KeyError: raise DITrack.DB.Exceptions.InvalidListingFormatError(key) def __init__(self, path): cfg = ConfigParser.ConfigParser() try: cfg.read(path) except ConfigParser.MissingSectionHeaderError: # XXX: Remove mention about listing format when single # configuration file will be used raise DITrack.DB.Exceptions.CorruptedDB_UnparseableListingFormatError( "ERROR: Incorrect configuration file %s:\n" "No section [listing formats] found" % path) if not cfg.has_section("listing-formats"): raise DITrack.DB.Exceptions.CorruptedDB_UnparseableListingFormatError( "ERROR: Incorrect configuration file %s:\n" "ERROR: No section [listing formats] found" % path) self.items = {} for key, value in cfg.items("listing-formats", True): self.items[key] = value; class UserCfg: """ A representation of user accounts configuration. """ def __contains__(self, key): return self.items.has_key(key) def __init__(self, fname): """ Parses user accounts configuration file 'fname'. """ users = {} f = open(fname) while 1: str = f.readline() if not str: break user = str.strip() if user in users: raise DITrack.DB.Exceptions.CorruptedDB_DuplicateUserError( "Duplicate user entry '" + user + "' in" " '" + fname + "'") users[user] = user f.close() self.items = users def __len__(self): return len(self.items) def keys(self): return self.items.keys() has_key = __contains__ class FilterCfg: """ A representation of filters configuration. """ def __contains__(self, key): return self.items.has_key(key) def __getitem__(self, key): return self.items[key] def __init__(self, path, username): f = open(path) filters = email.message_from_file(f) f.close() if len(filters.get_payload()): raise DITrack.DB.Exceptions.CorruptedDB_UnparseableFiltersError( "Empty line in filter configuration file") self.items = {} for s in filters.keys(): if s in self.items: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableFiltersError( "Duplicate filter " + s + "' definition") filter = filters[s].strip() if not filter: raise DITrack.DB.Exceptions.CorruptedDB_UnparseableFiltersError( "Empty filter " + s + "' definition") try: self.items[s] = DITrack.DB.Common.Filter(filter, username) except (DITrack.DB.Exceptions.FilterIsPredefinedError, DITrack.DB.Exceptions.FilterExpressionError): raise DITrack.DB.Exceptions.CorruptedDB_UnparseableFiltersError( "Syntax error in filter " + s + "' definition") def __len__(self): return len(self.items) def keys(self): return self.items.keys() has_key = __contains__ class Configuration: """ Database configuration object (everything under /etc in a database). Exported: category Categories configuration object, CategoryCfg. filters Filters configuration object, FilterCfg. path A mapping of strings identifying various objects to their respective locations. The keys currently supported are: / Root of the issue database. categories Location of categories configuration file. data Root of the issue data. filters Location of predefined filters configuration file. users Location of user accounts configuration file. version Location of versions configuration file. users User accounts configuration object, UserCfg. versions Version sets configuration object, VersionCfg. """ def __init__(self, path, username): # Prepare pathnames mapping. self.path = {} self.path["/"] = path self.path["categories"] = os.path.join(path, "etc", "categories") self.path["data"] = os.path.join(path, "data") self.path["filters"] = os.path.join(path, "etc", "filters") self.path["users"] = os.path.join(path, "etc", "users") self.path["versions"] = os.path.join(path, "etc", "versions") self.path["listing_format"] = os.path.join(path, "etc", "listing-format") self.users = UserCfg(self.path["users"]) self.versions = VersionCfg(self.path["versions"]) self.category = CategoryCfg(self.path["categories"], self.users, self.versions) self.listing_format = ListingFormatCfg(self.path["listing_format"]) try: self.filters = FilterCfg(self.path["filters"], username) except DITrack.DB.Exceptions.CorruptedDB_UnparseableFiltersError: raise DITrack-0.8/DITrack/DB/Exceptions.py0000644000076500007650000000722011032045264016563 0ustar vssvss00000000000000# # Exceptions.py - database exceptions # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: Exceptions.py 2273 2007-10-23 22:20:56Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/DB/Exceptions.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # # Corrupted DB exceptions # class CorruptedDBError(Exception): message = "Database is corrupted" class CorruptedDB_DuplicateUserError(CorruptedDBError): message = CorruptedDBError.message + ": duplicate user account entry" class CorruptedDB_UnparseableCategoriesError(CorruptedDBError): message = CorruptedDBError.message \ + ": unparseable categories configuration" class CorruptedDB_UnparseableVersionsError(CorruptedDBError): message = CorruptedDBError.message \ + ": unparseable version sets configuration" class CorruptedDB_UnparseableListingFormatError(CorruptedDBError): message = CorruptedDBError.message \ + ": unparseable listing format configuration" class CorruptedDB_UnparseableFiltersError(CorruptedDBError): message = CorruptedDBError.message \ + ": unparseable filter configuration" # # Other exceptions # class BadAttachmentNameError(Exception): message = "The attachment name can't be used in this transaction" class BackendError(Exception): message = "Backend failed" def __init__(self, msg, backend_exception): self.message = msg self.backend_e = backend_exception class DBIsLockedError(Exception): message = "Database is locked" class FilterExpressionError(Exception): message = "Syntax error in filter expression" class FilterIsPredefinedError(Exception): message = "This looks like a predefined filter name" class InconsistentActionError(Exception): message = "The action would lead to database inconsistency" class InvalidListingFormatError(Exception): message = "Invalid listing format" class InvalidUserError(Exception): message = "Invalid username" class InvalidVersionError(Exception): message = "Database format is not supported" class IssueIdSyntaxError(Exception): message = "Invalid issue identifier syntax" class NotDatabaseError(Exception): message = "Path specified is not an issue database root" class NotDirectoryError(Exception): message = "Database path is not an [existing] directory" # # Not errors, but various conditions. # class NoDifferenceCondition(Exception): message = "No difference between passed entities" DITrack-0.8/DITrack/DB/Issue.py0000644000076500007650000005566111032045264015546 0ustar vssvss00000000000000# # Issue.py - issue definition # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: Issue.py 2605 2008-06-23 03:07:37Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/DB/Issue.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import email import email.Message import os.path import re import sys # DITrack modules import DITrack.Common from DITrack.Logging import DEBUG, get_caller # Comment file name regular expression. comment_fname_re = re.compile("^comment(\\d+)$") class AttachedFile: """ Representation of a file attached to an issue. """ def __init__(self, shortname, path, is_local): self.name = shortname self.path = path self.is_local = is_local class _Attachments: """ File attachments storage for an issue. Internal use only. """ def __contains__(self, key): """ Check whether an attachment named KEY exists. """ return key in self._files def __getitem__(self, key): """ Returns the attached file object (AttachedFile) by its name. """ return self._files[key] def __init__(self): self._files = {} # A list of attachments (short names) removed from the storage since it # was created. It's needed to prevent removing an attachment and adding # another one with the same name within a single transaction. # # Only keys (which are short attachment names) matter. self._previously_removed = {} def __iter__(self): return self._files.iteritems() def add(self, fname, is_local, key=None): """ Adds file FNAME (full path) to the list of attachments. KEY is the "short" name used to refer to this attachment. If KEY is None, the base name of FNAME (e.g. its last component) is used as a key. The "local" flag for the attachment is set according to IS_LOCAL. May raise the same exceptions as add_object(). """ if key is None: key = os.path.basename(fname) self.add_object(AttachedFile(key, fname, is_local)) def add_object(self, af): """ Adds AttachedFile object AF to the storage. AF.name is used as a key. May raise the following exceptions: BadAttachmentNameError Can't use the attachment name since another attachment with similar name was removed in the same transaction. ValueError Another attachment with the same [base] name is already present. """ if af.name in self._files: raise ValueError, af.name if af.name in self._previously_removed: raise DITrack.DB.Exceptions.BadAttachmentNameError(af.name) self._files[af.name] = af def keys(self): """ Returns a list of the attached file names in alphabetical order. """ keys = self._files.keys() keys.sort() return keys def remove(self, key): """ Removes a file referred to by KEY (short name) from the list of attachments. Assertion will fail if the KEY is not actually in the list. """ assert key in self._files, key assert key not in self._previously_removed, key del self._files[key] self._previously_removed[key] = True class Issue: """ A representation of an issue record. """ def __contains__(self, key): """ Checks whether a comment indexed by the string KEY is present in the issue. """ present = (key in self.local_names) or (key in self.firm_names) if present: assert(key in self.comment) return present def __getitem__(self, key): """ Returns comment object indexed by KEY (which should be a string). """ # XXX: Make sure the key is a string. if key not in self: raise KeyError return self.comment[key] def __init__(self): # XXX: all other data members should also be prefixed with '_', since # they are not publicly available. self._attachments = _Attachments() self.comment = {} self.firm_names = [] self.local_names = [] def __len__(self): """ Returns total number of attached comments. """ return len(self.firm_names) + len(self.local_names) def add_attachment(self, fname, is_local=True): """ Store a reference to file FNAME as an attachment. This method merely adds the file _name_ to the list of attached files, without actually copying data or modifying the working copy. The attachment's "local" flag is set according to IS_LOCAL. XXX: raises the same exceptions as _Attachments.add() """ self._attachments.add(fname, is_local) self.update_attachments_header() def add_comment(self, comment, is_local, update_info=True): """ Adds comment COMMENT to the list of comments and names it according to IS_LOCAL. If UPDATE_INFO is true, updates the issue info afterwards. Returns simple comment id as a string. """ DEBUG("Adding comment: is_local=%s, update_info=%s (called from %s)" % (is_local, update_info, get_caller())) # XXX assert(is_local) name = DITrack.DB.Common.next_entity_name(self.local_names) assert(name not in self.comment) self.comment[name] = comment self.local_names.append(name) if update_info: self.update_info() return name def attachments(self): """ Returns a list of AttachedFile objects for all files attached to the issue in alphabetical order of their names. """ return map(lambda x: self._attachments[x], self._attachments.keys()) def change_due_version(self, version): """ Changes due version of the issue to VERSION. No sanity checks are made. """ self.replace_header("Due-in", version) def close(self, resolution): """ Closes the issue with specified RESOLUTION (which should be one of "dropped", "fixed" or "invalid"). Assertion will fail if the issue is already closed or there is already a "Resolution" header present. XXX: we should probably raise an exception, not fire an assert XXX: resolution enumeration should be defined elsewhere (in a single place for all modules that use it). """ if self.info["Status"] != "open": raise DITrack.DB.Exceptions.InconsistentActionError( "The issue is not open" ) self.replace_header("Status", "closed") assert "Resolution" not in self.info self.info["Resolution"] = resolution def comments(self, firm=True, local=True): """ Returns a list of tuples (ID, COMMENT) for all existing comments. IDs are strings. First all firm comments go in order, then the local ones. FIRM and LOCAL prescribe which comments to include into the result. """ assert firm or local if firm: # We assume the list is sorted. names = self.firm_names else: names = [] result = [(id, self.comment[id]) for id in names] if local: names = self.local_names names.sort() result.extend([(id, self.comment[id]) for id in names]) return result def create(cls, title, opened_by, opened_on, owned_by, category, version_reported, version_due, description): """ Create new issue instance from scratch. """ issue = cls() # Create an initial comment. comment = Comment.create( text=description, added_by=opened_by, added_on=opened_on, delta=[ ("Opened-by", None, opened_by), ("Opened-on", None, opened_on), ("Owned-by", None, owned_by), ("Title", None, title), ("Category", None, category), ("Status", None, "open"), ("Reported-for", None, version_reported), ("Due-in", None, version_due) ]) # Now append the comment to the issue. issue.add_comment(comment, is_local=True) return issue create = classmethod(create) def diff(self, other): """ Returns a difference in headers between SELF and the OTHER issue, by checking the headers. The changed headers are returned as a list of tuples (HEADER, OLD, NEW), where the HEADER is the header name and the OLD and NEW are the old and new values respectively (any of these may be None, but not both). XXX: should be renamed to diff_headers """ delta = [] for h in self.info.keys(): if h in other.info: if self.info[h] != other.info[h]: delta.append((h, self.info[h], other.info[h])) else: delta.append((h, self.info[h], None)) for h in other.info.keys(): if h not in self.info: delta.append((h, None, other.info[h])) return delta def diff_attachments(self, another): """ Figure out the difference in attachments between SELF and ANOTHER issue. If there is no difference, None is returned. Otherwise returns a dictionary with keys "added" and "removed", where corresponding values are the lists of added and removed files. The items of the lists are instances of AttachedFile objects. """ added = [] removed = [] # Note: the contents of attached file can't change. It should be done # via delete/add operation. for name, attach in self._attachments: if name not in another._attachments: removed.append(attach) for name, attach in another._attachments: if name not in self._attachments: added.append(attach) if (not added) and (not removed): return None return { "added": added, "removed": removed } def get_attachment(self, fname): """ Returns AttachedFile object of an attachment named FNAME. Raises KeyError if there is no such attachment. """ return self._attachments[fname] def info_as_strings(self, terminator="\n"): """ Returns a list of strings, representing the issue info, one header per line. The list is sorted by header names. The lines are terminated with the specified TERMINATOR. """ keys = self.info.keys() keys.sort() return map(lambda k: "%s: %s%s" % (k, self.info[k], terminator), keys) def is_up_to_date(self, path): """ Check if the issue is up to date with respect to its on-disk representation at PATH (should point to the issue directory). """ n = int(self.firm_names[-1]) fname = os.path.join(path, "comment%d" % (n + 1)) return not os.path.exists(fname) def load(cls, path): """ Load an issue from path PATH (should point to a directory). """ DEBUG("Loading an issue from '%s' (called from %s)" % (path, get_caller())) issue = cls() comments = {} keys = [] for fn in os.listdir(path): m = comment_fname_re.match(fn) if m: n = m.group(1) fname = os.path.join(path, "comment" + n) comments[int(n)] = Comment.load(fname) keys.append(int(n)) keys.sort() issue.firm_names = [] # k is numeric here. for k in keys: key = "%d" % k issue.comment[key] = comments[k] issue.firm_names.append(key) issue.update_info() # Take care of the attachments now. # XXX: should be a constant if "Attachments" in issue.info: # XXX: there should be a function to parse this out attachments = issue.info["Attachments"].strip().split() for a in attachments: # Check that the attachment is really there a_path = os.path.join(path, "attaches", a) # XXX: this should be an exception assert os.path.exists(a_path) issue.add_attachment(a_path, is_local=False) return issue load = classmethod(load) def merge_local_comments(self, local): """ Merges all local comments from the LOCAL issue to SELF, retaining the names as is. Names collision will raise an assertion fault. NB! Does NOT update the info, use the update_info() method for that. """ for name in local.local_names: DEBUG("Merging local comment '%s' (called from %s)" % (name, get_caller()) ) assert name not in self.local_names, name self.comment[name] = local.comment[name] self.local_names.extend(local.local_names) self.local_names.sort() def reassign(self, new_owner): """ Reassigns the issue to NEW_OWNER. No checks are made on the validity of the user name passed. """ self.replace_header("Owned-by", new_owner) def remove_attachment(self, fname): """ Removed FNAME from the list of attached files (no on-disk operations are performed) and updates the corresponding header with update_attachments_header(). FNAME is a short name. """ self._attachments.remove(fname) self.update_attachments_header() def remove_comment(self, name): """ Removes a local comment named NAME from the issue. The comment should exist, otherwise KeyError is raised. The info is *not* updated. """ if name not in self.local_names: raise KeyError, name assert name in self.comment, "name='%s'" % name del self.comment[name] self.local_names.remove(name) def reopen(self): """ Reopens the issue (which must be closed, otherwise the assertion will fail). An assertion will also fail if there is no "Resolution" header. """ if self.info["Status"] != "closed": raise DITrack.DB.Exceptions.InconsistentActionError, \ "The issue is not closed" # i#95: work around absent resolution header. try: del self.info["Resolution"] except KeyError: pass self.replace_header("Status", "open") def replace_header(self, name, value): """ Replaces the issue header NAME with VALUE. The header should exist. """ del self.info[name] self.info[name] = value def update_attachments_header(self): """ Updates the 'Attachments' info header with the current status of attached files. XXX: should be internal """ self.info["Attachments"] = " ".join(self._attachments.keys()) def update_info(self): """ Goes through all the comments and updates the info and attachments list accordingly (applies deltas in sequence). May be used repeatedly. """ self.info = {} names = self.local_names names.sort() names = self.firm_names + names DEBUG("Updating the info (called from %s)" % get_caller()) for name in names: DEBUG("Processing comment '%s'" % name) self.comment[name].apply_delta(self.info, self._attachments) DEBUG("Updated info: %s" % self.info) def write_info(self): """ Prints out current issue headers. """ sys.stdout.writelines(self.info_as_strings()) class Comment: """ A representation of a comment record. """ def apply_delta(self, info, attachments): """ Applies own delta to the info dictionary INFO. ATTACHMENTS is an object representing attached files. It should provide methods add() and remove() to apply attachment deltas. XXX: should deal with conflicts """ # XXX: should check for duplicate headers in delta DEBUG("Applying the delta") for header, old, new in self.delta: DEBUG("Delta: header='%s' old='%s' new='%s'" % (header, old, new)) # XXX if old: assert header in info, "header = '%s'" % header assert(info[header] == old) if new: info[header] = new else: del info[header] else: assert(new is not None) assert(header not in info) info[header] = new # Process attachments if self.attachments_changed(): added = self.attachments_added() removed = self.attachments_removed() DEBUG("Processing attachment delta (%d added files)" % len(added)) for a in added: DEBUG("attachment name: %s" % a.name) assert a.name not in attachments, \ "%s shouldn't be in %s" % (a.name, attachments.keys()) attachments.add_object(a) DEBUG("Processing attachment delta (%d removed files)" % len(removed)) for a in removed: DEBUG("attachment name: %s" % a.name) assert a.name in attachments, \ "%s should be in %s" % (a.name, attachments.keys()) attachments.remove(a.name) # XXX: should make use of update_attachments_header info["Attachments"] = " ".join(attachments.keys()) else: DEBUG("Attachment delta is empty") def attachments_added(self): """ Returns a list of added attachments for this comment or None if there is none. The returned list is a list of AttachedFile objects. """ if self.attachments_changed(): return self._attachment_delta["added"] def attachments_changed(self): """ Returns True if this comment makes any changes to the attached files. Assertions will fail if the data is inconsistent in any way. """ if self._attachment_delta is None: return False assert "added" in self._attachment_delta assert "removed" in self._attachment_delta return self._attachment_delta["added"] or \ self._attachment_delta["removed"] def attachments_removed(self): """ Returns a list of removed attachments for this comment or None if there is none. The returned list is a list of AttachedFile objects. """ if self.attachments_changed(): return self._attachment_delta["removed"] def create(cls, text, added_on, added_by, delta, logmsg="", attachment_delta=None): comment = cls() comment.text = text comment.added_on = added_on comment.added_by = added_by comment.delta = delta comment.logmsg = logmsg comment._attachment_delta = attachment_delta return comment create = classmethod(create) def headers(self): """ Constructs the comment header and returns it as a list of tuples (NAME, VALUE) sorted alphabetically by the header name. """ output = [] output.append(("Added-on", "%s" % self.added_on)) output.append(("Added-by", "%s" % self.added_by)) for header, old, new in self.delta: assert((old is not None) or (new is not None)) old = old or "" new = new or "" output.append(("DT-Old-%s" % header, old)) output.append(("DT-New-%s" % header, new)) output.sort(lambda (a_k, a_v), (b_k, b_v): cmp(a_k, b_k)) return output def header_as_strings(self, terminator="\n"): """ Constructs the comment header and returns it as a list of strings, sorted alphabetically by the header name and terminated with TERMINATOR. """ return ["%s: %s%s" % (k, v, terminator) for (k, v) in self.headers()] def load(cls, path): """ Load a comment from path PATH. """ f = open(path) data = email.message_from_file(f) f.close() # XXX: we should also handle user headers here (somehow). # To ignore case difference, put all headers in our own map. header = {} orig = {} for h in data.keys(): header[h.lower()] = data[h] orig[h.lower()] = h delta = [] for h in header.keys(): h = h.lower() if h.startswith("dt-old-"): name = h[7:] nh = "dt-new-" + name assert(nh in header) if not (data[h] or data[nh]): continue delta.append((orig[h][7:], data[h], data[nh])) return Comment.create( text=data.get_payload(), added_on=data["Added-on"], added_by=data["Added-by"], delta=delta ) load = classmethod(load) def write(self, dest=sys.stdout, headers_only=False, display_headers=True): """ Write out the comment into file object DEST. """ if display_headers: dest.writelines(self.header_as_strings()) if not headers_only: if display_headers: dest.write("\n") dest.write("%s" % self.text) DITrack-0.8/DITrack/DB/LMA.py0000644000076500007650000001224611032045264015057 0ustar vssvss00000000000000# # LMA.py - local modifications area interface # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: LMA.py 2425 2007-12-25 08:38:11Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/DB/LMA.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import os.path # DITrack modules import DITrack.DB.Issue import DITrack.Common from DITrack.Logging import DEBUG LMA_FILE = "LMA" class LocalModsArea: """ Class representing local modifications area. MEMBERS: data A mapping of existing LMA issues: maps identifiers (as strings) to issue objects. """ def __contains__(self, key): """ Check if the issue KEY (string) is contained in the LMA. """ return key in self.data; def __del__(self): self.data.close() def __getitem__(self, key): return self.data[key] def __init__(self, path): self.data = DITrack.DB.Common.open_local_dt_shelve(path, LMA_FILE) def _comment_keys(self, issue_id): """ Return a sorted list of comment names for all comments of the issue ISSUE_ID in the LMA. """ prefix = "%s." % issue_id ids = filter(lambda x: x.startswith(prefix), self.data.keys()) ids.sort() return ids def comments(self, issue_id): """ Return a sorted list of tuples (ID, COMMENT) for all comments of the issue ISSUE_ID in the LMA. """ return [(x, self.data[x]) for x in self._comment_keys(issue_id)] def issues(self, firm=True, local=True): """ Return a sorted list of tuples (ID, ISSUE) for all issues in the LMA. The FIRM and LOCAL parameters control which kind of issues to include into the resulting list. Firm issues precede local ones in the result. Either FIRM or LOCAL should be True (or both). """ assert firm or local, "firm=%s, local=%s" % (firm, local) # XXX: replace with is_valid_XXX() once those are available for the # whole module. def is_firm(s): try: int(s) except ValueError: return False return True keys = filter( lambda x: (firm and is_firm(x)) or (local and not is_firm(x)), self.data.keys() ) keys.sort() return [(k, self.data[k]) for k in keys] def new_comment(self, issue_id, comment): """ Add issue ISSUE_ID (string) COMMENT to the LMA. Returns newly assigned comment name. """ DEBUG("Creating a new comment in the LMA for issue '%s'" % issue_id) try: issue = self.data[issue_id] except KeyError: # The very first local comment to the issue. issue = DITrack.DB.Issue.Issue() # Don't update the info, since this issue is merely a collection of # comments. # # XXX: make it a list then? name = issue.add_comment(comment, is_local=True, update_info=False) self.data[issue_id] = issue return name def new_issue(self, issue): # Figure out a name for the new issue name = DITrack.DB.Common.next_entity_name( [k for k, i in self.issues(firm=False)] ) self.data[name] = issue return name def remove_comment(self, issue_number, comment_name): """ Removes comment COMMENT_NAME from the issue ISSUE_NUMBER in the LMA. The issue and comment should exist. If the issue has no comments after the removal, removes the issue altogether. """ assert issue_number in self.data, "issue_number='%s'" % issue_number issue = self.data[issue_number] issue.remove_comment(comment_name) if len(issue): self.data[issue_number] = issue else: del self.data[issue_number] def remove_issue(self, name): del self.data[name] DITrack-0.8/DITrack/DB/WC.py0000644000076500007650000003235211032045264014757 0ustar vssvss00000000000000# # WC.py - working copy interface # # Copyright (c) 2006-2008 The DITrack Project, www.ditrack.org. # # $Id: WC.py 2605 2008-06-23 03:07:37Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/DB/WC.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import datetime import errno import os.path import re import shutil # DITrack modules import DITrack.SVN import DITrack.DB.Cache import DITrack.DB.Issue # Comment file name regular expression. comment_fname_re = re.compile("^comment(\\d+)$") META_FILE = "meta" # # Classes # # XXX: should use inheritance to avoid code duplication in different Ops. class WC_Operation: """ Class representing a single operation in a working copy. """ def __init__(self): raise NotImplementedError def fill_txn(self, txn): """ Include the change into given Subversion transaction TXN. """ raise NotImplementedError, self class WC_Op_Modification(WC_Operation): """ Modification of an existing file. """ def __init__(self, fname): self.fname = fname def fill_txn(self, txn): # Just record the file name in the transaction. txn.include(self.fname) class WC_Op_NewDirectory(WC_Operation): """ New directory creation. """ def __init__(self, fname): self.fname = fname def fill_txn(self, txn): txn.add(self.fname) class WC_Op_NewFile(WC_Operation): """ New file addition. """ def __init__(self, fname): self.fname = fname def fill_txn(self, txn): txn.add(self.fname) class WC_Op_RemovedFile(WC_Operation): """ A file removal. """ def __init__(self, fname): self.fname = fname def fill_txn(self, txn): txn.remove(self.fname) class WorkingCopy: """ Class encapsulating working copy operations. """ def __del__(self): self.meta.close() def __getitem__(self, key): """ Read an issue named by KEY from the working copy. """ # We deal with issues only (not comments). assert("." not in key) issue_dir = self._issue_path(key) return DITrack.DB.Issue.Issue.load(issue_dir) def __init__(self, path, svn_path): """ PATH is a path to an issue database. SVN_PATH is a path to Subversion command-line client. """ self.path = path self.data_path = os.path.join(path, "data") self.next_num_path = os.path.join(path, "meta", "next-id") self.svn = DITrack.SVN.Client(svn_path) self.meta = DITrack.DB.Common.open_local_dt_shelve(path, META_FILE) self.cache = DITrack.DB.Cache.Cache(path) def _get_next_comment_number(self, path): """ Look up the issue directory PATH (it has to exist) and return next available comment number. """ assert os.path.exists(path), "path='%s'" % path numbers = [] for fn in os.listdir(path): m = comment_fname_re.match(fn) if m: numbers.append(int(m.group(1))) numbers.sort() if numbers: return numbers[-1] + 1 else: return 0 def _get_next_issue_number(self): """ Look up the next available issue number and return it. """ if not os.path.exists(self.next_num_path): raise DITrack.DB.Exceptions.CorruptedDBError( self.next_num_path + " doesn't exist") f = open(self.next_num_path) s = f.readline() if not s: f.close() raise DITrack.DB.Exceptions.CorruptedDBError( self.next_num_path + " is empty") try: num = int(s.strip()) except ValueError: raise DITrack.DB.Exceptions.CorruptedDBError( "Contents of " + self.next_num_path + " are invalid") f.close() return num def _issue_path(self, id): """ Returns directory path of issue ID. Raises KeyError if no such issue exists. """ issue_dir = os.path.join(self.data_path, "i%s" % id) if not os.path.exists(issue_dir): raise KeyError return issue_dir def _set_next_issue_number(self, num): """ Update the next available issue number. Returns WC_Operation obejct. """ f = open(self.next_num_path, "w") f.write("%s\n" % num) f.close() return WC_Op_Modification(self.next_num_path) def comments(self, issue_id): """ Return a sorted list of tuples (ID, COMMENT) for all comments of the issue ISSUE_ID in the working copy. """ # XXX: do we need this? issue_path = self._issue_path(issue_id) comments = {} for fn in os.listdir(issue_path): m = comment_fname_re.match(fn) if m: n = m.group(1) fname = os.path.join(issue_path, "comment" + n) comments[int(n)] = DITrack.DB.Issue.Comment.load(fname) keys = comments.keys() keys.sort() return [(x, comments[x]) for x in keys] def commit(self, changes, logmsg): """ Attempts to commit the CHANGES list of operations with log message LOGMSG. """ txn = self.svn.start_txn() for op in changes: op.fill_txn(txn) # XXX: if failed to commit, revert local changes. txn.commit(logmsg) def issues(self): """ Return a sorted list of tuples (ID, ISSUE) for all issues in the working copy. """ res = [] issues = self.cache.get() if issues: for id, issue in issues: path = os.path.join(self.data_path, "i%s" % id) if not issue.is_up_to_date(path): issue = DITrack.DB.Issue.Issue.load(path) res.append((id, issue)) while True: id += 1 path = os.path.join(self.data_path, "i%s" % id) if os.path.isdir(path): issue = DITrack.DB.Issue.Issue.load(path) res.append((id, issue)) else: break else: issue_re = re.compile("^i(\\d+)$") lst = [] for fn in os.listdir(self.data_path): m = issue_re.match(fn) if m: lst.append(int(m.group(1))) lst.sort() for id in lst: path = os.path.join(self.data_path, "i%s" % id) issue = DITrack.DB.Issue.Issue.load(path) res.append((id, issue)) self.cache.set(res) return res def new_comment(self, issue_num, comment): """ Writes a comment COMMENT for the issue ISSUE_NUM to the working copy and returns a tuple (NUMBER, OPS) where the NUMBER is a newly assigned comment number and the OPS is a list of corresponding operations. """ issue_dir = self._issue_path(issue_num) # Fetch next comment number num = self._get_next_comment_number(issue_dir) fname = os.path.join(issue_dir, "comment%d" % num) # Write out the comment. f = open(fname, 'w') comment.write(dest=f) f.close() # Remember that ops = [WC_Op_NewFile(fname)] # Handle the attachments if comment.attachments_changed(): # XXX: What if the names collide? I.e. a user has removed # [previously existent] file 'a.txt' and then added another # 'a.txt'? This should be prohibited. added = comment.attachments_added() removed = comment.attachments_removed() attaches_path = os.path.join(issue_dir, "attaches") # If we are removing something, it means that the attaches # directory is already there, otherwise it's a corrupted working # copy. if removed: if not os.path.exists(attaches_path): raise DITrack.DB.Exceptions.CorruptedDBError( "%s doesn't exist; yet comment %d tries to remove " "attachment(s)" % (attaches_path, num) ) for a in removed: attach_fname = os.path.join(attaches_path, a.name) assert os.path.exists(attach_fname) os.unlink(attach_fname) ops.append(WC_Op_RemovedFile(attach_fname)) else: # No attachments removed. So we might need to create the # attachments directory. add_dir = True try: os.mkdir(attaches_path) except OSError, (err, str): if err == errno.EEXIST: # XXX: Assuming its addition has already been # scheduled. This might not be true though. add_dir = False else: raise if add_dir: ops.append(WC_Op_NewDirectory(attaches_path)) for a in added: assert a.is_local attach_fname = os.path.join(attaches_path, a.name) # XXX: shouldn't be assertions really assert os.path.exists(a.path), "path=%s" % a.path assert not os.path.exists(attach_fname), \ "path=%s" % attach_fname shutil.copyfile(a.path, attach_fname) ops.append(WC_Op_NewFile(attach_fname)) return num, ops def new_issue(self, issue): """ Writes issue ISSUE data to the working copy and returns a tuple (NUMBER, OPS) where the NUMBER is a newly assigned issue number and the OPS is a list of corresponding operations. """ # Fetch next issue number num = self._get_next_issue_number() issue_dir = os.path.join(self.data_path, "i%s" % num) ops = [] # Update next issue numer ops.append(self._set_next_issue_number(num + 1)) # Create the issue directory os.mkdir(issue_dir, 0755) # Remember that we've created the directory ops.append(WC_Op_NewDirectory(issue_dir)) # XXX: for now we assume that the very first comment in this issue is # named 'A'. This may not be actually the case. Also, we write out one # comment only (as 'comment0'). assert("A" in issue) fname = os.path.join(issue_dir, "comment0") # Write out the info f = open(fname, 'w') issue["A"].write(dest=f) f.close() # Remember that ops.append(WC_Op_NewFile(fname)) return num, ops def update(self, maxage=None): """ Update the working copy (sync with the repository). If MAXAGE is None, update the database. If the database was updated more than MAXAGE seconds back, update the database. Otherwise return taking no action. On success returns the UpdateStatus object returned by the backend or None if no update happened. On failure raises DITrack.DB.Exceptions.BackendError, with the backend exception object stored inside for later examination. """ if (maxage is not None) and ("last_update" in self.meta): maxage = int(maxage) assert maxage >= 0, maxage maxage = datetime.timedelta(days=0, seconds=maxage) delta = datetime.datetime.today() - self.meta["last_update"] if delta <= maxage: # No update needed return None try: us = self.svn.update(self.path) except DITrack.Backend.Common.Error, e: raise DITrack.DB.Exceptions.BackendError( "Failed to update the database", e ) self.meta["last_update"] = datetime.datetime.today() return us DITrack-0.8/DITrack/dt/0000755000076500007650000000000011032045407014210 5ustar vssvss00000000000000DITrack-0.8/DITrack/dt/__init__.py0000644000076500007650000000000011032045265016311 0ustar vssvss00000000000000DITrack-0.8/DITrack/dt/globals.py0000644000076500007650000001053111032045265016207 0ustar vssvss00000000000000# # dt - DITrack command-line client globals # # Copyright (c) 2006-2008 The DITrack Project, www.ditrack.org. # # $Id: globals.py 2618 2008-06-30 03:00:45Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/dt/globals.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import datetime import os.path import stat import sys import time import DITrack.Util.common VERSION = "0.8" class Globals: def __init__(self): # Our version. self.version = VERSION self.dt_title = "DITrack command-line client, version %s" % \ self.version # Figure out the binary name. self.binname = os.path.basename(sys.argv[0]) # Username can be defined with get_username() self.username = None # Timezone string. if time.daylight: tz = time.altzone else: tz = time.timezone if tz > 0: offs = "-" elif tz == 0: offs = " " else: offs = "+" tz = -tz self.timezone_string = offs + \ ("%02d%02d" % (tz / 3600, (tz / 60) % 60)) # Path to svn executable. if os.name == "posix": self.svn_path = "svn" elif os.name == "nt": self.svn_path = "svn.exe" # Editor is not necessarily required for any command. self.editor = "" self.text_delimiter = "=" * 78 def fmt_timestamp(self): """ Returns the current time string formatted. """ weekdays = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") now = datetime.datetime.now() return now.strftime("%s %Y-%m-%d %H:%M:%S ") + \ weekdays[now.weekday()] + " " + self.timezone_string def get_editor(self): "Figure out user editor; bail out if something goes wrong." if len(self.editor): return notdefined = "Text editor is not defined (use EDITOR " \ "environment variable)" if not os.environ.has_key("EDITOR"): DITrack.Util.common.err(notdefined) self.editor = os.environ["EDITOR"] msg = "Text editor '" + self.editor + "' (set with " \ "EDITOR environment variable) " if len(self.editor) == 0: DITrack.Util.common.err(notdefined) if self.editor[0] == "/": if not os.path.exists(self.editor): DITrack.Util.common.err(msg + "doesn't exist") if not os.path.isfile(self.editor): DITrack.Util.common.err(msg + "is not a file") st = os.stat(self.editor) if not (st[stat.ST_MODE] & stat.S_IEXEC): DITrack.Util.common.err(msg + "is not executable") def get_username(self, opts): """ Figure out the user name, using the environment and passed options OPTS. """ name = None if (opts) and ("user" in opts.var): name = opts.var["user"] elif "DITRACK_USER" in os.environ: name = os.environ["DITRACK_USER"] elif "USER" in os.environ: name = os.environ["USER"] self.username = name DITrack-0.8/DITrack/Edit.py0000644000076500007650000000455611032045266015055 0ustar vssvss00000000000000# # Edit.py - DITrack module dealing with editions # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: Edit.py 1696 2007-07-10 22:45:31Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Edit.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import os import tempfile import DITrack.Util.Misc class EditorInvocationError(Exception): "Editor didn't finish successfully" pass def edit_text(globals, text): """ Create temporary file, dump 'text' into that and invoke user editor. Check if the editor is finished successfully, ask user if he is satisfied with the edition and return either new version of the text or the old one. """ assert(len(globals.editor) > 0) (fd, name) = tempfile.mkstemp(prefix = "ditrack") os.write(fd, text) os.close(fd) # Invoke editor. args = [ globals.editor, name ] rc = DITrack.Util.Misc.spawnvp(os.P_WAIT, globals.editor, args) if rc: os.unlink(name) raise EditorInvocationError(rc) # Rewind and read. f = open(name) text = f.read() # Close and delete. f.close() os.unlink(name) return text DITrack-0.8/DITrack/Logging.py0000644000076500007650000000442011032045267015545 0ustar vssvss00000000000000# # Logging.py - DITrack logging (not client-specific) # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: Logging.py 2399 2007-11-28 08:38:58Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Logging.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import os.path import sys import traceback import logging def get_caller(): """ Returns the grand-caller of the function in a form of string "[FILE:LINE]". """ tb = traceback.extract_stack(limit=3)[0] s = os.path.split(tb[0]) fname = os.path.join(os.path.split(s[0])[1], s[1]) return "[%s:%d]" % (fname, tb[1]) def DEBUG(str, show_caller=False): """ Sends STR to logging.debug(), prepending it with the file name and line of the caller if SHOW_CALLER is True. """ logging.debug("%s %s" % (get_caller(), str)) def init(): if "DITRACK_DEBUG" in os.environ: if "c" not in os.environ["DITRACK_DEBUG"]: global get_caller get_caller = lambda: "undef" logging.getLogger().setLevel(logging.DEBUG) DITrack-0.8/DITrack/SVN.py0000644000076500007650000001567511032045267014643 0ustar vssvss00000000000000# # SVN.py - Subversion interface # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: SVN.py 1944 2007-08-24 23:38:42Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/SVN.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import os import re import sys import DITrack.Backend.Common import DITrack.Util.Misc at_rev_re = re.compile("^At revision (\\d+).$") update_file_re = re.compile("^([ADU])\\s+(.+)$") updated_rev_re = re.compile("^Updated to revision (\\d+).$") class Error(DITrack.Backend.Common.Error): def __init__(self, cmd, message, details): # XXX: we should really call the parent class constructor here self.cmd = cmd self.message = message self.details = details class UpdateStatus(DITrack.Backend.Common.UpdateStatus): """ Class representing a result of the update operation. """ def __init__(self, lines): """ LINES is the list of lines produced by 'svn up' command to be parsed. Raises ValueError is the data passed is somehow unparseable. """ if not lines: raise ValueError, "Empty input" self.modifications = [] if len(lines) == 1: m = at_rev_re.match(lines[0]) if not m: raise ValueError, "At unknown revision" else: m = updated_rev_re.match(lines[-1]) if not m: raise ValueError, "Updated to unknown revision" for str in lines[:-1]: str = str.rstrip() fm = update_file_re.match(str) if not fm: raise ValueError, "Can't parse: '%s'" % str status, fname = fm.group(1), fm.group(2) # XXX: will need to probably wrap into a class later self.modifications.append((status, fname)) # Should convert without problem since matched by the regexp. self.revision = int(m.group(1)) def __str__(self): """ Mimics Subversion 'update' command output. """ if self.modifications: return "%s\nUpdated to revision %d.\n" \ % ( "\n".join( map( lambda (x, y): "%s %s" % (x, y), self.modifications ) ), self.revision ) else: return "At revision %d.\n" % self.revision # XXX: this needs to be moved to the Client class def propget(propname, path, svn_path): p = os.popen("\"%s\" propget %s %s" % (svn_path, propname, path)) str = "" while 1: s = p.readline() if not s: break str = str + s ec = p.close() if not ec: return str.rstrip("\n") return None class Transaction: """ Single Subversion transaction which end with a commit. """ def __init__(self, svn): self.files = {} self.svn = svn def add(self, name): """ Add new file/directory into the transaction. """ self.svn.add(name) self.include(name) def commit(self, logmsg): """ Perform Subversion commit. """ fnames = self.files.keys() self.svn.commit(logmsg, fnames) def include(self, fname): """ Include file FNAME into the transaction. """ self.files[fname] = True def remove(self, name): """ Remove file/directory (nonrecursive) from the version control within the transaction. """ self.svn.remove(name) self.include(name) class Client: """ Subversion interface class. All Subversion-related operations come through this class instance. """ def __init__(self, svn_path): """ SVN_PATH is a path to Subversion command line client. """ self.svn_path = svn_path def add(self, fname): """ Schedule addition of a file. """ args = [ self.svn_path, "add", "-Nq", fname ] # XXX: raise exception on failure here return DITrack.Util.Misc.spawnvp(os.P_WAIT, self.svn_path, args) def commit(self, logmsg, fnames): assert(fnames) assert(logmsg) if os.name == "posix": args = [ self.svn_path, "commit", "-q", "-m", logmsg ] + fnames elif os.name == "nt": # XXX: spaces in file names should be handled by # DITrack.Util.Misc.spawnvp() args = [ self.svn_path, "commit", "-q", "-m", "\"%s\"" % logmsg ] + fnames # XXX: raise exception on failure here return DITrack.Util.Misc.spawnvp(os.P_WAIT, self.svn_path, args) def remove(self, fname): """ Schedule the removal of a file FNAME. """ args = [ self.svn_path, "rm", "-q", fname ] # XXX: raise exception on failure here return DITrack.Util.Misc.spawnvp(os.P_WAIT, self.svn_path, args) def start_txn(self): """ Start a new transaction. """ return Transaction(self) def update(self, wc_path): cmd = "%s update %s" % (self.svn_path, wc_path) p = os.popen(cmd) output = p.readlines() # The close method returns the exit status of the process. See # `pydoc os.popen`. status = p.close() if status: raise Error(cmd, "'svn update' didn't succeed", ["Exit status: %d" % status] ) try: us = UpdateStatus(output) except ValueError: raise Error(cmd, "Can't parse the output", map(lambda x: x.rstrip(), output) ) return us DITrack-0.8/DITrack/ThirdParty/0000755000076500007650000000000011032045407015673 5ustar vssvss00000000000000DITrack-0.8/DITrack/ThirdParty/__init__.py0000644000076500007650000000000011032045265017774 0ustar vssvss00000000000000DITrack-0.8/DITrack/ThirdParty/ezt.py0000644000076500007650000005774711032045265017075 0ustar vssvss00000000000000#!/usr/bin/env python """ezt.py -- easy templating ezt templates are simply text files in whatever format you so desire (such as XML, HTML, etc.) which contain directives sprinkled throughout. With these directives it is possible to generate the dynamic content from the ezt templates. These directives are enclosed in square brackets. If you are a C-programmer, you might be familar with the #ifdef directives of the C preprocessor 'cpp'. ezt provides a similar concept. Additionally EZT has a 'for' directive, which allows it to iterate (repeat) certain subsections of the template according to sequence of data items provided by the application. The final rendering is performed by the method generate() of the Template class. Building template instances can either be done using external EZT files (convention: use the suffix .ezt for such files): >>> template = Template("../templates/log.ezt") or by calling the parse() method of a template instance directly with a EZT template string: >>> template = Template() >>> template.parse(''' ... [title_string] ...

[title_string]

... [for a_sequence]

[a_sequence]

... [end]
... The [person] is [if-any state]in[else]out[end]. ... ... ... ''') The application should build a dictionary 'data' and pass it together with the output fileobject to the templates generate method: >>> data = {'title_string' : "A Dummy Page", ... 'a_sequence' : ['list item 1', 'list item 2', 'another element'], ... 'person': "doctor", ... 'state' : None } >>> import sys >>> template.generate(sys.stdout, data) A Dummy Page

A Dummy Page

list item 1

list item 2

another element


The doctor is out. Template syntax error reporting should be improved. Currently it is very sparse (template line numbers would be nice): >>> Template().parse("[if-any where] foo [else] bar [end unexpected args]") Traceback (innermost last): File "", line 1, in ? File "ezt.py", line 220, in parse self.program = self._parse(text) File "ezt.py", line 275, in _parse raise ArgCountSyntaxError(str(args[1:])) ArgCountSyntaxError: ['unexpected', 'args'] >>> Template().parse("[if unmatched_end]foo[end]") Traceback (innermost last): File "", line 1, in ? File "ezt.py", line 206, in parse self.program = self._parse(text) File "ezt.py", line 266, in _parse raise UnmatchedEndError() UnmatchedEndError Directives ========== Several directives allow the use of dotted qualified names refering to objects or attributes of objects contained in the data dictionary given to the .generate() method. Qualified names --------------- Qualified names have two basic forms: a variable reference, or a string constant. References are a name from the data dictionary with optional dotted attributes (where each intermediary is an object with attributes, of course). Examples: [varname] [ob.attr] ["string"] Simple directives ----------------- [QUAL_NAME] This directive is simply replaced by the value of the qualified name. Numbers are converted to a string, and None becomes an empty string. [QUAL_NAME QUAL_NAME ...] The first value defines a substitution format, specifying constant text and indices of the additional arguments. The arguments are then substituted and the resulting is inserted into the output stream. Example: ["abc %0 def %1 ghi %0" foo bar.baz] Note that the first value can be any type of qualified name -- a string constant or a variable reference. Use %% to substitute a percent sign. Argument indices are 0-based. [include "filename"] or [include QUAL_NAME] This directive is replaced by content of the named include file. Note that a string constant is more efficient -- the target file is compiled inline. In the variable form, the target file is compiled and executed at runtime. Block directives ---------------- [for QUAL_NAME] ... [end] The text within the [for ...] directive and the corresponding [end] is repeated for each element in the sequence referred to by the qualified name in the for directive. Within the for block this identifiers now refers to the actual item indexed by this loop iteration. [if-any QUAL_NAME [QUAL_NAME2 ...]] ... [else] ... [end] Test if any QUAL_NAME value is not None or an empty string or list. The [else] clause is optional. CAUTION: Numeric values are converted to string, so if QUAL_NAME refers to a numeric value 0, the then-clause is substituted! [if-index INDEX_FROM_FOR odd] ... [else] ... [end] [if-index INDEX_FROM_FOR even] ... [else] ... [end] [if-index INDEX_FROM_FOR first] ... [else] ... [end] [if-index INDEX_FROM_FOR last] ... [else] ... [end] [if-index INDEX_FROM_FOR NUMBER] ... [else] ... [end] These five directives work similar to [if-any], but are only useful within a [for ...]-block (see above). The odd/even directives are for example useful to choose different background colors for adjacent rows in a table. Similar the first/last directives might be used to remove certain parts (for example "Diff to previous" doesn't make sense, if there is no previous). [is QUAL_NAME STRING] ... [else] ... [end] [is QUAL_NAME QUAL_NAME] ... [else] ... [end] The [is ...] directive is similar to the other conditional directives above. But it allows to compare two value references or a value reference with some constant string. [define VARIABLE] ... [end] The [define ...] directive allows you to create and modify template variables from within the template itself. Essentially, any data between inside the [define ...] and its matching [end] will be expanded using the other template parsing and output generation rules, and then stored as a string value assigned to the variable VARIABLE. The new (or changed) variable is then available for use with other mechanisms such as [is ...] or [if-any ...], as long as they appear later in the template. """ # # Copyright (C) 2001-2005 Greg Stein. All Rights Reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # # This software is maintained by Greg and is available at: # http://svn.webdav.org/repos/projects/ezt/trunk/ # import string import re from types import StringType, IntType, FloatType import os import cgi try: import cStringIO except ImportError: import StringIO cStringIO = StringIO # # Formatting types # FORMAT_RAW = 'raw' FORMAT_HTML = 'html' FORMAT_XML = 'xml' # # This regular expression matches three alternatives: # expr: DIRECTIVE | BRACKET | COMMENT # DIRECTIVE: '[' ITEM (whitespace ITEM)* '] # ITEM: STRING | NAME # STRING: '"' (not-slash-or-dquote | '\' anychar)* '"' # NAME: (alphanum | '_' | '-' | '.')+ # BRACKET: '[[]' # COMMENT: '[#' not-rbracket* ']' # # When used with the split() method, the return value will be composed of # non-matching text and the two paren groups (DIRECTIVE and BRACKET). Since # the COMMENT matches are not placed into a group, they are considered a # "splitting" value and simply dropped. # _item = r'(?:"(?:[^\\"]|\\.)*"|[-\w.]+)' _re_parse = re.compile(r'\[(%s(?: +%s)*)\]|(\[\[\])|\[#[^\]]*\]' % (_item, _item)) _re_args = re.compile(r'"(?:[^\\"]|\\.)*"|[-\w.]+') # block commands and their argument counts _block_cmd_specs = { 'if-index':2, 'for':1, 'is':2, 'define':1, 'format':1 } _block_cmds = _block_cmd_specs.keys() # two regular expresssions for compressing whitespace. the first is used to # compress any whitespace including a newline into a single newline. the # second regex is used to compress runs of whitespace into a single space. _re_newline = re.compile('[ \t\r\f\v]*\n\\s*') _re_whitespace = re.compile(r'\s\s+') # this regex is used to substitute arguments into a value. we split the value, # replace the relevant pieces, and then put it all back together. splitting # will produce a list of: TEXT ( splitter TEXT )*. splitter will be '%' or # an integer. _re_subst = re.compile('%(%|[0-9]+)') class Template: _printers = { FORMAT_RAW : '_cmd_print', FORMAT_HTML : '_cmd_print_html', FORMAT_XML : '_cmd_print_xml', } def __init__(self, fname=None, compress_whitespace=1, base_format=FORMAT_RAW): self.compress_whitespace = compress_whitespace if fname: self.parse_file(fname, base_format) def parse_file(self, fname, base_format=FORMAT_RAW): "fname -> a string object with pathname of file containg an EZT template." self.parse(_FileReader(fname), base_format) def parse(self, text_or_reader, base_format=FORMAT_RAW): """Parse the template specified by text_or_reader. The argument should be a string containing the template, or it should specify a subclass of ezt.Reader which can read templates. The base format for printing values is given by base_format. """ if not isinstance(text_or_reader, Reader): # assume the argument is a plain text string text_or_reader = _TextReader(text_or_reader) printer = getattr(self, self._printers[base_format]) self.program = self._parse(text_or_reader, base_printer=printer) def generate(self, fp, data): if hasattr(data, '__getitem__') or callable(getattr(data, 'keys', None)): # a dictionary-like object was passed. convert it to an # attribute-based object. class _data_ob: def __init__(self, d): vars(self).update(d) data = _data_ob(data) ctx = _context() ctx.data = data ctx.for_index = { } ctx.defines = { } self._execute(self.program, fp, ctx) def _parse(self, reader, for_names=None, file_args=(), base_printer=None): """text -> string object containing the template. This is a private helper function doing the real work for method parse. It returns the parsed template as a 'program'. This program is a sequence made out of strings or (function, argument) 2-tuples. Note: comment directives [# ...] are automatically dropped by _re_parse. """ # parse the template program into: (TEXT DIRECTIVE BRACKET)* TEXT parts = _re_parse.split(reader.text) program = [ ] stack = [ ] if not for_names: for_names = [ ] if base_printer: printers = [ base_printer ] else: printers = [ self._cmd_print ] for i in range(len(parts)): piece = parts[i] which = i % 3 # discriminate between: TEXT DIRECTIVE BRACKET if which == 0: # TEXT. append if non-empty. if piece: if self.compress_whitespace: piece = _re_whitespace.sub(' ', _re_newline.sub('\n', piece)) program.append(piece) elif which == 2: # BRACKET directive. append '[' if present. if piece: program.append('[') elif piece: # DIRECTIVE is present. args = _re_args.findall(piece) cmd = args[0] if cmd == 'else': if len(args) > 1: raise ArgCountSyntaxError(str(args[1:])) ### check: don't allow for 'for' cmd idx = stack[-1][1] true_section = program[idx:] del program[idx:] stack[-1][3] = true_section elif cmd == 'end': if len(args) > 1: raise ArgCountSyntaxError(str(args[1:])) # note: true-section may be None try: cmd, idx, args, true_section = stack.pop() except IndexError: raise UnmatchedEndError() else_section = program[idx:] if cmd == 'format': printers.pop() else: func = getattr(self, '_cmd_' + re.sub('-', '_', cmd)) program[idx:] = [ (func, (args, true_section, else_section)) ] if cmd == 'for': for_names.pop() elif cmd in _block_cmds: if len(args) > _block_cmd_specs[cmd] + 1: raise ArgCountSyntaxError(str(args[1:])) ### this assumes arg1 is always a ref unless cmd is 'define' if cmd != 'define': args[1] = _prepare_ref(args[1], for_names, file_args) # handle arg2 for the 'is' command if cmd == 'is': args[2] = _prepare_ref(args[2], for_names, file_args) elif cmd == 'for': for_names.append(args[1][0]) # append the refname elif cmd == 'format': if args[1][0]: raise BadFormatConstantError(str(args[1:])) funcname = self._printers.get(args[1][1]) if not funcname: raise UnknownFormatConstantError(str(args[1:])) printers.append(getattr(self, funcname)) # remember the cmd, current pos, args, and a section placeholder stack.append([cmd, len(program), args[1:], None]) elif cmd == 'include': if args[1][0] == '"': include_filename = args[1][1:-1] f_args = [ ] for arg in args[2:]: f_args.append(_prepare_ref(arg, for_names, file_args)) program.extend(self._parse(reader.read_other(include_filename), for_names, f_args, printers[-1])) else: if len(args) != 2: raise ArgCountSyntaxError(str(args)) program.append((self._cmd_include, (_prepare_ref(args[1], for_names, file_args), reader))) elif cmd == 'if-any': f_args = [ ] for arg in args[1:]: f_args.append(_prepare_ref(arg, for_names, file_args)) stack.append(['if-any', len(program), f_args, None]) else: # implied PRINT command if len(args) > 1: f_args = [ ] for arg in args: f_args.append(_prepare_ref(arg, for_names, file_args)) ### this should obey the current format... program.append((self._cmd_subst, (f_args[0], f_args[1:]))) else: program.append((printers[-1], _prepare_ref(args[0], for_names, file_args))) if stack: ### would be nice to say which blocks... raise UnclosedBlocksError() return program def _execute(self, program, fp, ctx): """This private helper function takes a 'program' sequence as created by the method '_parse' and executes it step by step. strings are written to the file object 'fp' and functions are called. """ for step in program: if isinstance(step, StringType): fp.write(step) else: step[0](step[1], fp, ctx) def _cmd_print(self, valref, fp, ctx): _write_value(fp.write, valref, ctx) def _cmd_print_html(self, valref, fp, ctx): _write_value(lambda s, w=fp.write: w(cgi.escape(s)), valref, ctx) def _cmd_print_xml(self, valref, fp, ctx): ### use the same quoting as HTML for now self._cmd_print_html(valref, fp, ctx) def _cmd_subst(self, (valref, args), fp, ctx): fmt = _get_value(valref, ctx) parts = _re_subst.split(fmt) for i in range(len(parts)): piece = parts[i] if i%2 == 1 and piece != '%': idx = int(piece) if idx < len(args): piece = _get_value(args[idx], ctx) else: piece = '' fp.write(piece) def _cmd_include(self, (valref, reader), fp, ctx): fname = _get_value(valref, ctx) ### note: we don't have the set of for_names to pass into this parse. ### I don't think there is anything to do but document it. we also ### don't have a current format (since that is a compile-time concept). self._execute(self._parse(reader.read_other(fname)), fp, ctx) def _cmd_if_any(self, args, fp, ctx): "If any value is a non-empty string or non-empty list, then T else F." (valrefs, t_section, f_section) = args value = 0 for valref in valrefs: if _get_value(valref, ctx): value = 1 break self._do_if(value, t_section, f_section, fp, ctx) def _cmd_if_index(self, args, fp, ctx): ((valref, value), t_section, f_section) = args list, idx = ctx.for_index[valref[0]] if value == 'even': value = idx % 2 == 0 elif value == 'odd': value = idx % 2 == 1 elif value == 'first': value = idx == 0 elif value == 'last': value = idx == len(list)-1 else: value = idx == int(value) self._do_if(value, t_section, f_section, fp, ctx) def _cmd_is(self, args, fp, ctx): ((left_ref, right_ref), t_section, f_section) = args value = _get_value(right_ref, ctx) value = string.lower(_get_value(left_ref, ctx)) == string.lower(value) self._do_if(value, t_section, f_section, fp, ctx) def _do_if(self, value, t_section, f_section, fp, ctx): if t_section is None: t_section = f_section f_section = None if value: section = t_section else: section = f_section if section is not None: self._execute(section, fp, ctx) def _cmd_for(self, args, fp, ctx): ((valref,), unused, section) = args list = _get_value(valref, ctx) if isinstance(list, StringType): raise NeedSequenceError() refname = valref[0] ctx.for_index[refname] = idx = [ list, 0 ] for item in list: self._execute(section, fp, ctx) idx[1] = idx[1] + 1 del ctx.for_index[refname] def _cmd_define(self, args, fp, ctx): ((name,), unused, section) = args valfp = cStringIO.StringIO() if section is not None: self._execute(section, valfp, ctx) ctx.defines[name] = valfp.getvalue() def boolean(value): "Return a value suitable for [if-any bool_var] usage in a template." if value: return 'yes' return None def _prepare_ref(refname, for_names, file_args): """refname -> a string containing a dotted identifier. example:"foo.bar.bang" for_names -> a list of active for sequences. Returns a `value reference', a 3-tuple made out of (refname, start, rest), for fast access later. """ # is the reference a string constant? if refname[0] == '"': return None, refname[1:-1], None parts = string.split(refname, '.') start = parts[0] rest = parts[1:] # if this is an include-argument, then just return the prepared ref if start[:3] == 'arg': try: idx = int(start[3:]) except ValueError: pass else: if idx < len(file_args): orig_refname, start, more_rest = file_args[idx] if more_rest is None: # the include-argument was a string constant return None, start, None # prepend the argument's "rest" for our further processing rest[:0] = more_rest # rewrite the refname to ensure that any potential 'for' processing # has the correct name ### this can make it hard for debugging include files since we lose ### the 'argNNN' names if not rest: return start, start, [ ] refname = start + '.' + string.join(rest, '.') if for_names: # From last to first part, check if this reference is part of a for loop for i in range(len(parts), 0, -1): name = string.join(parts[:i], '.') if name in for_names: return refname, name, parts[i:] return refname, start, rest def _get_value((refname, start, rest), ctx): """(refname, start, rest) -> a prepared `value reference' (see above). ctx -> an execution context instance. Does a name space lookup within the template name space. Active for blocks take precedence over data dictionary members with the same name. """ if rest is None: # it was a string constant return start # get the starting object if ctx.for_index.has_key(start): list, idx = ctx.for_index[start] ob = list[idx] elif ctx.defines.has_key(start): ob = ctx.defines[start] elif hasattr(ctx.data, start): ob = getattr(ctx.data, start) else: raise UnknownReference(refname) # walk the rest of the dotted reference for attr in rest: try: ob = getattr(ob, attr) except AttributeError: raise UnknownReference(refname) # make sure we return a string instead of some various Python types if isinstance(ob, IntType) or isinstance(ob, FloatType): return str(ob) if ob is None: return '' # string or a sequence return ob def _write_value(func, valref, ctx): value = _get_value(valref, ctx) # if the value has a 'read' attribute, then it is a stream: copy it if hasattr(value, 'read'): while 1: chunk = value.read(16384) if not chunk: break func(chunk) else: func(value) class _context: """A container for the execution context""" class Reader: "Abstract class which allows EZT to detect Reader objects." class _FileReader(Reader): """Reads templates from the filesystem.""" def __init__(self, fname): self.text = open(fname, 'rb').read() self._dir = os.path.dirname(fname) def read_other(self, relative): return _FileReader(os.path.join(self._dir, relative)) class _TextReader(Reader): """'Reads' a template from provided text.""" def __init__(self, text): self.text = text def read_other(self, relative): raise BaseUnavailableError() class EZTException(Exception): """Parent class of all EZT exceptions.""" class ArgCountSyntaxError(EZTException): """A bracket directive got the wrong number of arguments.""" class UnknownReference(EZTException): """The template references an object not contained in the data dictionary.""" class NeedSequenceError(EZTException): """The object dereferenced by the template is no sequence (tuple or list).""" class UnclosedBlocksError(EZTException): """This error may be simply a missing [end].""" class UnmatchedEndError(EZTException): """This error may be caused by a misspelled if directive.""" class BaseUnavailableError(EZTException): """Base location is unavailable, which disables includes.""" class BadFormatConstantError(EZTException): """Format specifiers must be string constants.""" class UnknownFormatConstantError(EZTException): """The format specifier is an unknown value.""" # --- standard test environment --- def test_parse(): assert _re_parse.split('[a]') == ['', '[a]', None, ''] assert _re_parse.split('[a] [b]') == \ ['', '[a]', None, ' ', '[b]', None, ''] assert _re_parse.split('[a c] [b]') == \ ['', '[a c]', None, ' ', '[b]', None, ''] assert _re_parse.split('x [a] y [b] z') == \ ['x ', '[a]', None, ' y ', '[b]', None, ' z'] assert _re_parse.split('[a "b" c "d"]') == \ ['', '[a "b" c "d"]', None, ''] assert _re_parse.split(r'["a \"b[foo]" c.d f]') == \ ['', '["a \\"b[foo]" c.d f]', None, ''] def _test(argv): import doctest, ezt verbose = "-v" in argv return doctest.testmod(ezt, verbose=verbose) if __name__ == "__main__": # invoke unit test for this module: import sys sys.exit(_test(sys.argv)[0]) DITrack-0.8/DITrack/ThirdParty/Python/0000755000076500007650000000000011032045407017154 5ustar vssvss00000000000000DITrack-0.8/DITrack/ThirdParty/Python/__init__.py0000644000076500007650000000000011032045265021255 0ustar vssvss00000000000000DITrack-0.8/DITrack/ThirdParty/Python/string.py0000644000076500007650000004604211032045265021044 0ustar vssvss00000000000000#!/usr/bin/env python # # string.py - A collection of string operations # # Copyright (c) 2001, 2002, 2003, 2004 Python Software Foundation; # All Rights Reserved # # $Id: string.py 1696 2007-07-10 22:45:31Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/ThirdParty/Python/string.py $ # # PSF LICENSE AGREEMENT FOR PYTHON 2.4 # ------------------------------------ # # 1. This LICENSE AGREEMENT is between the Python Software Foundation # ("PSF"), and the Individual or Organization ("Licensee") accessing and # otherwise using Python 2.4 software in source or binary form and its # associated documentation. # # 2. Subject to the terms and conditions of this License Agreement, PSF # hereby grants Licensee a nonexclusive, royalty-free, world-wide # license to reproduce, analyze, test, perform and/or display publicly, # prepare derivative works, distribute, and otherwise use Python 2.4 # alone or in any derivative version, provided, however, that PSF's # License Agreement and PSF's notice of copyright, i.e., "Copyright (c) # 2001, 2002, 2003, 2004 Python Software Foundation; All Rights Reserved" # are retained in Python 2.4 alone or in any derivative version prepared # by Licensee. # # 3. In the event Licensee prepares a derivative work that is based on # or incorporates Python 2.4 or any part thereof, and wants to make # the derivative work available to others as provided herein, then # Licensee hereby agrees to include in any such work a brief summary of # the changes made to Python 2.4. # # 4. PSF is making Python 2.4 available to Licensee on an "AS IS" # basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR # IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND # DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS # FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 2.4 WILL NOT # INFRINGE ANY THIRD PARTY RIGHTS. # # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON # 2.4 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS # A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 2.4, # OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. # # 6. This License Agreement will automatically terminate upon a material # breach of its terms and conditions. # # 7. Nothing in this License Agreement shall be deemed to create any # relationship of agency, partnership, or joint venture between PSF and # Licensee. This License Agreement does not grant permission to use PSF # trademarks or trade name in a trademark sense to endorse or promote # products or services of Licensee, or any third party. # # 8. By copying, installing or otherwise using Python 2.4, Licensee # agrees to be bound by the terms and conditions of this License # Agreement. """A collection of string operations (most are no longer used). Warning: most of the code you see here isn't normally used nowadays. Beginning with Python 1.6, many of these functions are implemented as methods on the standard string object. They used to be implemented by a built-in module called strop, but strop is now obsolete itself. Public module variables: whitespace -- a string containing all characters considered whitespace lowercase -- a string containing all characters considered lowercase letters uppercase -- a string containing all characters considered uppercase letters letters -- a string containing all characters considered letters digits -- a string containing all characters considered decimal digits hexdigits -- a string containing all characters considered hexadecimal digits octdigits -- a string containing all characters considered octal digits punctuation -- a string containing all characters considered punctuation printable -- a string containing all characters considered printable """ # Some strings for ctype-style character classification whitespace = ' \t\n\r\v\f' lowercase = 'abcdefghijklmnopqrstuvwxyz' uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' letters = lowercase + uppercase ascii_lowercase = lowercase ascii_uppercase = uppercase ascii_letters = ascii_lowercase + ascii_uppercase digits = '0123456789' hexdigits = digits + 'abcdef' + 'ABCDEF' octdigits = '01234567' punctuation = """!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" printable = digits + letters + punctuation + whitespace # Case conversion helpers # Use str to convert Unicode literal in case of -U l = map(chr, xrange(256)) _idmap = str('').join(l) del l # Functions which aren't available as string methods. # Capitalize the words in a string, e.g. " aBc dEf " -> "Abc Def". def capwords(s, sep=None): """capwords(s, [sep]) -> string Split the argument into words using split, capitalize each word using capitalize, and join the capitalized words using join. Note that this replaces runs of whitespace characters by a single space. """ return (sep or ' ').join([x.capitalize() for x in s.split(sep)]) # Construct a translation string _idmapL = None def maketrans(fromstr, tostr): """maketrans(frm, to) -> string Return a translation table (a string of 256 bytes long) suitable for use in string.translate. The strings frm and to must be of the same length. """ if len(fromstr) != len(tostr): raise ValueError, "maketrans arguments must have same length" global _idmapL if not _idmapL: _idmapL = map(None, _idmap) L = _idmapL[:] fromstr = map(ord, fromstr) for i in range(len(fromstr)): L[fromstr[i]] = tostr[i] return ''.join(L) #################################################################### import re as _re class _multimap: """Helper class for combining multiple mappings. Used by .{safe_,}substitute() to combine the mapping and keyword arguments. """ def __init__(self, primary, secondary): self._primary = primary self._secondary = secondary def __getitem__(self, key): try: return self._primary[key] except KeyError: return self._secondary[key] class _TemplateMetaclass(type): pattern = r""" %(delim)s(?: (?P%(delim)s) | # Escape sequence of two delimiters (?P%(id)s) | # delimiter and a Python identifier {(?P%(id)s)} | # delimiter and a braced identifier (?P) # Other ill-formed delimiter exprs ) """ def __init__(cls, name, bases, dct): super(_TemplateMetaclass, cls).__init__(name, bases, dct) if 'pattern' in dct: pattern = cls.pattern else: pattern = _TemplateMetaclass.pattern % { 'delim' : _re.escape(cls.delimiter), 'id' : cls.idpattern, } cls.pattern = _re.compile(pattern, _re.IGNORECASE | _re.VERBOSE) class Template: """A string class for supporting $-substitutions.""" __metaclass__ = _TemplateMetaclass delimiter = '$' idpattern = r'[_a-z][_a-z0-9:]*' def __init__(self, template): self.template = template # Search for $$, $identifier, ${identifier}, and any bare $'s def _invalid(self, mo): i = mo.start('invalid') lines = self.template[:i].splitlines(True) if not lines: colno = 1 lineno = 1 else: colno = i - len(''.join(lines[:-1])) lineno = len(lines) raise ValueError('Invalid placeholder in string: line %d, col %d' % (lineno, colno)) def substitute(self, *args, **kws): if len(args) > 1: raise TypeError('Too many positional arguments') if not args: mapping = kws elif kws: mapping = _multimap(kws, args[0]) else: mapping = args[0] # Helper function for .sub() def convert(mo): # Check the most common path first. named = mo.group('named') or mo.group('braced') if named is not None: val = mapping[named] # We use this idiom instead of str() because the latter will # fail if val is a Unicode containing non-ASCII characters. return '%s' % (val,) if mo.group('escaped') is not None: return self.delimiter if mo.group('invalid') is not None: self._invalid(mo) raise ValueError('Unrecognized named group in pattern', self.pattern) return self.pattern.sub(convert, self.template) def safe_substitute(self, *args, **kws): if len(args) > 1: raise TypeError('Too many positional arguments') if not args: mapping = kws elif kws: mapping = _multimap(kws, args[0]) else: mapping = args[0] # Helper function for .sub() def convert(mo): named = mo.group('named') if named is not None: try: # We use this idiom instead of str() because the latter # will fail if val is a Unicode containing non-ASCII return '%s' % (mapping[named],) except KeyError: return self.delimiter + named braced = mo.group('braced') if braced is not None: try: return '%s' % (mapping[braced],) except KeyError: return self.delimiter + '{' + braced + '}' if mo.group('escaped') is not None: return self.delimiter if mo.group('invalid') is not None: return self.delimiter raise ValueError('Unrecognized named group in pattern', self.pattern) return self.pattern.sub(convert, self.template) #################################################################### # NOTE: Everything below here is deprecated. Use string methods instead. # This stuff will go away in Python 3.0. # Backward compatible names for exceptions index_error = ValueError atoi_error = ValueError atof_error = ValueError atol_error = ValueError # convert UPPER CASE letters to lower case def lower(s): """lower(s) -> string Return a copy of the string s converted to lowercase. """ return s.lower() # Convert lower case letters to UPPER CASE def upper(s): """upper(s) -> string Return a copy of the string s converted to uppercase. """ return s.upper() # Swap lower case letters and UPPER CASE def swapcase(s): """swapcase(s) -> string Return a copy of the string s with upper case characters converted to lowercase and vice versa. """ return s.swapcase() # Strip leading and trailing tabs and spaces def strip(s, chars=None): """strip(s [,chars]) -> string Return a copy of the string s with leading and trailing whitespace removed. If chars is given and not None, remove characters in chars instead. If chars is unicode, S will be converted to unicode before stripping. """ return s.strip(chars) # Strip leading tabs and spaces def lstrip(s, chars=None): """lstrip(s [,chars]) -> string Return a copy of the string s with leading whitespace removed. If chars is given and not None, remove characters in chars instead. """ return s.lstrip(chars) # Strip trailing tabs and spaces def rstrip(s, chars=None): """rstrip(s [,chars]) -> string Return a copy of the string s with trailing whitespace removed. If chars is given and not None, remove characters in chars instead. """ return s.rstrip(chars) # Split a string into a list of space/tab-separated words def split(s, sep=None, maxsplit=-1): """split(s [,sep [,maxsplit]]) -> list of strings Return a list of the words in the string s, using sep as the delimiter string. If maxsplit is given, splits at no more than maxsplit places (resulting in at most maxsplit+1 words). If sep is not specified or is None, any whitespace string is a separator. (split and splitfields are synonymous) """ return s.split(sep, maxsplit) splitfields = split # Split a string into a list of space/tab-separated words def rsplit(s, sep=None, maxsplit=-1): """rsplit(s [,sep [,maxsplit]]) -> list of strings Return a list of the words in the string s, using sep as the delimiter string, starting at the end of the string and working to the front. If maxsplit is given, at most maxsplit splits are done. If sep is not specified or is None, any whitespace string is a separator. """ return s.rsplit(sep, maxsplit) # Join fields with optional separator def join(words, sep = ' '): """join(list [,sep]) -> string Return a string composed of the words in list, with intervening occurrences of sep. The default separator is a single space. (joinfields and join are synonymous) """ return sep.join(words) joinfields = join # Find substring, raise exception if not found def index(s, *args): """index(s, sub [,start [,end]]) -> int Like find but raises ValueError when the substring is not found. """ return s.index(*args) # Find last substring, raise exception if not found def rindex(s, *args): """rindex(s, sub [,start [,end]]) -> int Like rfind but raises ValueError when the substring is not found. """ return s.rindex(*args) # Count non-overlapping occurrences of substring def count(s, *args): """count(s, sub[, start[,end]]) -> int Return the number of occurrences of substring sub in string s[start:end]. Optional arguments start and end are interpreted as in slice notation. """ return s.count(*args) # Find substring, return -1 if not found def find(s, *args): """find(s, sub [,start [,end]]) -> in Return the lowest index in s where substring sub is found, such that sub is contained within s[start,end]. Optional arguments start and end are interpreted as in slice notation. Return -1 on failure. """ return s.find(*args) # Find last substring, return -1 if not found def rfind(s, *args): """rfind(s, sub [,start [,end]]) -> int Return the highest index in s where substring sub is found, such that sub is contained within s[start,end]. Optional arguments start and end are interpreted as in slice notation. Return -1 on failure. """ return s.rfind(*args) # for a bit of speed _float = float _int = int _long = long # Convert string to float def atof(s): """atof(s) -> float Return the floating point number represented by the string s. """ return _float(s) # Convert string to integer def atoi(s , base=10): """atoi(s [,base]) -> int Return the integer represented by the string s in the given base, which defaults to 10. The string s must consist of one or more digits, possibly preceded by a sign. If base is 0, it is chosen from the leading characters of s, 0 for octal, 0x or 0X for hexadecimal. If base is 16, a preceding 0x or 0X is accepted. """ return _int(s, base) # Convert string to long integer def atol(s, base=10): """atol(s [,base]) -> long Return the long integer represented by the string s in the given base, which defaults to 10. The string s must consist of one or more digits, possibly preceded by a sign. If base is 0, it is chosen from the leading characters of s, 0 for octal, 0x or 0X for hexadecimal. If base is 16, a preceding 0x or 0X is accepted. A trailing L or l is not accepted, unless base is 0. """ return _long(s, base) # Left-justify a string def ljust(s, width, *args): """ljust(s, width[, fillchar]) -> string Return a left-justified version of s, in a field of the specified width, padded with spaces as needed. The string is never truncated. If specified the fillchar is used instead of spaces. """ return s.ljust(width, *args) # Right-justify a string def rjust(s, width, *args): """rjust(s, width[, fillchar]) -> string Return a right-justified version of s, in a field of the specified width, padded with spaces as needed. The string is never truncated. If specified the fillchar is used instead of spaces. """ return s.rjust(width, *args) # Center a string def center(s, width, *args): """center(s, width[, fillchar]) -> string Return a center version of s, in a field of the specified width. padded with spaces as needed. The string is never truncated. If specified the fillchar is used instead of spaces. """ return s.center(width, *args) # Zero-fill a number, e.g., (12, 3) --> '012' and (-3, 3) --> '-03' # Decadent feature: the argument may be a string or a number # (Use of this is deprecated; it should be a string as with ljust c.s.) def zfill(x, width): """zfill(x, width) -> string Pad a numeric string x with zeros on the left, to fill a field of the specified width. The string x is never truncated. """ if not isinstance(x, basestring): x = repr(x) return x.zfill(width) # Expand tabs in a string. # Doesn't take non-printing chars into account, but does understand \n. def expandtabs(s, tabsize=8): """expandtabs(s [,tabsize]) -> string Return a copy of the string s with all tab characters replaced by the appropriate number of spaces, depending on the current column, and the tabsize (default 8). """ return s.expandtabs(tabsize) # Character translation through look-up table. def translate(s, table, deletions=""): """translate(s,table [,deletions]) -> string Return a copy of the string s, where all characters occurring in the optional argument deletions are removed, and the remaining characters have been mapped through the given translation table, which must be a string of length 256. The deletions argument is not allowed for Unicode strings. """ if deletions or table is None: return s.translate(table, deletions) else: # Add s[:0] so that if s is Unicode and table is an 8-bit string, # table is converted to Unicode. This means that table *cannot* # be a dictionary -- for that feature, use u.translate() directly. return s.translate(table + s[:0]) # Capitalize a string, e.g. "aBc dEf" -> "Abc def". def capitalize(s): """capitalize(s) -> string Return a copy of the string s with only its first character capitalized. """ return s.capitalize() # Substring replacement (global) def replace(s, old, new, maxsplit=-1): """replace (str, old, new[, maxsplit]) -> string Return a copy of string str with all occurrences of substring old replaced by new. If the optional argument maxsplit is given, only the first maxsplit occurrences are replaced. """ return s.replace(old, new, maxsplit) # Try importing optional built-in module "strop" -- if it exists, # it redefines some string operations that are 100-1000 times faster. # It also defines values for whitespace, lowercase and uppercase # that match 's definitions. try: from strop import maketrans, lowercase, uppercase, whitespace letters = lowercase + uppercase except ImportError: pass # Use the original versions DITrack-0.8/DITrack/UI.py0000644000076500007650000001057011032045266014476 0ustar vssvss00000000000000# # UI.py - DITrack User Interface # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: UI.py 1842 2007-08-03 00:01:37Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/UI.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import sys class MenuItem: def __init__(self, key, descr): self.key = key self.descr = descr self.enabled = True class Menu: def __init__(self, title, items, eof_returns_value=True): """ Create a menu object with TITLE and passed list of MenuItem's ITEMS. EOF_RETURNS_VALUE controls what to return in case of reading EOF: the first item of ITEMS (if True) or None (otherwise). """ assert(len(items)) self.eof_returns_value = eof_returns_value self.title = title self.default = items[0] self.key2item = {} for i in items: self.key2item[i.key] = i def run(self): assert(len(self.key2item)) keys = self.key2item.keys() keys.sort() while 1: sys.stdout.write("%s:\n" % self.title) for k in keys: item = self.key2item[k] if not item.enabled: continue sys.stdout.write("%s) %s\n" % (k, item.descr)) sys.stdout.write("> ") s = sys.stdin.readline() if not s: if self.eof_returns_value: return self.default else: return None s = s.strip() if s.isdigit(): s = int(s) if self.key2item.has_key(s): if self.key2item[s].enabled: return self.key2item[s] sys.stdout.write("Unrecognized input: '%s'\n\n" % str(s)) class EnumMenu: def __init__(self, title, items, abort_option=None): """ Creates a menu object with TITLE and ITEMS where the 'hotkey' for each item is its sequential number starting with 1. If ABORT_OPTION is True an option to abort the choice is included as the last item. """ items_list = [] for i, v in enumerate(items): items_list.append(MenuItem((i + 1), v)) self.abort_option = None if abort_option: self.abort_option = MenuItem("a", "abort") items_list.append(self.abort_option) self.menu = Menu(title, items_list, eof_returns_value=None) def run(self): """ Runs the menu. In case if the user has aborted the menu (if there was an option) or input EOF, None will be returned. Otherwise the selected item's text is returned. """ r = self.menu.run() if not r: return None if self.abort_option and (r == self.abort_option): return None return r.descr class TextInput: """ Prints out a prompt, reads user input and returns it stripped (if any). """ def __init__(self, title): self.title = title def run(self): sys.stdout.write("%s:\n> " % self.title) str = sys.stdin.readline() if not str: return None return str.strip() DITrack-0.8/DITrack/Util/0000755000076500007650000000000011032045407014516 5ustar vssvss00000000000000DITrack-0.8/DITrack/Util/__init__.py0000644000076500007650000000000011032045266016620 0ustar vssvss00000000000000DITrack-0.8/DITrack/Util/cmdline.py0000644000076500007650000001116211032045266016507 0ustar vssvss00000000000000# # DITrack module: command line parsing # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: cmdline.py 1696 2007-07-10 22:45:31Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Util/cmdline.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import sys class DuplicateOptionError(Exception): "Duplicate option supplied" def __init__(self, option): self.option = option class InvalidValueTypeError(Exception): "Option parameter type is invalid" def __init__(self, option): self.option = option class MissingParameterError(Exception): "Option requires a parameter, but none specified" def __init__(self, option): self.option = option class OptSpecError(Exception): "Error in options specification passed to ParseResults()" def __init__(self, option): self.option = option class UnknownOptionError(Exception): "Unknown option supplied" def __init__(self, option): self.option = option class Option: def __init__(self, name, descr, type, arg, aliases): self.name = name self.descr = descr self.type = type self.arg = arg self.aliases = aliases def print_description(self): s = " " + self.aliases[0] if len(self.aliases) > 1: assert len(self.aliases) == 2 s = s + " [" + self.aliases[1] + "]" if len(self.arg): s = s + " " + self.arg print "%-28s: %s" % (s, self.descr) class ParseResults: def __init__(self, options): "Parse arguments array according to options specification options" self.fixed = [] self.var = { } self.var_alias = { } self.warnings = [] optmap = { } for o in options: for a in o.aliases: optmap[a] = o assign_next = assign_next_type = "" for v in sys.argv[1:]: if len(assign_next): if v[0] == '-': self.warnings.append("'%s' is treated as the argument " + \ "for '%s'" % (v[0], assign_next)) if assign_next_type == "int": try: v = int(v) except ValueError: raise InvalidValueTypeError(v) self.var[optmap[assign_next].name] = v self.var_alias[optmap[assign_next].name] = v_a assign_next = "" continue if optmap.has_key(v): o = optmap[v] if self.var.has_key(o.name): raise DuplicateOptionError(v) if o.type == "boolean": self.var[o.name] = 1 self.var_alias[o.name] = v elif (o.type == "string") or (o.type == "int"): assign_next = v assign_next_type = o.type v_a = v else: raise OptSpecError(optname) else: if v[0] == '-': raise UnknownOptionError(v) else: self.fixed.append(v) if len(assign_next): raise MissingParameterError(assign_next) for o in options: if (o.type == "boolean") and (not self.var.has_key(o.name)): self.var[o.name] = 0 DITrack-0.8/DITrack/Util/common.py0000644000076500007650000000710111032045266016362 0ustar vssvss00000000000000# # common.py - DITrack common utility functions # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: common.py 1846 2007-08-04 11:25:09Z gli $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Util/common.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import datetime import os import sys # DITrack modules import DITrack.DB.Common import DITrack.DB.Exceptions # Environment variable pointing to a database root. DITRACK_ROOT="DITRACK_ROOT" def err(msg, fatal=True): "Print error message and exit with nonzero error status" sys.stderr.write("%s\n" % msg) if fatal: sys.exit(1) def get_db_root(opts=None): "Returns database root, given options" if opts and opts.var.has_key("database"): return opts.var["database"], "specified in command line" elif os.environ.has_key(DITRACK_ROOT): return os.environ[DITRACK_ROOT], \ "specified by " + DITRACK_ROOT + " environment variable" else: return ".", "default path" # XXX: Should be moved to somewhere like DITrack.DB.Util. def open_db(globals, opts, mode="r"): assert (mode == "r") or (mode == "w"), mode dbroot, source = get_db_root(opts) assert(len(dbroot)) # Now try opening this database. globals.get_username(opts); try: db = DITrack.DB.Common.Database(dbroot, globals.username, globals.svn_path, mode) except DITrack.DB.Exceptions.NotDatabaseError, e: err("'" + dbroot + "' (" + source + ") is not an issue database root") except DITrack.DB.Exceptions.NotDirectoryError, e: err("'" + dbroot + "' (" + source + ") doesn't exist or is not a " "directory") except DITrack.DB.Exceptions.InvalidVersionError, e: err("'" + dbroot + "' (" + source + ") is not of a supported database " "format version") except DITrack.DB.Exceptions.InvalidUserError, e: err("Invalid user name: '%s'" % globals.username) except DITrack.DB.Exceptions.DBIsLockedError, e: err("Database '" + dbroot + "' (" + source + ") is locked") except DITrack.DB.Exceptions.CorruptedDB_UnparseableListingFormatError, e: err(e) except: raise return db def svn_commit(globals, opts, ops, comment): if (not opts.var["no_commits"]) and len(ops): txn = DITrack.SVN.Transaction(globals, ops) txn.commit(comment) DITrack-0.8/DITrack/Util/Locking.py0000644000076500007650000000666211032045266016473 0ustar vssvss00000000000000# # Locking.py - functions for locking/unlocking files # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: Locking.py 1696 2007-07-10 22:45:31Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Util/Locking.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import datetime import errno import os import time class FileIsLockedError(Exception): message = "File is locked" if os.name == "posix": import fcntl class LockObject: def __init__(self, mode, fileobj): self.fileobj = fileobj self.mode = mode self.datetime = datetime.datetime.now() self.pid = os.getpid if os.name == "posix": if mode == "w": operation = fcntl.LOCK_EX | fcntl.LOCK_NB else: operation = fcntl.LOCK_SH | fcntl.LOCK_NB try: fcntl.flock(self.fileobj.fileno(), operation) except IOError, (errid, strerror): if ((errid == errno.EACCES) or (errid == errno.EAGAIN)): raise FileIsLockedError else: raise def __del__(self): self.unlock() def unlock(self): if os.name == "posix": fcntl.flock(self.fileobj.fileno(), fcntl.LOCK_UN) def lock(fileobj, mode, timeout=0): """ Lock the file object. The mode parameter should be "r" (shared/read lock) or "w" (exclusive/write lock). FileIsLockedError would be raised if: - there's already a read lock on fileobj and this is an attempt to create write lock - there's already a write lock on fileobj and this is an attempt to create read/write lock One fileobj is permitted to have several shared locks at once, but it is not so for exclusive lock. """ if os.name == "posix": for i in range(timeout+1): try: lockobj = LockObject(mode, fileobj) return lockobj except FileIsLockedError: pass if timeout: time.sleep(1) else: raise FileIsLockedError else: return None def unlock(lockobj): if os.name == "posix": if lockobj: lockobj.unlock() DITrack-0.8/DITrack/Util/Misc.py0000644000076500007650000000457011032045266015774 0ustar vssvss00000000000000# # Misc.py - DITrack miscellaneous functions # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: Misc.py 1888 2007-08-16 15:07:03Z gli $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Util/Misc.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import os def spawnvp(mode, file, args): """ Make the same as os.spawnvp(), available at all platforms.""" if os.name == "posix": return os.spawnvp(mode, file, args) elif os.name == "nt": if os.path.exists(file): return os.spawnv(mode, file, ['"%s"' % args[0]] + args[1:]) else: for dir in os.environ["PATH"].split(";"): if os.path.isfile(os.path.join(dir, file)): break else: # XXX: Possibly use smth like DITrack.Common.ErrCode # if there's more then one error values # 127 is the error code returned by exec*() functions on POSIX # systems when a $PATH lookup has failed. return 127 full_path = os.path.join(dir, file); return os.spawnv(mode, full_path, ['"%s"' % full_path] + args[1:]) DITrack-0.8/DITrack/XML.py0000644000076500007650000000745611032045266014632 0ustar vssvss00000000000000# # XML.py - DITrack XML output support # # Copyright (c) 2007 The DITrack Project, www.ditrack.org. # # $Id: list.py 2182 2007-10-09 21:23:50Z vss $ # $HeadURL: https://127.0.0.1/ditrack/src/trunk/DITrack/Command/list.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # XXX: this module is dt-specific, so it should be moved to an appropriate # place. import sys from xml.sax.saxutils import XMLGenerator from xml.sax.xmlreader import AttributesImpl import DITrack.dt.globals # XML format version XML_FORMAT = "1" class Writer: """ Low level XML output primitives. """ def __init__(self, out=sys.stdout): self._xml = XMLGenerator(out, "utf-8") self._xml.startDocument() def closetag(self, tag, nl=False, indent=0): assert indent >= 0 self.text(" " * indent) self._xml.endElement(tag) if nl: self.text("\n") def finish(self): self._xml.endDocument() def opentag(self, tag, attrs={}, nl=False, indent=0): assert indent >= 0 self.text(" " * indent) self._xml.startElement(tag, AttributesImpl(attrs)) if nl: self.text("\n") def tag_enclose(self, tag, attrs, text, nl=False, indent=0): assert indent >= 0 self.text(" " * indent) self._xml.startElement(tag, attrs) self._xml.characters(text) self._xml.endElement(tag) if nl: self.text("\n") def text(self, s): self._xml.characters(s) class Output: """ DITrack-specific XML output producer. """ def __init__(self, command, input): """ Writes the dt XML output header to stdout. COMMAND is a command name and INPUT is a list of tuples (TAG, VALUE) to be output as input parameters to the command (e.g. enclosed in an '' block). """ self.writer = Writer() self.writer.opentag("dt", { "format": XML_FORMAT, "dt-version": DITrack.dt.globals.VERSION, } ) self.writer.text("\n\n") self.writer.opentag("input", { "command": command }) for k, v in input: self.writer.text("\n ") self.writer.tag_enclose(k, {}, v) self.writer.text("\n") self.writer.closetag("input") self.writer.text("\n\n") self.writer.opentag("output") self.writer.text("\n\n") def finish(self): self.writer.closetag("output") self.writer.text("\n") self.writer.closetag("dt") self.writer.text("\n") self.writer.finish() DITrack-0.8/doc/0000755000076500007650000000000011032045407013065 5ustar vssvss00000000000000DITrack-0.8/doc/html/0000755000076500007650000000000011032045407014031 5ustar vssvss00000000000000DITrack-0.8/doc/html/index.html0000644000076500007650000006464311032045301016034 0ustar vssvss00000000000000DITrack Manual

DITrack Manual

Version 0.4

The DITrack Project

This work is licensed under the BSD license terms. The full text of the license is available here.

Oct, 2 2006


Chapter 1. Overview

DITrack is an issue tracking system. Its primary purpose is to store and organize text records that reflect real-world issues an organization has to deal with. The system is primarily targeted to small software projects with flat organizational structure (where no complex access control policies have to be enforced).

Audience

This document is a general system architecture overview and a user manual at the same time. It is assumed that the reader is familiar with Subversion version control system and has a basic knowledge of UNIX environment.

System Design

DITrack uses a Subversion repository to store its data. The repository is used merely as a distributed versioned file system: DITrack makes no assumptions about its layout. The diagram below displays the system structure.

	Server side				Client side

	+-------------------+		     +-------------------+
	| Subversion server |----------------| Subversion Client |
	+-------------------+		     +-------------------+
		 /				|	    \
		/				|	+--------------+
(DITrack Pre-Commit Hook)			|	| Working copy |
	     /					|	+--------------+
+------------+					|	    /
| Repository |				    +----------------+
+------------+				    | DITrack Client |
					    +----------------+
					  	     |
						     |
					    +----------------+
					    |     End User   |
					    |   Or Software  |
					    |    Component   |
					    +----------------+

The diagram shows only single client instance; however there may be a number of them. Each client has a working copy which contains a snapshot of DITrack issues database. Since the latter is just a subtree of a Subversion repository, single repository may contain unlimited number of DITrack databases. Again, since the issues database is just a Subversion working copy, usual rules of dealing with that apply: it should be periodically updated (i.e. synchronized with the repository by 'svn update').

All data files DITrack makes use of within the working copy are plain text (mostly conforming to RFC2822 message format). In a case when the DITrack client is not available, a user may just hand-edit the files with any ASCII text editor and commit the changes.

However, since the issue database is a set of related entities, the consistency should always be preserved (at least at the synchronization points, i.e. when an 'svn update' or 'svn commit' happens). To enforce data consistency, a pre-commit hook script is installed on the server side. It basically ensures that the transaction which is about to be committed doesn't break the database consistency. Thus, even is a user edits data files manually, the database won't get corrupted.

NB! As of version 0.4, there is no server-side hook. It will be implemented in a future release.

It is worth noting that instead of a human user the client might be a software component that acts on behalf of the user. This way, for example, a web interface or e-mail integration facility for the issues database may be built.

Also note that DITrack on the client side is only a driver which controls Subversion client. The former does not initiate or handle any network activities. Its role is limited to modifications of local working copy and running appropriate Subversion commands to synchronize with the repository.

The Ontology

Issues

The data model used by DITrack is designed to be as simple and general as possible. The basic entity class is the issue - a collection of (primarily) textual records describing real-world issue and the progress being done to resolve that. Each issue is assigned a unique numeric identifier. The very first issue in a database is assigned identifier 1 and each following one is given the next integral number.

Each issue consists of a header and a description; it can also have comments added and files attached.

An issue header contains a number of fields that are used by DITrack and can also have arbitrary user-specified fields (provided they conform to certain syntax rules). DITrack makes use of the following fields:

Category
A category the issue falls into.
Due-in
Current estimate of the issue resolution deadline.
Opened-by
An identifier of the user who originally opened the issue.
Opened-on
A timestamp when the issue was initially opened.
Owned-by
An identifier of a user who is currently responsible for resolving the issue.
Reported-for
A version of a product the issue was reported against.
Status
Current status of the issue: "open" or "closed".
Title
A short description of the issue.

Users

The system has a notion of users. Each user has an identifier which is basically the user's login name. There are no roles or access rights attributed to any user in the system.

Categories

Issues handled by the system fall into different categories. Hence the notion of the latter. Each category has an identifier which is a sequence of non-blank characters. The name space for the categories is flat (i.e. the names are not structured in any sort of hierarchy that DITrack is aware of); however it is possible to imitate tree structure by crafting category names according to certain rules. For example, the following category names are treated as flat by DITrack but are perceived as hierarchially organized: "unknown", "frontend", "frontent/user-editor", "backend", "backend/server", "backend/tools", "backend/tools/cleaner", etc. "-" cannot be used as a category name.

Each category is associated with a version set. Different categories may share common version sets.

Versions

Versions represent different time points in a lifetime of a product. Obviously they are not strictly tied to real version numbers; they may represent arbitrary milestones in a development cycle. Version names are sequences of non-blank characters; "/" cannot be used as a version name.

Versions are arranged into sets. Since a single project may contain several products released on different schedules, different version sets may be used to track each product development. Version names within a set are divided into three groups ("tenses"): "past" versions, "current" versions and "future" versions.

The notion of "tenses" is introduced to aid a user in dealing with potentially large version sets. A product may have a huge number of versions released per its lifetime; however, when filing a bug report or planning the features for a couple of nearest releases of a product, a user needs only a handful of versions to consider. Thus, "future" versions are used for planning: the target milestone for the issue (the "Due-in" header field) may contain only a future version name. The "current" versions are used when reporting an issue: they indicate all versions of product that are currently in use. And finally the "past" versions represent versions of product that are no longer supported: their names cannot appear in an open issue.

Chapter 2. Installation

As of now, DITrack doesn't have an installation routine. DITrack itself is a couple of Python and Shell scripts and modules. Thus, it can be used without regular system-wide installation.

Alternatively, DITrack may be installed system-wide manually as follows:

  1. Copy the 'dt' and 'dt-createdb' files to the location where your locally installed binaries reside. Most probably, it is '/usr/local/bin', '/opt/bin' or alike.

  2. Copy the 'DITrack' directory to the location where your locally installed Python modules reside. Most probably, it is '/usr/local/lib/python' or alike.

Chapter 3. Usage

The DITrack command line client and the script to create new issue databases are named 'dt' and 'dt-createdb' respectively. The former is a Python script that uses library modules distributed with the system in 'DITrack' subdirectory. So make sure that the modules are available in system paths for Python modules (PYTHONPATH environment variable, see Python manual for details) or in current directory. This means that if you have not installed DITrack system-wide, you'll need to change current directory to the one where DITrack resides each time you run 'dt'.

All following examples assume that DITrack is installed system-wide. '$' represents shell prompt here.

Getting Help

You can always get a brief help message with a list of available commands by issuing:

$ dt help

To get help for specific command, append its name after 'help', as in

$ dt help act

Specifying Database To Use

The 'dt' script assumes that the current directory is the root of the issues database you want to work with. There are two ways to change this assumption.

DITrack checks the DITRACK_ROOT environment variable, and if it's set, its value is used to reference an issue database. In a Bourne shell, it may be set with the the command like the following:

$ export DITRACK_ROOT="/home/joe/myproj/issues"

Alternatively, the '-d' option may be used to specify the location of a database. It has the highest precedence so may be used to override DITRACK_ROOT environment variable:

$ dt ls -d ~/some/other/issue/database

Other Environment Settings

Since working with issues includes a fair bit of text editing, DITrack needs to know which application to use for that. Environment variable EDITOR should be set upon invocation of any 'dt' command that might involve editing.

Bootstrapping

Database Initialization

DITrack needs an issue database to work on. It has to be checked out and reside in a working copy and be available for both reading and writing. The database has certain structure, so it needs to be created with special utility; 'dt-createdb' is the one to use for that purpose.

An issue database is usually created on per-project basis. Depending on your preferences the Subversion repository you own probably hosts one or multiple projects. This detail is irrelevant here, since whatever repository structure is, we'll consider only a single project of that to use in the following examples.

Suppose, the project structure in the repository is as follows:

projects/
	myproj/
		trunk/
		branches/
			1.0/
			2.0/
		tags/
			1.0/
			1.1/
			

The natural placement for an issue database with such a layout would be under 'myproj' directory.

The following command will nonrecursively check out specified repository path into 'myproj-root' directory and initialize issue database named 'issues' in there.

$ dt-creatdb svn://server/projects/myproj issues myproj-root

The command will initialize database structure without committing any changes to Subversion repository. This action item is left for you do. You might want to tweak the database configuration as described below before committing that to your repository.

Database Configuration

The first things you might wish to configure for newly created issue database are: user list, version sets and categories.

User List

Users' identifiers are stored in the 'etc/users' file under a database root. The syntax is simple: it's just a list of user identifiers, one per line. Example:

joe
sally
rob
				

You may edit the list with any text editor and commit your changes manually.

Version Sets

Version sets are stored in the 'etc/versions' file under a database root. This configuration file defines one version set per line. Each line is arranged as follows:

set-name: [pv [pv [ ...]]] / [cv [cv [ ...]]] / [fv [fv [ ...]]]
				

where

set-name
is a name of the version set;
pv
is a name of a past version;
cv
is a name of a current version;
fv
is a name of a future version.

Example:

infrastructure-milestones: initial 200605 200606 / - / 200607 200608 sometimes
editor-versions: 1.0 2.0 2.1 / 2.2 3.0 3.1 / 2.3 3.2 4.0 5.0
backend-versions: 1.0.0 1.0.1 1.1 / 1.1.1 1.1.2 / 1.1.3 1.2.0 2.0

Categories

Category definitions are stored in the 'etc/categories' file under a database root. The configuration file defines one category per line. Each line is arranged as follows:

category-name: versions=version-set default-owner=user
				

where

category-name
is a name of the category being defined;
version-set
is a name of the version set associated with this category;
user
is an identifier of a user who is the default owner of issues for this category.

Example:

infrastructure: versions=infrastructure-milestones default-owner=joe
editor: versions=editor-versions default-owner=sally
backend: versions=backend-versions default-owner=rob
				

Regular Work Cycle

There are three basic actions that can be performed against an issue database: adding issues, acting on existing issues and querying. Thus DITrack command line client ('dt') features three basic commands for that: 'new', 'act' and 'ls'.

Adding A New Issue

To add a new issue, use the 'new' command. You will be prompted to choose a category that the new issue falls into, the version of a product this issue is filed against, the issue title and then shelled out into an editor to enter the issue description. Once you are done with that, you'll be finally asked for a due version of this issue and upon that the newly added issue will be committed into the database.

Acting On An Existing Issue

To take an action on an existing issue, use the 'act' command. A menu of possible actions will appear, which include closing/reopening the issue, changing due version, adding a comment, reassigning owner and edition of the issue header.

It is also possible to act on several issues at once. Just specify a list of issues as arguments for the 'act' command:

$ dt act 10 14 28

Actions you take will apply to all listed issues. Available menu options depend on the particular issues properties. For example, "change due version" menu option is only available if all issues listed share the same version set.

Querying The Database

Listing Existing Issues

The 'ls' command provides a way to query an issues database. If run without additional arguments, it dumps a list of all existing issues.

Additional arguments may be supplied that will be recognized as filter expressions or predefined filter names. If an argument doesn't look like an expression (doesn't contain '=', as of now), it's assumed to be a predefined filter name.

Filter expression is a list of comma-separated conditionals:

cond1,cond2,...condN

...where each condition

condX

is a field name and value, separated by an operator:

field{operator}value

Operators supported are '=' ("equals") and '!=' ("doesn't equal").

A filter expression matches if all of its conditionals match.

For example, the following command will list all issues which status is "open" and the owner is "joe":

$ dt ls Status=open,Owned-by=joe

Note that field names and values are case-sensitive!

If several filter expressions are specified, the output will include issues that match any of the expressions. The following command will list issues owned by 'joe' or 'rob':

$ dt ls Owned-by=joe Owned-by=rob

Predefined filters associate filter expressions with names to save typing for frequently used queries. The configuration file for predefined filters is called 'filters' and resides under 'etc' directory of an issue database. The file lists predefined filter names, one per line, followed by a colon and a list of conditions as in arguments for 'ls' command:

1.0: Due-in=1.0,Status=open
1.1: Due-in=1.1,Status=open
closed: Status=closed
				

...thus, with the configuration above the invocation of:

$ dt ls 1.1

...is equivalent to:

$ dt ls Due-in=1.1,Status=open

In addition to this, the right side of conditional expression may refer to environment variable set at the time of 'dt ls' invocation, like:

my: Owned-by=$USER

When invoked:

dt ls my

... 'my' will be treated as 'Owned-by=$USER' where '$USER' is replaced with 'USER' environment variable value.

Viewing A Full Record Of An Issue

The 'cat' command dumps a full text of an issue record to standard output.

DITrack-0.8/doc/quicktour/0000755000076500007650000000000011032045407015113 5ustar vssvss00000000000000DITrack-0.8/doc/quicktour/index.html0000644000076500007650000005047511032045301017114 0ustar vssvss00000000000000 DITrack: a quick tour

DITrack: a quick tour

$Id: index.html 2197 2007-10-17 23:30:35Z vss $ $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/doc/quicktour/index.html $

This is a quick tour of DITrack capabilities. It's not indended to serve as documentation, but rather a short description of a workflow and some design details to get you a feel of what DITrack is.

This document covers DITrack 0.6.

Issue database initialization

So, you've got a brand new project and need to start tracking bugs in that as soon as possible. And yes, you've got Subversion and DITrack installed. First off, you have to decide where the issue database will be located in your repository. Since issues are going to be applicable to different versions and branches of the code, a logical place for the issue database is something like '/issues'.

From now on we'll assume that environment variable 'R' points to your project's repository root:

	~ $ export R=http://your.svn.server.com/repos/myproject
To initialize the issue database you have to run 'dt-createdb.py' script, giving it the repository root, your chosen name of the issue database directory and the name of the local directory you want to work in ("work" for now, while initializing the issue database: you'll see what it means in a moment).
	~ $ dt-createdb $R issues myproject
	property 'ditrack:format' set on 'myproject/issues'
	property 'svn:ignore' set on 'myproject/issues'

	Empty issue database created at:
	'myproject/issues'

	Now you should probably type something like:

	svn commit myproject/issues

	~ $

As you can see, the script has (nonrecursively) checked out the root of your repository (assuming $R is pointing to the root, which is not really required) into 'myproject', created directory 'issues', populated it with some data and scheduled all for addition to SVN:

	~ $ ls myproject/
	issues
	~ $ cd myproject/
        myproject $ svn st
	A      issues
	A      issues/meta
	A      issues/meta/next-id
	A      issues/data
	A      issues/etc
	A      issues/etc/listing-format
	A      issues/etc/users
	A      issues/etc/categories
	A      issues/etc/filters
	A      issues/etc/versions
	A      issues/README.txt
        myproject $

Ok, now it's time to do a little customization. All the settings that might be of interest to you reside in issues/etc. Start with the 'users' file, which contains a list of users of the system. It would be helpful if DITrack user names actually match SVN user names. The format of the file is just one user name per line:

	myproject $ (echo "joe"; echo "sally") > issues/etc/users
	myproject $ cat issues/etc/users
	joe
	sally
        myproject $
Now let's define some categories. Each issue filed in the DITrack system is assigned to one of categories. Each category has a default owner and a set of "versions" (more on that later). For example, your project could have two components — frontend and backend — released on different schedules and each having its own version number. Thus you'd add at least two categories to the system. File 'categories' in issues/etc defines a set of categories; the entries are separated by blank lines and each entry is a set of lines following "Key: value" syntax. Below is the sample of how you might set up the categories.
	myproject $ vi issues/etc/categories
	... editing ...
	myproject $ cat issues/etc/categories
	Category: frontend
	Default-owner: joe
	Version-set: frontend-versions

	Category: frontend/docs
	Default-owner: joe
	Version-set: frontend-versions

	Category: backend
	Default-owner: sally
	Version-set: backend-versions
        myproject $

Note that for frontend we have two categories (one for code, one for docs) that share the same version set. What is version set? It's just a set of versions that are shared among different categories. In this case frontend and backend are released on different schedules, so the version sets are different. On the other hand the front end code and its documentation are released at the same time, so they share one.

As a product develops, the set of version numbers associated with that grows. First, there are future versions of the product, yet to be created; these are called "future versions" in DITrack. Then, there is [usually] a few version numbers that represent the currenly used releases of the project — against which bugs are reported; we call these "current versions". At last, there are version numbers that are no longer in use; we call them "past versions". So, the version set is just an enumeration of past, current and future versions of the product. DITrack needs to know which versions (for each category) can bugs be reported against and which target versions can be assigned. This all is specified in the 'versions' file in issues/etc. Each line of the file follows the syntax: "version-set-name: past versions / current versions / future versions". Let's say that you've already released versions 0.1 and 0.2 of your frontend and you want to plan for upcoming releases 0.3, 0.4 and 0.5 (there are no past versions since the project is still young). As for backend, versions 0.1, 0.2 and 0.2p1 are no longer in use, you run 0.3 and plan for 0.4 and 0.5. This is how that would be represented in the config:

	myproject $ vi issues/etc/versions
	... editing ...
	myproject $ cat issues/etc/versions
	frontend-versions: / 0.1 0.2 / 0.3 0.4 0.5
	backend-versions: 0.1 0.2 0.2p1 / 0.3 / 0.4 0.5
        myproject $
Ok, this is the absolute minimum that needs to be edited, you can tweak the rest later. So let's go ahead and finish our database initialization by committing the changes to the repository:
	myproject $ svn ci issues -m "Initialized issues database"
	Adding         issues
	Adding         issues/README.txt
	Adding         issues/data
	Adding         issues/etc
	Adding         issues/etc/categories
	Adding         issues/etc/filters
	Adding         issues/etc/listing-format
	Adding         issues/etc/users
	Adding         issues/etc/versions
	Adding         issues/meta
	Adding         issues/meta/next-id
	Transmitting file data .......
	Committed revision 1.
	myproject $

Ok, now we are ready to file some issues.

Working with issue database

Let's first check out the issue database into some convenient location.

	myproject $ cd ~
	~ $ rm -rf myproject
	~ $ svn co $R/issues my-issues
	A    my-issues/meta
	A    my-issues/meta/next-id
	A    my-issues/data
	A    my-issues/README.txt
	A    my-issues/etc
	A    my-issues/etc/listing-format
	A    my-issues/etc/users
	A    my-issues/etc/categories
	A    my-issues/etc/filters
	A    my-issues/etc/versions
	 U   my-issues
	Checked out revision 1.
	~ $

Now to save some extra typing, we are going to point DITrack to this directory by setting up DITRACK_ROOT environment variable (this can always be overridden with '-d' switch to 'dt'):

	~ $ export DITRACK_ROOT=`pwd`/my-issues

Adding issues

Ok, we are ready to add a new issue. This is accomplished with 'new' command of 'dt' (if your login name is different from the one you use in DITrack, you might need to use '-u' option). DITrack will ask a few questions and drop you to an editor so that you can enter the issue description:

	~ $ dt new
	Choose the issue category:
	1) backend
	2) frontend
	3) frontend/docs
	a) abort
	> 1
	Choose the version the issue is reported against:
	1) 0.3
	a) abort
	> 1
	Enter the issue title:
	> possible memory leak in dispatcher

	... editor starts here ...

	Choose the version the issue is due:
	1) 0.4
	2) 0.5
	a) abort
	> 1
	New local issue #A committed as i#1
	~ $
The last line of the output tells us that the issue you've just typed has been stored under number 1. Let's add another one for the frontend:
	~ $ dt new
	Choose the issue category:
	1) backend
	2) frontend
	3) frontend/docs
	a) abort
	> 2
	Choose the version the issue is reported against:
	1) 0.1
	2) 0.2
	a) abort
	> 2
	Enter the issue title:
	> funny characters in the main window

	... editor starts here ...

	Choose the version the issue is due:
	1) 0.3
	2) 0.4
	3) 0.5
	a) abort
	> 1
	New local issue #A committed as i#2
	~ $

Viewing issues

Ok, now we have two issues in the database. We can always get a list of issues (possibly, filtered by some criteria) with the 'ls' command:

	~ $ dt ls
	1    sally    0.4      open   possible memory leak in dispatcher
	2    joe      0.3      open   funny characters in the main window
	~ $ 

The output gives us an issue number, owner, due version, status and a title. There is a way to customize the output (see '-f' option and etc/listing-formats file under the database root). We won't get into these details for now.

We can also filter the output by specifying the filter expression as an argument to 'ls'. Here we filter by the issue owner:

	$ ~ dt ls Owned-by=joe
	2    joe      0.3      open   funny characters in the main window
	$ ~

Where did we get the 'Owned-by' part from? It's a standard DITrack header which is present in each issue. To learn which headers are there we can use the 'cat' command which basically 'concatenates' an issue to the standard output. Here is the simple form:

	$ ~ dt cat 2
	Issue: 2
	Category: frontend
	Due-in: 0.3
	Opened-by: joe
	Opened-on: 1192578520 2007-10-16 16:48:40 Tue -0700
	Owned-by: joe
	Reported-for: 0.2
	Status: open
	Title: funny characters in the main window

	There is a few funny-looking characters in the main window when viewed in IE.
	Firefox seems to be ok.

	==============================================================================
	$ ~

You can see there is an issue header, followed by a blank line and then the issue description that we originally entered. The header includes a number of fields which are maintained by DITrack (but you can freely edit them, actually; or add new ones, specific to your needs).

Currently the issue has only original description (which is called 'comment 0' in DITrack speak), since it has not been yet commented. We can always display specific comment by adding '.' followed by the comment number to the issue number in 'cat' argument. So, if we'd need to display comment 10 of issue 123, we'd say 'dt cat 123.10'.

Any comment has a header and a text. If we'd like to display header only, we have to specify '--headers-only' option to 'cat'. Since our goal was currently to learn about various headers, it is best achieved with:

	~ $ dt cat --headers-only 2.0
	Issue: 2
	Category: frontend
	Due-in: 0.3
	Opened-by: joe
	Opened-on: 1192578520 2007-10-16 16:48:40 Tue -0700
	Owned-by: joe
	Reported-for: 0.2
	Status: open
	Title: funny characters in the main window
	~ $

Working with issues

Okay. Now, let's say we discovered that issue 2 (the conventional notation in DITrack for this is 'i#2') is too hard to get delivered for 0.3 and thus we want to move it to version 0.4. To do something about an issue we have to use the 'act' command, which offers us a menu to close/reopen the issue, change due version, add a comment text (you might skip adding any text and just change header fields, so this is optional), manage files attached to the issue, manually edit the header or reassign issue to another user. We want to change the due version, so here we go:

	~ $ dt act 2

	Acting on:

	[frontend-versions]:
	i#2: funny characters in the main window


	Choose an action for the issue(s):
	a) abort, discarding changes
	c) close the issue
	d) change due version
	e) edit comment text
	f) manage file attaches
	h) edit the issue header
	o) reassign the issue owner
	q) quit, saving changes
	> d
	Current due version: 0.3
	Choose new due version:
	1) 0.3
	2) 0.4
	3) 0.5
	> 2

	Acting on:

	[frontend-versions]:
	i#2: funny characters in the main window


	Choose an action for the issue(s):
	a) abort, discarding changes
	c) close the issue
	d) change due version
	e) edit comment text
	f) manage file attaches
	h) edit the issue header
	o) reassign the issue owner
	q) quit, saving changes
	>

	...

As you can see, when entering the action, DITrack prints out the list of issues we are working with (here it's just one, but we can actually specify multiple issue numbers as arguments to 'dt act') and the version sets the issues belong to (here i#2 belongs to 'frontend-versions'). Then we have a menu which we exploit to change the due version. After doing that we end up in the same menu so we can do more stuff with the issue. We are going to post an excuse for delaying the fix by entering a comment text:

	...

	Acting on:

	[frontend-versions]:
	i#2: funny characters in the main window


	Choose an action for the issue(s):
	a) abort, discarding changes
	c) close the issue
	d) change due version
	e) edit comment text
	f) manage file attaches
	h) edit the issue header
	o) reassign the issue owner
	q) quit, saving changes
	> e

	... editor starts here ...

	Acting on:

	[frontend-versions]:
	i#2: funny characters in the main window


	Choose an action for the issue(s):
	a) abort, discarding changes
	c) close the issue
	d) change due version
	e) edit comment text
	f) manage file attaches
	h) edit the issue header
	o) reassign the issue owner
	q) quit, saving changes
	> q
	Comment A added to issue 2
	Local i#2.A committed as i#2.1
	~ $

After hitting 'q' the newly added comment gets committed to the database. We can ensure it's there by using the familiar 'cat' command:

	~ $ dt cat 2
	Issue: 2
	Category: frontend
	Due-in: 0.4
	Opened-by: joe
	Opened-on: 1192578520 2007-10-16 16:48:40 Tue -0700
	Owned-by: joe
	Reported-for: 0.2
	Status: open
	Title: funny characters in the main window

	There is a few funny-looking characters in the main window when viewed in
	IE.
	Firefox seems to be ok.

	==============================================================================
	Comment: 1
	Added-by: joe
	Added-on: 1192657999 2007-10-17 14:53:19 Wed -0700
	DT-New-Due-in: 0.4
	DT-Old-Due-in: 0.3

	Too hard to get it done for 0.3 -- needs much more testing.

	==============================================================================
	~ $

You can see now that the issue header (first block of lines in the output) has new due version and there is a comment added. The comment also consists of a header and a text and the former shows us that the 'Due-in' field was changed from '0.3' to '0.4'.

Syncing the database

DITrack is a distributed system which uses Subversion as its underlying versioned distributed storage layer. Thus the working copy your issue database is checked out into is a snapshot of the repository at a given time. As with any version control system you need to periodically bring your copy of the data with the master. DITrack offers 'update' command for this (abbreviated as 'up'):

	~ $ dt up
	A    /u/joe/my-issues/data/i1/comment1
	Updated to revision 8.
	~ $

What happened is DITrack invoked Subversion to update the working copy and you can see that there is something happened with i#1.

Under the hood

Here we are going to make a quick look under DITrack's hood to show you the way your valuable data ends up being stored. First, this is how the top directory of our issue database looks like:

	~ $ ls -la my-issues
	total 16
	drwxr-xr-x  7 joe  users  512 Oct 16 16:43 .
	drwxr-xr-x  4 joe  users  512 Oct 15 16:50 ..
	drwxr-xr-x  2 joe  users  512 Oct 15 16:56 .ditrack
	drwxr-xr-x  6 joe  users  512 Oct 17 15:04 .svn
	-rw-r--r--  1 joe  users  105 Oct 15 16:50 README.txt
	drwxr-xr-x  5 joe  users  512 Oct 16 16:48 data
	drwxr-xr-x  3 joe  users  512 Oct 15 16:56 etc
	drwxr-xr-x  3 joe  users  512 Oct 16 16:42 meta
	~ $

Skipping '.svn' (since it's a Subversion working copy) and 'README.txt' (which is a short description of what the heck this directory is), there is:

.ditrack
This hidden directory serves for DITrack internal purposes and is present only on your local machine. It is used to store some metadata about this snapshot of the database (such as "when was the last time we synced up?") and also an "LMA", which stands for "Local Modifications Area". The LMA is a kind of a buffer which holds all your local changes right up to the moment when we are about to commit. The philosophy behind DITrack is that the database working copy should be exact copy of the repository at all times. Thus we store all the changes in LMA and only when a commit is about to happen, changes are written to the working copy. This also helps us out when there is no network connectivity: LMA is the place where everything goes until we want to (and can) push our changes to the server.
data
This is where all issue data resides. We'll look inside shortly.
etc
All database-wide configuration files are here (we've seen a few while setting up the database).
meta
DITrack metadata which is shared with other users (as opposed to '.ditrack' which is exclusively yours).

All you really care about is under 'data', specifically:

	~ $ ls -l my-issues/data
	total 4
	drwxr-xr-x  3 vss  users  512 Oct 17 15:04 i1
	drwxr-xr-x  3 vss  users  512 Oct 17 14:53 i2
	~ $

...e.g. a directory for each existing issue with plain text (RFC2822) files representing "comments" (i.e. changes to issues):

	~ $ cat my-issues/data/i1/comment0
	Added-by: joe
	Added-on: 1192578325 2007-10-16 16:45:25 Tue -0700
	DT-New-Category: backend
	DT-New-Due-in: 0.4
	DT-New-Opened-by: joe
	DT-New-Opened-on: 1192578325 2007-10-16 16:45:25 Tue -0700
	DT-New-Owned-by: sally
	DT-New-Reported-for: 0.3
	DT-New-Status: open
	DT-New-Title: possible memory leak in dispatcher
	DT-Old-Category: 
	DT-Old-Due-in: 
	DT-Old-Opened-by: 
	DT-Old-Opened-on: 
	DT-Old-Owned-by: 
	DT-Old-Reported-for: 
	DT-Old-Status: 
	DT-Old-Title: 

	The dispatcher seems to leak memory. Restart helps.

	This seems to have started happening after the ABC changes.
	~ $ cat my-issues/data/i1/comment1
	Added-by: sally
	Added-on: 1192658476 2007-10-17 15:01:16 Wed -0700
	DT-New-Resolution: fixed
	DT-New-Status: closed
	DT-Old-Resolution: 
	DT-Old-Status: open

	Fixed in r12345.
	~ $ 

Besides the exciting fact that it's all plain text (hello, /bin/cat and /usr/bin/vi in emergency!) there is one important point to note. DITrack never modifies existing data, only adds new. Hence conflict resolution is [going to be] easy. However, if, say, there is a mistake in one of the older comments, you can just go ahead and fix it in a text editor and commit the change without using DITrack at all. This is our equivalent of hand-editing a row in an SQL database. Or, if you feel like, you can roll back the whole issue database to an older revision (now, there is no easy way to do this with traditional issue tracking systems!). And one final neat thing is that you can also use 'svn log' to walk through an issue history:

	~ $ svn log my-issues/data/i1
	------------------------------------------------------------------------
	r8 | joe | 2007-10-17 15:01:16 -0700 (Wed, 17 Oct 2007) | 6 lines

	i#1: possible memory leak in dispatcher
	 * closed as fixed

	Fixed in r12345.


	------------------------------------------------------------------------
	r5 | joe | 2007-10-16 16:45:25 -0700 (Tue, 16 Oct 2007) | 1 line

	i#1 added: possible memory leak in dispatcher
	------------------------------------------------------------------------
	~ $

Isn't that nice, huh?

Not covered

This is the end of our introduction to DITrack. There is quite a few things that are left uncovered (so feel free to ask on the mailing list about those or use 'dt help' to work your way through), among which are:

  • file attaches;
  • advanced 'ls' features (complex and predefined filters, custom output formats);
  • working offline (commands 'commit', 'status', 'remove');
  • a read-only web interface.

Hope by now you've got enough interest in DITrack to try it out! Enjoy! DITrack-0.8/dt0000755000076500007650000001723411032045302012656 0ustar vssvss00000000000000#!/usr/bin/env python # # dt - DITrack command-line client # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: dt 2352 2007-11-13 18:47:28Z oleg $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/dt $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import logging import os.path import sys # DITrack modules import DITrack.dt.globals import DITrack.Command.act import DITrack.Command.cat import DITrack.Command.commit import DITrack.Command.help import DITrack.Command.list import DITrack.Command.new import DITrack.Command.remove import DITrack.Command.status import DITrack.Command.update import DITrack.Logging import DITrack.Util.cmdline ################################# GLOBALS ############################ globals = DITrack.dt.globals.Globals() ################################# CLASSES ############################ class CommandTable: def __init__(self, table): self.table = table keys = table.keys() cmds = {} for k in keys: item = table[k] if not cmds.has_key(item.canonical_name): cmds[item.canonical_name] = [ item.canonical_name ] if k != item.canonical_name: cmds[item.canonical_name].append(k) self.map = cmds def dispatch(self, opts): if self.table.has_key(opts.fixed[0]): self.table[opts.fixed[0]].run(opts, globals) else: usage() ############################### FUNCTIONS ########################### def usage(): print "Syntax error, type '%s help' for help." % globals.binname sys.exit(1) ############################## ENTRY POINT ########################## # # Options # opt_actions = DITrack.Util.cmdline.Option ( name = "actions", descr = "specify actions to take", type = "string", arg = "ACTIONLIST", aliases = [ "-a" ] ) opt_comment_file = DITrack.Util.cmdline.Option ( name = "comment_file", descr = "specify a file containing the comment message", type = "string", arg = "FILE", aliases = [ "-F" ] ) opt_comment_message = DITrack.Util.cmdline.Option ( name = "comment_message", descr = "specify a comment message", type = "string", arg = "MESSAGE", aliases = [ "-m" ] ) opt_database = DITrack.Util.cmdline.Option ( name = "database", descr = "specify the path to an issue database", type = "string", arg = "PATH", aliases = [ "-d" ] ) opt_listing_format = DITrack.Util.cmdline.Option ( name = "listing_format", descr = "specify listing format", type = "string", arg = "FORMAT", aliases = [ "-f", "--format" ] ) opt_listing_format_list = DITrack.Util.cmdline.Option ( name = "listing_format_list", descr = "list available output formats", type = "boolean", arg = "", aliases = [ "--list-formats" ] ) opt_headers_only = DITrack.Util.cmdline.Option ( name = "headers_only", descr = "display only headers", type = "boolean", arg = "", aliases = [ "--headers-only" ], ) opt_maxage = DITrack.Util.cmdline.Option ( name = "maxage", descr = "tolerate the database to be out-of-date this much", type = "string", arg = "SECONDS", aliases = [ "--maxage" ] ) opt_no_commits = DITrack.Util.cmdline.Option ( name = "no_commits", descr = "do not perform commits", type = "boolean", arg = "", aliases = [ "-n", "--no-commits" ] ) opt_path = DITrack.Util.cmdline.Option ( name = "path", descr = "dump the path of the file, not its contents", type = "boolean", arg = "", aliases = [ "--path" ], ) opt_user = DITrack.Util.cmdline.Option ( name = "user", descr = "specify the user name", type = "string", arg = "NAME", aliases = [ "-u", "--user" ], ) opt_version = DITrack.Util.cmdline.Option ( name = "version", descr = "display version information", type = "boolean", arg = "", aliases = [ "--version" ], ) opt_xml = DITrack.Util.cmdline.Option ( name = "xml", descr = "output in XML", type = "boolean", arg = "", aliases = [ "--xml" ], ) # # Commands # cmd_act = DITrack.Command.act.Handler( opt_actions, opt_comment_file, opt_comment_message, opt_database, opt_no_commits, opt_user, ) cmd_cat = DITrack.Command.cat.Handler( opt_database, opt_headers_only, opt_path, opt_xml, ) cmd_commit = DITrack.Command.commit.Handler( opt_database, opt_user, ) cmd_help = DITrack.Command.help.Handler( ) cmd_list = DITrack.Command.list.Handler( opt_database, opt_user, opt_listing_format, opt_listing_format_list, opt_xml, ) cmd_new = DITrack.Command.new.Handler( opt_database, opt_no_commits, opt_user, ) cmd_remove = DITrack.Command.remove.Handler( opt_database, opt_user, ) cmd_status = DITrack.Command.status.Handler( opt_database, ) cmd_update = DITrack.Command.update.Handler( opt_database, opt_maxage, opt_user, ) # Create command dispatching table. globals.command_table = CommandTable({ "act": cmd_act, "cat": cmd_cat, "ci": cmd_commit, "commit": cmd_commit, "help": cmd_help, "list": cmd_list, "ls": cmd_list, "new": cmd_new, "remove": cmd_remove, "rm": cmd_remove, "status": cmd_status, "st": cmd_status, "update": cmd_update, "up": cmd_update, }) DITrack.Logging.init() try: opts = DITrack.Util.cmdline.ParseResults([ opt_actions, opt_comment_file, opt_comment_message, opt_database, opt_headers_only, opt_listing_format, opt_listing_format_list, opt_maxage, opt_no_commits, opt_path, opt_user, opt_version, opt_xml, ]) except DITrack.Util.cmdline.DuplicateOptionError, e: print "Duplicate option:", e.option sys.exit(1) except DITrack.Util.cmdline.InvalidValueTypeError, e: print "Invalid parameter type for: -" + e.option sys.exit(1) except DITrack.Util.cmdline.MissingParameterError, e: print "Option requires a parameter:", e.option sys.exit(1) except DITrack.Util.cmdline.UnknownOptionError, e: print "Unknown option:", e.option sys.exit(1) if opts.var["version"]: print globals.dt_title sys.exit(0) if len(opts.fixed) < 1: usage() try: globals.command_table.dispatch(opts) except DITrack.Command.generic.SyntaxError: usage() DITrack-0.8/dt-createdb0000755000076500007650000000760611032045302014427 0ustar vssvss00000000000000#! /usr/bin/env python # # The script to create empty issue database. # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: dt-createdb 2439 2008-01-21 16:17:21Z gli $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/dt-createdb $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import os import sys def mkdir(path): try: os.mkdir(path) except: sys.stdout.write("Failed to mkdir '%s'\n" % path) sys.exit(1) ############################################################################# # ENTRY POINT # ############################################################################# if len(sys.argv) != 4: sys.stdout.write( """Syntax: %s The script non-recursively checks out into (which should not exist), creates there and schedules it for addition. """ % os.path.basename(sys.argv[0])) sys.exit(1) (repo, dbdir, localwc) = sys.argv[1:4] if os.path.exists(localwc): sys.stdout.write("'%s' already exists\n" % localwc) sys.exit(1) if os.system("svn co -Nq %s %s" % (repo, localwc)): sys.stdout.write("Can't check out working copy to %s\n" % localwc) sys.exit(1) dbdir = os.path.join(localwc, dbdir) mkdir(dbdir) mkdir("%s/data" % dbdir) mkdir("%s/etc" % dbdir) mkdir("%s/meta" % dbdir) open("%s/README.txt" % dbdir, "w").write( """This is an issue database maintained by DITrack. Check out http://www.ditrack.org/ for more information. """ ) open("%s/etc/categories" % dbdir, "w").write( """Category: sample Default-owner: sample-user Version-set: sample-versions-set """ ) open("%s/etc/filters" % dbdir, "w").write( """sample-filter: Status=open,Owned-by=sample-user """ ) open("%s/etc/listing-format" % dbdir, "w").write( """[listing-formats] default: %(id)-4s %(Owned-by)-8s %(Due-in)-8s %(Status)-6s %(Title)s """ ) open("%s/etc/users" % dbdir, "w").write( """sample-user """ ) open("%s/etc/versions" % dbdir, "w").write( """sample-versions-set: 0.1 0.2 / 0.3 0.4 / 0.5 0.6 """ ) open("%s/meta/next-id" % dbdir, "w").write("1\n") if os.system("svn add -q %s" % dbdir): sys.stdout.write("Can't schedule '%s' for addition\n" % dbdir) sys.exit(1) try: open(os.path.join(dbdir, "format"),"w").write("4\n") except: sys.stdout.write("Can't initialize database format version file\n") sys.exit(1) if os.system("svn ps svn:ignore .ditrack %s" %dbdir): sys.stdout.write("Can't set the ignore list\n") sys.exit(1) sys.stdout.write(""" Empty issue database created at: '%s' Now you should probably type something like: svn commit %s """ % (dbdir, dbdir)) DITrack-0.8/FAQ0000644000076500007650000000146711032045302012654 0ustar vssvss00000000000000DITrack: Frequently Asked Questions $Id: FAQ 1615 2007-05-18 20:22:19Z gli $ $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/FAQ $ 1. How do I close an issue? =========================== You can close an issue with the 'act' command: you'll be brought to a menu where several actions for the issue are available. The 'c' menu option marks the issue as 'closed'. 2. The documentation mentions a pre-commit hook script to be installed on the server side, but there seems to be no such one in the distribution archive? =========================================================================== That's correct. There is no such script implemented in current version of DITrack. It will be added in future releases. Though, you can still use DITrack even without this additional consistency enforcement measure. DITrack-0.8/LICENSE0000644000076500007650000000247211032045302013324 0ustar vssvss00000000000000Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. DITrack-0.8/PKG-INFO0000644000076500007650000000041511032045407013415 0ustar vssvss00000000000000Metadata-Version: 1.0 Name: DITrack Version: 0.8 Summary: Distributed Issue Tracking system Home-page: http://www.ditrack.org Author: The DITrack Project Author-email: dev@lists.ditrack.org License: http://www.ditrack.org/LICENSE Description: UNKNOWN Platform: UNKNOWN DITrack-0.8/README0000644000076500007650000000267511032045302013204 0ustar vssvss00000000000000DITrack README $Id: README 2522 2008-05-28 06:58:24Z vss $ $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/README $ Copyright (c) 2006-2008 The DITrack Project, www.ditrack.org. REQUIREMENTS ============ * Python 2.3 or higher * Subversion 1.3 or higher DOCUMENTATION ============= NB! The documentation describes DITrack as of v.0.4. Sorry, we don't have enough bandwidth to update it timely. :-( Any contributions are welcome. See doc/html/index.html. An up-to-date brief introduction to DITrack can be found at doc/quicktour/index.html. INSTALLATION ============ Run the setup.py installation script: $ python setup.py install To get a list of available options, type: $ python setup.py --help See webui/README for the web interface installation instructions. UPGRADING FROM PREVIOUS VERSIONS ================================ Automatic upgrade is possible for databases created by DITrack 0.7. If you are upgrading from an older version of DITrack, use the upgrage utility from DITrack 0.7 first. To upgrade a database run the 'upgrade-0.7-db.py' script, passing the database path as the argument, like: $ ./upgrade-0.7-db.py /home/user/ditrack-database The upgrade procedure merely modifies the working copy (nothing gets committed to the repository). So, when done, you should commit the changes manually, like: $ svn ci -m "Upgraded to DITrack 0.8 format" /home/user/ditrack-database RESOURCES ========= http://www.ditrack.org DITrack-0.8/setup.py0000644000076500007650000000103211032045406014025 0ustar vssvss00000000000000#! /usr/bin/env python from distutils.core import setup setup( name="DITrack", version="0.8", description="Distributed Issue Tracking system", author="The DITrack Project", author_email="dev@lists.ditrack.org", url="http://www.ditrack.org", license="http://www.ditrack.org/LICENSE", packages=[ "DITrack", "DITrack/Backend", "DITrack/DB", "DITrack/dt", "DITrack/Command", "DITrack/ThirdParty", "DITrack/ThirdParty/Python", "DITrack/Util" ], scripts=[ "dt", "dt-createdb" ] ) DITrack-0.8/upgrade-0.7-db.py0000755000076500007650000000503711032045302015210 0ustar vssvss00000000000000#!/usr/bin/env python # # upgrade-0.7-db.py - utility to convert database format from version 3 (0.6, # 0.7) to version 4 (0.8) # # Copyright (c) 2006-2008 The DITrack Project, www.ditrack.org. # # $Id: upgrade-0.7-db.py 2520 2008-05-28 06:56:26Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/upgrade-0.7-db.py $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import os import sys if len(sys.argv) != 2: sys.stderr.write(""" This is a utility to convert DITrack database from format 3 (DITrack version 0.6, 0.7) to format 4 (DITrack 0.8). Usage: %s DBPATH """ % sys.argv[0]) sys.exit(1) dbpath = sys.argv[1] version = os.popen("svn pg ditrack:format %s" % (dbpath), "r").readline().strip() if version != "3": sys.stderr.write("Unsupported database format version: %s" % version) sys.exit(1) listing_format = "%s/etc/listing-format" % (dbpath) open(listing_format, "w").write( """[listing-formats] default: %(id)-4s %(Owned-by)-8s %(Due-in)-8s %(Status)-6s %(Title)s """ ) format=os.path.join(dbpath, "format") try: open(format,"w").write("4\n") except: sys.stdout.write("Can't initialize database format version file\n") sys.exit(1) os.system("svn add %s" % (format)) sys.stdout.write(""" Database was successully converted Now you should probably type something like: svn commit %s """ % (dbpath)) DITrack-0.8/webui/0000755000076500007650000000000011032045407013433 5ustar vssvss00000000000000DITrack-0.8/webui/config0000644000076500007650000000104011032045302014610 0ustar vssvss00000000000000# # DITrack web interface configuration file # [paths] database: /X/W/ditrack.org/issues/data/issues templates: /u/vss/ditrack/src/trunk/webui/templates svn: /usr/local/bin/svn [appearance] title = DITrack Issues: The Web Interface [misc] # If set, references to revisions in issue comments will be linked to ViewSVN # at specified location. viewsvn-url: http://viewsvn.ditrack.org/index.cgi # Perform database update (sync with the repository) if it's older than this # number of minutes at the time the CGI is invoked. update-interval: 5 DITrack-0.8/webui/index.cgi0000755000076500007650000001407711032045302015234 0ustar vssvss00000000000000#!/usr/bin/env python # # index.cgi - DITrack Web UI CGI script # # Copyright (c) 2006-2007 The DITrack Project, www.ditrack.org. # # $Id: index.cgi 2056 2007-09-11 04:09:15Z vss $ # $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/webui/index.cgi $ # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import cgi import os import re import sys import ConfigParser # XXX: debugging import cgitb; cgitb.enable() import DITrack.dt.globals import DITrack.Client import DITrack.DB.Common import DITrack.ThirdParty.ezt class _data_obj: def __init__(self, d): vars(self).update(d) class Issue: def __init__(self, id, issue): self.id = html_escape(str(id)) self.view_url = html_escape("%s?issue=%s" % (script_url, id)) for h in issue.info: vars(self).update({ "hdr_%s" % h.lower().replace("-", "_"): html_escape(issue.info[h]) }) def html_escape(str): if str is None: return "" return cgi.escape(str, quote=True) def generate_page(template_name, data): template = DITrack.ThirdParty.ezt.Template( os.path.join(tpl_dir, "%s.ezt" % template_name) ) data["general"] = general sys.stdout.write("Content-type: text/html\n\n") template.generate(sys.stdout, data) def linkify(s): s = re.sub( r"(i#(\d+))", '\\1' % html_escape(script_url), s ) if viewsvn_url: s = re.sub( r"((\W|^)(r(\d+)))", '\\2\\3' % html_escape(viewsvn_url), s ) return s def do_list(): filters = db.cfg.filters.keys() filters.sort() filters = map(html_escape, filters) current_filter = filter = None if ("filter" in form) and (form["filter"].value in db.cfg.filters): current_filter = html_escape(form["filter"].value) filter = [ db.cfg.filters[form["filter"].value] ] issues = dt.issues(filter) data = { "current_filter": current_filter, "filters": filters, "id": None, # no issue id here "issues": map( lambda (id, issue): Issue(id, issue), issues ), "qty": len(issues) } generate_page("list", data) def do_view(): id = form["issue"].value try: issue = db[id] except KeyError: # Invalid issue id supplied generate_page("invalid-issue", { "id": html_escape(id) }) return general.title = "Issue #%s: %s" % (html_escape(id), html_escape(issue.info["Title"])) data = { "id": html_escape(id), "info": map(html_escape, issue.info_as_strings(terminator="")), "comments": map( lambda (cid, comment): _data_obj({ "author": html_escape(comment.added_by), "datetime": html_escape( " ".join((comment.added_on or "").split()[1:]) ), "header": map( html_escape, comment.header_as_strings(terminator="") ), "id": html_escape(cid), "link_url": html_escape( "%s?issue=%s#c%s" % (script_url, id, cid) ), "text": linkify(html_escape(comment.text)) }), issue.comments() ), "title": html_escape(issue.info["Title"]), } generate_page("view", data) # # ENTRY POINT # if "DTWEB_CONFIG" not in os.environ: sys.stderr.write("DTWEB_CONFIG not set\n") sys.exit(1) cfg = ConfigParser.ConfigParser() cfg.read(os.environ["DTWEB_CONFIG"]) # XXX: catch exceptions here dbroot = cfg.get("paths", "database") tpl_dir = cfg.get("paths", "templates") svn_path = cfg.get("paths", "svn") maxage = cfg.get("misc", "update-interval") try: maxage = int(maxage) except ValueError: sys.stderr.write("Invalid value of misc/update-interval: '%s'\n" % maxage) sys.exit(1) # Not mandatory viewsvn_url = None try: viewsvn_url = cfg.get("misc", "viewsvn-url") except (ConfigParser.NoSectionError, ConfigParser.NoOptionError): pass script_url = os.environ["SCRIPT_NAME"] # XXX: should be combined into a single call to Client(). db = DITrack.DB.Common.Database(dbroot, None, svn_path) dt = DITrack.Client.Client(db) try: dt.update(maxage * 60) except DITrack.Client.Error, e: sys.stderr.write("%s\n" % e.message) sys.exit(1) form = cgi.FieldStorage() general = _data_obj({ "title": "" }) try: general.title = html_escape(cfg.get("appearance", "title")) except NoOptionError: pass if "issue" in form: # Viewing an issue do_view() else: do_list() DITrack-0.8/webui/README0000644000076500007650000000205611032045302014310 0ustar vssvss00000000000000WebUI is a read-only interface to DITrack database. Its main purpose is to provide a quick glance into the database, not to create a full-fledged tool for manipulating the data. Here are the steps to install WebUI: * Set up a working copy for WebUI to use. Make sure that the user your web server runs as has read/write access to the directory. * Edit the WebUI configuration ('config' is a sample). * Tweak the templates as needed (samples are in the 'templates' directory). Make sure that the configuration file refers to a correct path. * Edit your web server configuration. Make sure that 'index.cgi' is treated as a CGI executable. Make sure that environment variable 'DTWEB_CONFIG' points to your configuration file. Make sure that PYTHONPATH includes path to DITrack modules. Here is an example configuration for Apache: Options -Indexes +ExecCGI +FollowSymlinks DirectoryIndex index.cgi SetEnv DTWEB_CONFIG /usr/local/etc/ditrack/webui.config SetEnv PYTHONPATH /usr/local/lib/python2.4/site-packages DITrack-0.8/webui/templates/0000755000076500007650000000000011032045407015431 5ustar vssvss00000000000000DITrack-0.8/webui/templates/footer.ezt0000644000076500007650000000002011032045302017435 0ustar vssvss00000000000000 DITrack-0.8/webui/templates/header.ezt0000644000076500007650000000030711032045302017377 0ustar vssvss00000000000000 [general.title]

DITrack-0.8/webui/templates/invalid-issue.ezt0000644000076500007650000000012411032045302020720 0ustar vssvss00000000000000[include "header.ezt"]

Invalid issue number: [id]

[include "footer.ezt"] DITrack-0.8/webui/templates/list-banner.ezt0000644000076500007650000000006111032045302020362 0ustar vssvss00000000000000

This is the DITrack web interface. Wow!

DITrack-0.8/webui/templates/list.ezt0000644000076500007650000000131411032045302017121 0ustar vssvss00000000000000[include "header.ezt"] [include "list-banner.ezt"]

[if-any current_filter] Issues matching '[current_filter]' [else] All existing issues [end]


[for issues] [end]
[issues.id] [issues.hdr_owned_by] [issues.hdr_due_in] [issues.hdr_status] [issues.hdr_title]

[qty] issues found. [include "footer.ezt"] DITrack-0.8/webui/templates/view.ezt0000644000076500007650000000076411032045302017130 0ustar vssvss00000000000000[include "header.ezt"]

Issue #[id]: [title]

[for comments]
[is comments.id "0"]
		[for info][info]
		[end]
		
[else] Comment #[comments.id] (link) by [comments.author] on [comments.datetime]
[for comments.header][comments.header]
		[end]
[end]
	[comments.text]
	
[end]
[include "footer.ezt"]