@@ 0,0 1,262 @@
+# Trac syncing ditz plugin
+#
+# Provides ditz <-> trac synchronization
+#
+# Command added:
+# ditz sync: synchronize issues with Trac
+#
+# Usage:
+# 1. add a line "- trac-sync" to the .ditz-plugins file in the project root.
+
+require 'rubygems'
+require 'trac4r'
+require 'ditz'
+
+module Ditz
+ class Issue
+ def log_at time, what, who, comment
+ add_log_event([time, who, what, comment || ""])
+ self
+ end
+ end
+
+ class TracUtil
+ # If new types or dispositions are added to either ditz or trac, they'll need
+ # to be mapped here
+ #
+ # trac_type -> ditz_type
+ TTYPE_DTYPE = { "defect" => :bugfix,
+ "enhancement" => :feature,
+ "task" => :task }
+ DTYPE_TTYPE = TTYPE_DTYPE.invert
+ # trac_resolution -> ditz_disposition
+ RES_DISPO = { "fixed" => :fixed,
+ "wontfix" => :wontfix,
+ "worksforme" => :wontfix,
+ "invalid" => :wontfix,
+ "duplicate" => :duplicate,
+ }
+ DISPO_RES = RES_DISPO.invert
+ # But, because it isn't 1-1, fix the mapping
+ DISPO_RES[ :wontfix ] = "wontfix"
+ DISPO_RES[ :reorg ] = "wontfix"
+ TSTATUS_DSTATUS = { "accepted" => :in_progress,
+ "repoened" => :in_progress,
+ "assigned" => :unstarted,
+ "new" => :unstarted,
+ "closed" => :closed,
+ }
+ DSTATUS_TSTATUS = TSTATUS_DSTATUS.invert
+ # But, because it isn't 1-1, fix the mapping
+ DSTATUS_TSTATUS[ :in_progress ] = "accepted"
+ DSTATUS_TSTATUS[ :unstarted ] = "assigned"
+ DSTATUS_TSTATUS[ :paused ] = "assigned"
+
+ def initialize( project, config, trac )
+ @project, @config, @trac = project, config, trac
+ end
+
+ def equal?( ticket, issue )
+ ticket.summary == issue.title
+ end
+
+ def pair( tickets )
+ rv = []
+ issues = @project.issues.clone
+ for ticket in tickets
+ issue = issues.find { |i| equal?( ticket, i ) }
+ issues.delete(issue) if issue
+ rv << [ticket,issue]
+ end
+ for issue in issues
+ rv << [nil,issue]
+ end
+ rv
+ end
+
+ def create_tickets( issues )
+ end
+
+ def create_issues( tickets )
+ tickets.each do |t,i| # i will always be nil
+ # trac4r doesn't yet support resolution
+ resolution = t.status == "closed" ? :fixed : nil
+ release = @project.releases.find { |r| r.name == t.milestone }
+ unless release
+ puts "Creating release #{t.milestone}"
+ release = Ditz::Release.create({:name=>t.milestone}, [@config, @project])
+ @project.add_release(release)
+ end
+
+ if release.status == :released
+ puts "Orphaned ticket ##{t.id}: milestone #{t.milestone} already released!"
+ puts "\t#{t.summary}"
+ else
+ issue = Ditz::Issue.create({:reporter => t.reporter,
+ :creation_time => t.created_at.to_time,
+ :title => t.summary,
+ :type => TTYPE_DTYPE[ t.type ],
+ :desc => t.description,
+ :release => t.milestone,
+ :component => t.component,
+ :status => TSTATUS_DSTATUS[ t.status ],
+ :disposition => resolution,
+ :references => []
+ }, [@config, @project])
+
+ @project.add_issue( issue )
+ puts "Created issue #{issue.id[0,4]}: #{issue.title}"
+ end
+ end
+ end
+
+ def change_status status, issue
+ if issue.status != status
+ old_status = issue.status
+ issue.status = status
+ return "changed status from #{old_status} to #{status}"
+ end
+ nil
+ end
+
+ def group_by_time(changelog)
+ rv = {}
+ changelog.each do |log|
+ t = log[0].to_time
+ rv[t] = [] unless rv[t]
+ rv[t] << log
+ end
+ rv.sort
+ end
+
+ def update_issues( pairs, trac )
+ pairs.each do |ticket, issue|
+ puts "Working on #{ticket.id}/#{issue.id[0,4]}"
+ issue_last_updated = if issue.log_events.size == 0
+ issue.creation_time
+ else
+ issue.log_events[-1][0]
+ end
+ cl = trac.tickets.changelog( ticket.id )
+ puts "Got #{cl.length} changes from Trac"
+ puts "Ditz issue last changed #{issue_last_updated}"
+ group_by_time( cl ).each do |time, array_of_changes|
+ puts "Trac changegroup @ #{time}"
+ if time > issue_last_updated
+ whats = []
+ comment = nil
+ array_of_changes.each do |new_change|
+ case new_change[2]
+ when "comment"
+ comment = new_change[2]
+ when "description"
+ if new_change[3] != issue.description
+ whats << "edited description"
+ issue.desc = new_change[3]
+ end
+ when "milestone"
+ if new_change[3] != issue.release
+ whats << "assigned to release #{new_change[3]} from #{issue.release || 'unassigned'}"
+ issue.release = new_change[3]
+ end
+ when "owner"
+ whats << change_status( :in_progress, issue )
+ when "resolution"
+ RES_DISPO
+ issue.disposition = RES_DISPO[ new_change[3] ]
+ when "status"
+ whats << change_status( TSTATUS_DSTATUS[ new_change[3] ], issue )
+ when "type"
+ new_t = TTYPE_DTYPE[ new_change[3] ]
+ whats << "type changed to #{new_t} from #{issue.type}"
+ issue.type = new_t
+
+ when "attachment", "cc", "component", "os", "priority",
+ "severity", "version"
+ # NOOP
+ else
+ # NOOP
+ end
+ end
+ # All changes were at the same time, and therefore by the same person
+ issue.log_at( time, whats.join(", "), array_of_changes[0][1], comment )
+ end
+ end
+ end
+ end
+ end
+
+ class Config
+ field :trac_sync_url, :prompt => "URL of Trac project (without the login/xmlrpc)"
+ field :trac_sync_user, :prompt => "The Trac user ID that has XMLRPC permissions"
+ field :trac_sync_pass, :prompt => "The Trac password for the account"
+ end
+
+ class Operator
+ operation :sync, "Sync with a Trac repository"
+ def sync( project, config )
+ unless config.trac_sync_url
+ STDERR.puts( "Please run 'ditz reconfigure' and set the Trac URL" )
+ return
+ end
+ unless config.trac_sync_user
+ STDERR.puts( "Please run 'ditz reconfigure' and set the Trac XMLRPC user name" )
+ return
+ end
+ unless config.trac_sync_pass
+ STDERR.puts( "Please run 'ditz reconfigure' and set the Trac XMLRPC password" )
+ return
+ end
+ trac = Trac.new(config.trac_sync_url, config.trac_sync_user, config.trac_sync_pass)
+ util = Ditz::TracUtil.new( project, config, trac )
+
+ tickets = trac.tickets.get_all.values
+ changelogs = []
+
+ pairs = util.pair(tickets)
+ util.create_issues( pairs.find_all {|m| m[1] == nil} )
+ pairs = util.pair(tickets)
+ util.update_issues( pairs.find_all {|m| m[0] != nil && m[1] != nil}, trac )
+ #util.create_tickets( pairs.find_all {|m| m[0] == nil} )
+ #issue.log "commented", config.user, comment
+ end
+ end
+end
+
+
+
+
+
+
+# ====================================== ======================================
+# Ditz Trac
+# ====================================== ======================================
+# Tickets
+# id id
+# reporter reporter
+# creation_time time
+# title summary
+# type type
+# desc description
+# release milestone
+# component component
+# status status
+# ??? comments
+# disposition resolution
+# references ???
+# N/A keywords
+# N/A operating system
+# N/A priority
+# N/A owner
+# N/A cc
+# N/A attachments
+# N/A version
+# N/A severity
+# N/A changetime
+#
+# Releases
+# release name
+# due_date
+# status completed
+# release_time completion date
+# description