diff --git a/hooks/trac-post-receive b/hooks/trac-post-receive new file mode 100755 index 0000000..9578186 --- /dev/null +++ b/hooks/trac-post-receive @@ -0,0 +1,373 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Main script: +# trac-post-commit-hook +# ---------------------------------------------------------------------------- +# Copyright (c) 2004 Stephen Hansen +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# ---------------------------------------------------------------------------- + +# Git functionality: +# ---------------------------------------------------------------------------- +# Copyright (c) 2010 Grzegorz SobaƄski +# +# Git post receive script developed for mlabs +# - adds the commits to trac +# based on post-receive-email from git-contrib +# ---------------------------------------------------------------------------- + +### Changes for the Timing and Estimation plugin +# +# This script is very similar to "trac-post-commit" included with trac +# itself. This section explains functional changes relative to that +# script, and comments throughout the code explain other differences. +# +## Logging support for debugging this hook +# +## Support for specifying time spent in commit messages. +# +# "Blah refs #12 (1)" will add 1h to the spent time for issue #12 +# "Blah refs #12 (spent 1.5)" will add 1h to the spent time for issue #12 +# +# As above it is possible to use complicated messages: +# +# "Changed blah and foo to do this or that. Fixes #10 (1) and #12 (2), +# and refs #13 (0.5)." +# +# This will close #10 and #12, and add a note to #13 and also add 1h +# spent time to #10, add 2h spent time to #12 and add 30m spent time +# to #13. +# +# Note that: +# (spent 2), (sp 2) or simply (2) may be used for spent +# ' ', ',', '&' or 'and' may be used references + +# This Subversion post-commit hook script is meant to interface to the +# Trac (http://www.edgewall.com/products/trac/) issue tracking/wiki/etc +# system. +# +# It should be called from the 'post-commit' script in Subversion, such as +# via: +# +# REPOS="$1" +# REV="$2" +# TRAC_ENV="/path/to/tracenv" + +# (or as a git post-update hook -- set USE_GIT, below) + +# /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \ +# -p "$TRAC_ENV" -r "$REV" +# +# (all the other arguments are now deprecated and not needed anymore) +# +# It searches commit messages for text in the form of: +# command #1 +# command #1, #2 +# command #1 & #2 +# command #1 and #2 +# +# Instead of the short-hand syntax "#1", "ticket:1" can be used as well, e.g.: +# command ticket:1 +# command ticket:1, ticket:2 +# command ticket:1 & ticket:2 +# command ticket:1 and ticket:2 +# +# In addition, the ':' character can be omitted and issue or bug can be used +# instead of ticket. +# +# You can have more then one command in a message. The following commands +# are supported. There is more then one spelling for each command, to make +# this as user-friendly as possible. +# +# close, closed, closes, fix, fixed, fixes +# The specified issue numbers are closed with the contents of this +# commit message being added to it. +# references, refs, addresses, re, see +# The specified issue numbers are left in their current status, but +# the contents of this commit message are added to their notes. +# +# A fairly complicated example of what you can do is with a commit message +# of: +# +# Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12. +# +# This will close #10 and #12, and add a note to #12. + +import re +import os +import sys +from datetime import datetime +from optparse import OptionParser +from subprocess import Popen, PIPE, call + +parser = OptionParser() +depr = '(not used anymore)' +parser.add_option('-e', '--require-envelope', dest='envelope', default='', + help=""" +Require commands to be enclosed in an envelope. +If -e[], then commands must be in the form of [closes #4]. +Must be two characters.""") +parser.add_option('-p', '--project', dest='project', + help='Path to the Trac project.') +parser.add_option('-r', '--revision', dest='rev', + help='Repository revision number.') +parser.add_option('-u', '--user', dest='user', + help='The user who is responsible for this action '+depr) +parser.add_option('-m', '--msg', dest='msg', + help='The log message to search '+depr) +parser.add_option('-c', '--encoding', dest='encoding', + help='The encoding used by the log message '+depr) +parser.add_option('-s', '--siteurl', dest='url', + help=depr+' the base_url from trac.ini will always be used.') + +(options, args) = parser.parse_args(sys.argv[1:]) + + +USE_GIT = True + +# Location of the git command +GIT_PATH='/usr/bin/git' + +# File mapping SVN-style usernames to git name/addresses. +# See git-svn for the format of this file. +SVN_AUTHORS_FILE='/srv/git/svn-authors.txt' + +if not 'PYTHON_EGG_CACHE' in os.environ: + os.environ['PYTHON_EGG_CACHE'] = os.path.join(options.project, '.egg-cache') + +from trac.env import open_environment +from trac.ticket.notification import TicketNotifyEmail +from trac.ticket import Ticket +from trac.ticket.web_ui import TicketModule +# TODO: move grouped_changelog_entries to model.py +from trac.util.text import to_unicode +from trac.util.datefmt import utc +from trac.versioncontrol.api import NoSuchChangeset, RepositoryManager + +from trac.ticket.default_workflow import ConfigurableTicketWorkflow +from trac.ticket import TicketSystem + +Author_map = {} +if USE_GIT: + for line in open(SVN_AUTHORS_FILE): + (trac_name, git_name) = [part.strip() for part in line.split('=')] + if not Author_map.has_key(git_name): + Author_map[git_name] = trac_name + +def get_available_actions(env, action='resolve'): + # The list should not have duplicates. + ts = TicketSystem(env) + for controller in ts.action_controllers: + if isinstance(controller, ConfigurableTicketWorkflow): + return controller.actions.get(action) + return None + +def get_next_status(env,action='resolve'): + action = get_available_actions(env,action) + return action['newstate'] + +def call_git(command, args, input=None): + return Popen([GIT_PATH, command] + args, stdin=PIPE, stdout=PIPE).communicate(input)[0] + +def handle_ref_trac(old, new, ref): + # branch delete, skip it + if re.match('0*$', new): + return [] + + if re.match('0*$', old): + # create + revspec = "%s" % new + else: + # update + revspec = "%s..%s" % (old, new) + + all_branches = call_git('for-each-ref', ['--format=%(refname)', 'refs/heads/']).splitlines() + other_branches = [branch for branch in all_branches if not branch == ref] + not_other_branches = call_git('rev-parse', ['--not'] + other_branches) + new_commits = call_git('rev-list', ['--stdin', '--reverse', revspec], not_other_branches).splitlines() + return new_commits + +# Change logfile to point to someplace this script can write. +logfile = "/var/trac/commithook.log" +LOG = True + +if LOG: + f = open (logfile,"w") + f.close() + def log (s, *params): + f = open (logfile,"a") + f.write(s % params) + f.write("\n") + f.close() +else: + def log (s, *params): + pass + +# Relative to trac standard, this table is hoisted out of class +# CommitHook so that it can be used in constructing a regexp that only +# matches on supported commands. +_supported_cmds = {'close': '_cmdClose', + 'closed': '_cmdClose', + 'closes': '_cmdClose', + 'fix': '_cmdClose', + 'fixed': '_cmdClose', + 'fixes': '_cmdClose', + 'addresses': '_cmdRefs', + 're': '_cmdRefs', + 'references': '_cmdRefs', + 'refs': '_cmdRefs', + 'see': '_cmdRefs'} + +# Regexps are extended to include "(1)" and "(spent 1)". +ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)' +time_pattern = r'[ ]?(?:\((?:(?:spent|sp)[ ]?)?(-?[0-9]*(?:\.[0-9]+)?)\))?' +ticket_reference = ticket_prefix + '[0-9]+' + time_pattern +support_cmds_pattern = '|'.join(_supported_cmds.keys()) + +# Relative to upstream, only match command tokens (rather than +# matching all words). +ticket_command = (r'(?P(?:%s))[ ]*' + '(?P%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' % + (support_cmds_pattern, ticket_reference, ticket_reference)) + +if options.envelope: + ticket_command = r'\%s%s\%s' % (options.envelope[0], ticket_command, + options.envelope[1]) + +# Because we build the regexp to recognize only supported commands, +# ignore case here. +command_re = re.compile(ticket_command, re.IGNORECASE) +ticket_re = re.compile(ticket_prefix + '([0-9]+)' + time_pattern, re.IGNORECASE) + +class CommitHook: + def __init__(self, project=options.project, author=options.user, + rev=options.rev, url=options.url): + self.env = open_environment(project) + + repos = RepositoryManager(self.env).get_repository(None) + repos.sync() + + # Instead of bothering with the encoding, we'll use unicode data + # as provided by the Trac versioncontrol API (#1310). + try: + chgset = repos.get_changeset(rev) + except NoSuchChangeset: + return # out of scope changesets are not cached + + # Trac uses SVN-style usernames, so if using git, map the + # git name/email to a Trac username. + self.author = chgset.author + if Author_map.has_key(self.author): + self.author = Author_map[self.author] + self.rev = rev + self.msg = "(In [%s]) %s" % (rev, chgset.message) + self.now = datetime.now(utc) + + cmd_groups = command_re.findall(self.msg) + + log ("cmd_groups:%s", cmd_groups) + tickets = {} + # \todo Explain what xxx1 and xxx2 do; I can't see more params + # in command_re. + for cmd, tkts, xxx1, xxx2 in cmd_groups: + log ("cmd:%s, tkts%s ", cmd, tkts) + funcname = _supported_cmds.get(cmd.lower(), '') + if funcname: + for tkt_id, spent in ticket_re.findall(tkts): + func = getattr(self, funcname) + tickets.setdefault(tkt_id, []).append([func, spent]) + + for tkt_id, vals in tickets.iteritems(): + log ("tkt_id:%s, vals%s ", tkt_id, vals) + spent_total = 0.0 + try: + db = self.env.get_db_cnx() + + ticket = Ticket(self.env, int(tkt_id), db) + for (cmd, spent) in vals: + cmd(ticket) + if spent: + spent_total += float(spent) + + # determine sequence number... + cnum = 0 + tm = TicketModule(self.env) + for change in tm.grouped_changelog_entries(ticket, db): + if change['permanent']: + cnum += 1 + + if spent_total: + self._setTimeTrackerFields(ticket, spent_total) + ticket.save_changes(self.author, self.msg, self.now, db, cnum+1) + db.commit() + + tn = TicketNotifyEmail(self.env) + tn.notify(ticket, newticket=0, modtime=self.now) + except Exception, e: + # import traceback + # traceback.print_exc(file=sys.stderr) + log('Unexpected error while processing ticket ' \ + 'ID %s: %s' % (tkt_id, e)) + print>>sys.stderr, 'Unexpected error while processing ticket ' \ + 'ID %s: %s' % (tkt_id, e) + + + def _cmdClose(self, ticket): + status = get_next_status(ticket.env, 'resolve') or 'closed' + ticket['status'] = status + ticket['resolution'] = 'fixed' + + def _cmdRefs(self, ticket): + pass + + def _setTimeTrackerFields(self, ticket, spent): + log ("Setting ticket:%s spent: %s", ticket, spent) + if (spent != ''): + spentTime = float(spent) + # \bug If the ticket has not been modified since + # TimingAndEstimation was installed, then it might not + # have hours. It should still get hours applied because + # estimating and recording are separate. + if (ticket.values.has_key('hours')): + ticket['hours'] = str(spentTime) + +if __name__ == "__main__": + if USE_GIT: + commits = [] + for line in sys.stdin: + commits += handle_ref_trac(*line.split()) + for c in commits: + try: + CommitHook(rev=c) + except Exception, e: + log('ERROR while processing: %s' % str(e) ) + sys.exit(1) + elif len(sys.argv) < 5: + print "For usage: %s --help" % (sys.argv[0]) + print + print "Note that the deprecated options will be removed in Trac 0.12." + else: + try: + CommitHook() + except Exception, e: + log('ERROR while processing: %s' % str(e) ) + sys.exit(1) + diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 5022f4f..21fe9e2 --- a/setup.py +++ b/setup.py @@ -9,12 +9,17 @@ author_email='hvr@gnu.org', keywords='trac scm plugin git', url="http://trac-hacks.org/wiki/GitPlugin", - version='0.12.0.5', + version='0.12.0.7', license="GPL", long_description=""" This Trac 0.12 plugin provides support for the GIT SCM. See http://trac-hacks.org/wiki/GitPlugin for more details. + + This is a forked version from + https://github.com/dmarti/trac-git-plugin + This version does more mapping of old Subversion data, for + converted repositories. """, packages=['tracext', 'tracext.git'], namespace_packages=['tracext'], diff --git a/tracext/git/PyGIT.py b/tracext/git/PyGIT.py index 2f33951..c12dd2d 100644 --- a/tracext/git/PyGIT.py +++ b/tracext/git/PyGIT.py @@ -126,14 +126,14 @@ class StorageFactory(object): __dict_nonweak = dict() __dict_lock = Lock() - def __init__(self, repo, log, weak=True, git_bin='git', git_fs_encoding=None): + def __init__(self, repo, log, weak=True, git_bin='git', git_fs_encoding=None, was_svn=False): self.logger = log with StorageFactory.__dict_lock: try: i = StorageFactory.__dict[repo] except KeyError: - i = Storage(repo, log, git_bin, git_fs_encoding) + i = Storage(repo, log, git_bin, git_fs_encoding, was_svn) StorageFactory.__dict[repo] = i # create or remove additional reference depending on 'weak' argument @@ -205,7 +205,8 @@ def try_int(s): " (tried to execute/parse '%s --version' but got %s)" % (git_bin, repr(e))) - def __init__(self, git_dir, log, git_bin='git', git_fs_encoding=None): + def __init__(self, git_dir, log, git_bin='git', git_fs_encoding=None, + was_svn = False): """ Initialize PyGit.Storage instance @@ -222,6 +223,8 @@ def __init__(self, git_dir, log, git_bin='git', git_fs_encoding=None): unicode objects is performed, and bytestrings are returned instead + `was_svn`: try looking up revisions from the git-svn-id lines added + on import from Subversion. """ self.logger = log @@ -253,6 +256,7 @@ def __init__(self, git_dir, log, git_bin='git', git_fs_encoding=None): self.repo = GitCore(git_dir, git_bin=git_bin) self.commit_encoding = None + self.was_svn = was_svn # caches self.__rev_cache = None @@ -550,6 +554,11 @@ def fullrev(self, srev): # short-cut if len(srev) == 40 and srev in _rev_cache.rev_dict: return srev + + if self.was_svn: + sha = self.svn_rev_to_sha(srev) + if sha: + return sha if not GitCore.is_sha(srev): return None @@ -593,6 +602,28 @@ def split_ls_tree_line(l): return [ split_ls_tree_line(e) for e in tree if e ] + def svn_rev_to_sha(self, rev, cache = {}): + tc = '' + commitline = re.compile("commit ([0-9a-fA-F]{40})") + svnline = re.compile('git-svn-id:.*\D+(\d+)\s') + try: + rev = int(rev) + cached = cache.get(rev) + if cached: + return cached + for line in self.repo.log().splitlines(): + m = commitline.match(line) + if m: + tc = m.group(1) + else: + m = svnline.search(line) + if m and int(m.group(1)) == rev: + cache.update({rev:tc}) + return tc + return None + except ValueError: + return rev + def read_commit(self, commit_id): if not commit_id: raise GitError("read_commit called with empty commit_id") @@ -602,7 +633,7 @@ def read_commit(self, commit_id): db = self.get_commits() if commit_id not in db: self.logger.info("read_commit failed for '%s' ('%s')" % - (commit_id, commit_id_orig)) + (commit_id, commit_id_orig)) raise GitErrorSha with self.__commit_msg_lock: diff --git a/tracext/git/git_fs.py b/tracext/git/git_fs.py index c33536c..896377b 100644 --- a/tracext/git/git_fs.py +++ b/tracext/git/git_fs.py @@ -363,9 +363,13 @@ def __init__(self, path, params, log, self._use_committer_time = use_committer_time self._use_committer_id = use_committer_id + _use_svn_id = BoolOption('git', 'use_svn_id', 'false', + "try looking up revisions by git-svn-id if present") + self.git = PyGIT.StorageFactory(path, log, not persistent_cache, git_bin=git_bin, - git_fs_encoding=git_fs_encoding).getInstance() + git_fs_encoding=git_fs_encoding, + was_svn=_use_svn_id).getInstance() Repository.__init__(self, "git:"+path, self.params, log)