@@ 2,6 2,9 @@
#
# Provides ditz <-> trac synchronization
#
+# Author:: Sean Russell <seanerussell@gmail.com>
+# Version:: 2010.1
+#
# Command added:
# ditz sync: synchronize issues with Trac
#
@@ 12,6 15,7 @@
require 'rubygems'
require 'trac4r'
require 'ditz'
+require 'logger'
module Ditz
@@ 105,8 109,8 @@ module Ditz
DSTATUS_TSTATUS[ :paused ] = "assigned"
- def initialize( project, config, trac )
- @project, @config, @trac = project, config, trac
+ def initialize( project, config, trac, logger )
+ @project, @config, @trac, @logger = project, config, trac, logger
end
@@ 115,15 119,17 @@ module Ditz
def pair( tickets )
rv = []
issues = @project.issues.clone
+ @logger.debug("Have #{tickets.size} tickets")
+ @logger.debug("Tickets: #{tickets.collect{|t|t.summary}.inspect}")
for ticket in tickets
issue = issues.find { |i| equal?( ticket, i ) }
if issue
if !issue.trac_id.nil? and (issue.trac_id != ticket.id)
- puts "ERROR! Found a match by title, but the IDs don't match!!"
- puts " issue has #{issue.trac_id}, ticket has #{ticket.id}"
+ @logger.error("ERROR! Found a match by title, but the IDs don't match!!")
+ @logger.error(" issue has #{issue.trac_id}, ticket has #{ticket.id}")
# TODO do something useful here
end
- puts "Found ticket/issue match #{ issue.trac_id ? "by id" : "by name"}"
+ @logger.info("Found ticket/issue match #{ issue.trac_id ? "by id" : "by name"}")
issues.delete(issue)
issue.trac_id = ticket.id if issue.trac_id.nil?
end
@@ 132,6 138,7 @@ module Ditz
# Now put the left-over, unmatched issues in with a nil ticket
# TODO check here that none of these issues have trac_ids.
for issue in issues
+ @logger.info("Found no match for #{issue.id[0,4]}: #{issue.title}")
rv << [nil,issue]
end
rv
@@ 144,7 151,7 @@ module Ditz
maybe_create_milestones
maybe_create_components
issues.each do |t,i| # t will always be nil
- tid = @trac.tickets.create( i.title, i.desc, {
+ attrs = {
"created_at" => i.creation_time,
"type" => DTYPE_TTYPE[ i.type ],
"reporter" => i.reporter || "",
@@ 152,10 159,19 @@ module Ditz
"component" => i.component || "",
"status" => DSTATUS_TSTATUS[ i.status ] || "",
"resolution" => DISPO_RES[ i.disposition ] || ""
- } )
+ }
+ tid = nil
+ try {
+ tid = @trac.tickets.create( i.title, i.desc, attrs )
+ }
+ @logger.debug("Created ticket #{tid} from #{i.id[0,4]} with #{attrs.inspect}")
i.trac_id = tid
- rv[i] = @trac.tickets.get(tid)
+ try {
+ rv[i] = @trac.tickets.get(tid)
+ }
+ raise "Got nil for a ticket we just created!" if rv[i].nil?
end
+ @logger.info("Created #{rv.size} new tickets")
rv
end
@@ 167,19 183,19 @@ module Ditz
tickets.each do |t,i| # i will always be nil
# trac4r doesn't yet support resolution
resolution = t.status == "closed" ? :fixed : nil
- maybe_create_release( t.milestone )
+ release = maybe_create_release( t.milestone )
maybe_create_component( t.component )
- if release.status == :released
- puts "Orphaned ticket ##{t.id}: milestone #{t.milestone} already released!"
- puts "\t#{t.summary}"
+ if release and release.status == :released
+ @logger.warn("Orphaned ticket ##{t.id}: milestone #{t.milestone} already released!")
+ @logger.warn("\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,
+ :release => release,
:component => t.component,
:status => TSTATUS_DSTATUS[ t.status ],
:disposition => resolution,
@@ 188,7 204,7 @@ module Ditz
}, [@config, @project])
@project.add_issue( issue )
- puts "Created issue #{issue.id[0,4]}: #{issue.title}"
+ @logger.info("Created issue #{issue.id[0,4]}: #{issue.title}")
rv[t] = issue
end
end
@@ 203,7 219,7 @@ module Ditz
# ticket status if it differs
def update_tickets( pairs )
pairs.each do |ticket, issue|
- puts "Working on #{ticket.id}/#{issue.id[0,4]}"
+ @logger.info("Working on #{ticket.id}/#{issue.id[0,4]}")
copy_comments( ticket, issue )
issue_last_changed = if issue.log_events.size == 0
issue.creation_time
@@ 227,8 243,10 @@ module Ditz
if DTYPE_TTYPE[issue.type] != ticket.type
attrs["type"] = DTYPE_TTYPE[issue.type]
end
- puts "Updating ticket #{ticket.id} with #{attrs.inspect}"
- @trac.query( "ticket.update", ticket.id, "Ticket synced from ditz by #{@config.user}", attrs )
+ @logger.info("Updating ticket #{ticket.id} with #{attrs.inspect}")
+ try {
+ @trac.query( "ticket.update", ticket.id, "Ticket synced from ditz by #{@config.user}", attrs )
+ }
end
end
end
@@ 236,7 254,7 @@ module Ditz
def update_issues( pairs )
pairs.each do |ticket, issue|
- puts "Working on #{ticket.id}/#{issue.id[0,4]}"
+ @logger.info( "Working on #{ticket.id}/#{issue.id[0,4]}" )
copy_comments( issue, ticket )
issue_last_changed = if issue.log_events.size == 0
issue.creation_time
@@ 268,46 286,88 @@ module Ditz
# Utility methods used internal to the class
private
+
+ def try
+ tries = 0
+ begin
+ @logger.debug( "Try #{tries}" )
+ yield
+ tries = 0
+ rescue Trac::TracException => err
+ @logger.warn(err.message) if tries == 0
+ case err.message
+ when /Wrong type NilClass/, /end of file reached/
+ @logger.warn("Had to re-connect to Trac")
+ @trac = Trac.new(@config.trac_sync_url,
+ @config.trac_sync_user,
+ @config.trac_sync_pass)
+ else
+ sleep 0.5
+ end
+ tries += 1
+ if tries <= 5
+ @logger.warn("Retry ##{tries}")
+ retry
+ else
+ raise
+ end
+ end
+ end
+
+
def copy_comments( a, b )
+ raise "Can't copy to/from nil." if a.nil? or b.nil?
if a.instance_of? Issue
issue, ticket = a, b
else
ticket, issue = a, b
end
- cl = @trac.tickets.changelog( ticket.id )
+ cl = nil
+ try {
+ cl = @trac.tickets.changelog( ticket.id )
+ }
el = issue.log_events
cl_comments = cl.find_all { |c| c[2] == "comment" }
el_comments = el.find_all { |e| e[2] == "commented" }
cl_comments.reject! do |c|
- m = el_comments.find { |e| e[2] == c[3] }
+ @logger.debug("Looking for #{c[4].inspect} in #{el_comments.collect{|m| m[3]}.inspect}")
+ m = el_comments.find { |e| e[3] == c[4] }
if m
+ @logger.debug("Found it.")
el_comments.delete(m)
true
else
+ @logger.debug("Not found")
false
end
end
cl_comments.each do |c|
- unless c[3]=="" || c[3].nil?
- puts "Updating ticket #{ticket.id} with comment #{c[3].inspect}"
- @trac.query( "ticket.update", ticket.id, c[3] )
+ unless c[4]=="" || c[4].nil?
+ @logger.info( "Updating issue #{issue.id} with comment #{c[4].inspect}" )
+ issue.log_at( c[0].to_time, "commented", c[1], c[4] )
end
end
- el_comments.each do |e|
- unless e[4]=="" || e[4].nil?
- puts "Updating issue #{issue.id} with comment #{e[4].inspect}"
- issue.log_at( e[0], "commented", e[1], e[4] )
+ if el_comments.size > 0
+ @logger.debug( "Issue comments =>" )
+ @logger.debug( el_comments.inspect )
+ el_comments.each do |e|
+ unless e[3]=="" || e[3].nil?
+ @logger.info( "Updating ticket #{ticket.id} with comment #{e[3].inspect}" )
+ try {
+ @trac.query( "ticket.update", ticket.id, e[3] )
+ }
+ end
end
end
end
def equal?( ticket, issue )
- ticket.id == issue.id || ticket.summary == issue.title
+ (ticket.id == issue.trac_id) || (ticket.summary == issue.title)
end
def change_status(status, issue)
@@ 322,23 382,28 @@ module Ditz
# Creates a ditz release, IFF it doesn't exist
# milestone: the String name of the release to create
def maybe_create_release( milestone )
+ return nil if milestone.nil? or milestone == ""
release = @project.releases.find { |r| r.name == milestone }
unless release
- puts "Creating release #{milestone}"
+ @logger.info( "Creating release #{milestone.inspect}" )
release = Ditz::Release.create({:name=>milestone}, [@config, @project])
@project.add_release(release)
end
+ release
end
# Creates a ditz component, IFF it doesn't already exist
# comp: the String name of the component to create
def maybe_create_component( comp )
+ return "" if comp.nil? or comp == ""
+ @logger.debug( @project.components.inspect )
component = @project.components.find { |r| r.name == comp }
unless component
- puts "Creating component #{comp}"
+ @logger.info( "Creating component #{comp}" )
component = Ditz::Component.create({:name=>comp}, [@config, @project])
@project.add_component(component)
end
+ component
end
def group_by_time(changelog)
@@ 355,10 420,15 @@ module Ditz
# in Trac
# trac: the trac4r object to create the components in
def maybe_create_components
- components = @trac.query("ticket.component.getAll")
+ components = nil
+ try {
+ components = @trac.query("ticket.component.getAll")
+ }
@project.components.each do |component|
next if components.include? component.name
- @trac.query("ticket.component.create", component.name, {})
+ try {
+ @trac.query("ticket.component.create", component.name, {})
+ }
end
end
@@ 366,23 436,50 @@ module Ditz
# Trac
# trac: the trac4r object to create the milestones in
def maybe_create_milestones
- milestones = @trac.query("ticket.milestone.getAll")
+ milestones = nil
+ try {
+ milestones = @trac.query("ticket.milestone.getAll")
+ }
@project.releases.each do |release|
next if milestones.include? release.name
- desc = rel = ""
+ attrs = {}
release.log_events.each do |ev|
case ev[2]
when "created"
- desc = ev[3]
+ attrs["description"] = ev[3]
when "released"
- rel = ev[0]
+ attrs["completed"] = ev[0]
end
end
+ try {
+ @trac.query("ticket.milestone.create", release.name, attrs)
+ }
+ end
+ end
+
+ def maybe_create_resolutions
+ try {
@trac.query("ticket.milestone.create", release.name, {
"completed" => rel,
"description" => desc
})
- end
+ }
+ end
+ def maybe_create_status
+ try {
+ @trac.query("ticket.milestone.create", release.name, {
+ "completed" => rel,
+ "description" => desc
+ })
+ }
+ end
+ def maybe_create_type
+ try {
+ @trac.query("ticket.milestone.create", release.name, {
+ "completed" => rel,
+ "description" => desc
+ })
+ }
end
end
@@ 401,24 498,36 @@ module Ditz
class Operator
- operation :trac, "Sync with a Trac repository"
- def trac( project, config )
+ operation :trac, "Sync with a Trac repository", :maybe_debug, :maybe_verbose, :maybe_version do
+ opt :debug, "Run in debug mode, for very verbose output", :short => 'd', :default => false
+ opt :verbose, "Run in verbose mode", :short => 'v', :default => false
+ end
+ def trac( project, config, opts, *args )
+ logger = Logger.new(STDERR)
+ logger.level = Logger::WARN
+ logger.level = Logger::DEBUG if opts[:debug]
+ logger.level = Logger::INFO if opts[:verbose]
unless config.trac_sync_url
- STDERR.puts( "Please run 'ditz reconfigure' and set the Trac URL" )
+ logger.error( "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" )
+ logger.error( "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" )
+ logger.error( "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 )
+ util = Ditz::TracUtil.new( project, config, trac, logger )
- tickets = trac.tickets.get_all.values
+ tickets = trac.query("ticket.query", "status=new&max=1000000&order=id")
+ logger.info("Fetching #{tickets.size} tickets...")
+ tickets.collect! {|id|
+ STDOUT.print(".") ; STDOUT.flush
+ trac.tickets.get(id)
+ }
changelogs = []
# TODO this is really inefficient. No need to re-update things that have
@@ 427,16 536,27 @@ module Ditz
# Existing [ticket,issue] matches
pairs = util.pair(tickets)
+ logger.debug( "All pairs: " )
+ logger.debug( pairs.collect {|t,i| [ t ? t.id : nil, i ? i.id[0,4] : nil ]}.inspect )
# Create and update any missing issues. new_pairs is {ticket=>new_issue} hash
- issues_only = pairs.find_all {|m| m[1] == nil}
- new_issues = util.create_issues( issues_only )
- issues_only.each { |m| m[1] = new_issues[m[0]] }
+ only_tickets = pairs.find_all {|m| m[1] == nil}
+ new_issues = util.create_issues( only_tickets )
+ pairs.each { |m| m[1] = new_issues[m[0]] unless m[1] }
# Create and update any missing tickets. new_tickets is {issue=>new_ticket} hash
- tickets_only = pairs.find_all {|m| m[0] == nil}
- new_tickets = util.create_tickets( tickets_only )
- tickets_only.each { |m| m[0] = new_tickets[m[1]] }
+ only_issues = pairs.find_all {|m| m[0] == nil}
+ new_tickets = util.create_tickets( only_issues )
+ pairs.each { |m| m[0] = new_tickets[m[1]] unless m[0] }
+
+ # We can't create issues for releases that are already released (ditz
+ # limitation), so ignore those.
+ pairs.reject! { |t,i| i.nil? }
+ pairs.each do |t,i|
+ if t.nil?
+ logger.error("Failed to create a ticket for issue #{i.id[0,4]}, somehow.")
+ end
+ end
# Now, update the issues and tickets with any changes that have occurred since
# the last change on the partner. We don't need to update objects which caused a
@@ 444,10 564,15 @@ module Ditz
# the ticket.
#
# Don't update issues that just created a new ticket
- issues_to_update = pairs.reject {|t,i| !new_tickets[i].nil? }
- util.update_issues( issues_to_update )
+ issues_to_update = pairs.reject {|t,i| new_tickets[i] }
+ logger.debug("Issues to update:")
+ logger.debug(issues_to_update.collect{|t,i| i.id[0,4] }.inspect)
+ #util.update_issues( issues_to_update )
+
# Don't update tickets that just created a new issue
- tickets_to_update = pairs.reject {|t,i| !new_issues[t].nil? }
+ tickets_to_update = pairs.reject {|t,i| new_issues[t] }
+ logger.debug("Tickets to update:")
+ logger.debug(tickets_to_update.collect{|t,i| t.id }.inspect)
util.update_tickets( tickets_to_update )
end
end