@@ 15,6 15,7 @@ require 'ditz'
module Ditz
+
class Issue
field :trac_id, :ask => false
@@ 25,6 26,9 @@ module Ditz
end
+
+
+
class ScreenView
add_to_view :issue_summary do |issue, config|
" Trac ID: #{issue.trac_id || 'none'}\n"
@@ 35,20 39,23 @@ module Ditz
end
+
+
+
class HtmlView
add_to_view :issue_summary do |issue, config|
next unless issue.trac_id
- [<<EOS, { :issue => issue }]
+ [<<-EOS, { :issue => issue }]
<tr>
<td class='attrname'>Trac ID:</td>
<td class='attrval'><%= issue.trac_id %></td>
</tr>
-EOS
+ EOS
end
add_to_view :issue_details do |issue, config|
next unless issue.trac_id
- [<<EOS, { :issue => issue, :config => config }]
+ [<<-EOS, { :issue => issue, :config => config }]
<h2>Trac Synchronization</h2>
<table>
<tr>
@@ 56,12 63,14 @@ EOS
<td class='attrval'><%= config.trac_sync_url %></td>
</tr>
</table>
-EOS
+ EOS
end
end
+
+
# A utility class for handling Trac tickets and milestones
class TracUtil
# If new types or dispositions are added to either ditz or trac, they'll need
@@ 78,7 87,7 @@ EOS
"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"
@@ 88,7 97,7 @@ EOS
"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"
@@ 101,19 110,13 @@ EOS
end
- def equal?( ticket, issue )
- ticket.summary == issue.title
- end
-
-
# Find matches between tickets and issues. First matches by
# ticket.trac_id, and if that fails, by title
def pair( tickets )
rv = []
issues = @project.issues.clone
for ticket in tickets
- issue = issues.find { |i| ticket.id == i.trac_id }
- issue = issues.find { |i| equal?( ticket, i ) } unless issue
+ 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!!"
@@ 136,59 139,25 @@ EOS
# Syncs ditz issues -> Trac
- def create_tickets( issues, trac )
+ def create_tickets( issues )
rv = {}
- maybe_create_milestones( trac )
- maybe_create_components( trac )
+ maybe_create_milestones
+ maybe_create_components
issues.each do |t,i| # t will always be nil
- tid = trac.tickets.create( i.title, i.desc, {
- "created_at" => i.creation_time,
- "type" => DTYPE_TTYPE[ i.type ],
- "reporter" => i.reporter || "",
- "milestone" => i.release || "",
- "component" => i.component || "",
- "status" => DSTATUS_TSTATUS[ i.status ] || "",
- "resolution" => DISPO_RES[ i.disposition ] || ""
- } )
+ tid = @trac.tickets.create( i.title, i.desc, {
+ "created_at" => i.creation_time,
+ "type" => DTYPE_TTYPE[ i.type ],
+ "reporter" => i.reporter || "",
+ "milestone" => i.release || "",
+ "component" => i.component || "",
+ "status" => DSTATUS_TSTATUS[ i.status ] || "",
+ "resolution" => DISPO_RES[ i.disposition ] || ""
+ } )
i.trac_id = tid
- rv[i] = trac.tickets.get(tid)
+ rv[i] = @trac.tickets.get(tid)
end
rv
end
-
- # Creates Trac components for all of the ditz components that are missing
- # in Trac
- # trac: the trac4r object to create the components in
- def maybe_create_components( trac )
- components = trac.query("ticket.component.getAll")
- @project.components.each do |component|
- next if components.include? component.name
- trac.query("ticket.component.create", component.name, {})
- end
- end
-
- # Creates Trac milestones for all of the ditz releases that are missing in
- # Trac
- # trac: the trac4r object to create the milestones in
- def maybe_create_milestones( trac )
- milestones = trac.query("ticket.milestone.getAll")
- @project.releases.each do |release|
- next if milestones.include? release.name
- desc = rel = ""
- release.log_events.each do |ev|
- case ev[2]
- when "created"
- desc = ev[3]
- when "released"
- rel = ev[0]
- end
- end
- trac.query("ticket.milestone.create", release.name, {
- "completed" => rel,
- "description" => desc
- })
- end
- end
# Syncs Trac tickets -> issues
@@ 227,6 196,120 @@ EOS
end
+ # This doesn't apply the change logs to the tickets like we do with
+ # issues, because the Trac XMLRPC API doesn't let us write log entries
+ # directly. So, we can't log user/timestamp changes. Consequently,
+ # the only things we do here are (a) log comments, and (b) update the
+ # ticket status if it differs
+ def update_tickets( pairs )
+ pairs.each do |ticket, issue|
+ puts "Working on #{ticket.id}/#{issue.id[0,4]}"
+ copy_comments( ticket, issue )
+ issue_last_changed = if issue.log_events.size == 0
+ issue.creation_time
+ else
+ issue.log_events[-1][0]
+ end
+ if issue_last_changed > ticket.updated_at.to_time
+ attrs = {}
+ attrs["summary"] = issue.title if issue.title != ticket.summary
+ attrs["description"] = issue.desc if issue.desc != ticket.description
+ attrs["milestone"] = issue.release if issue.release != ticket.milestone
+ if DSTATUS_TSTATUS[issue.status] != ticket.status
+ attrs["status"] = DSTATUS_TSTATUS[issue.status]
+ end
+ if DISPO_RES[issue.disposition] != ticket.status
+ attrs["resolution"] = DISPO_RES[issue.disposition] || ""
+ end
+ if issue.component != ticket.component
+ attrs["component"] = issue.component
+ end
+ 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 )
+ end
+ end
+ end
+
+
+ def update_issues( pairs )
+ pairs.each do |ticket, issue|
+ puts "Working on #{ticket.id}/#{issue.id[0,4]}"
+ copy_comments( issue, ticket )
+ issue_last_changed = if issue.log_events.size == 0
+ issue.creation_time
+ else
+ issue.log_events[-1][0]
+ end
+ if issue_last_changed < ticket.updated_at.to_time
+ attrs = {}
+ attrs["summary"] = issue.title if issue.title != ticket.summary
+ attrs["description"] = issue.desc if issue.desc != ticket.description
+ attrs["milestone"] = issue.release if issue.release != ticket.milestone
+ if DSTATUS_TSTATUS[issue.status] != ticket.status
+ attrs["status"] = DSTATUS_TSTATUS[issue.status]
+ end
+ if DISPO_RES[issue.disposition] != ticket.status
+ attrs["resolution"] = DISPO_RES[issue.disposition]
+ end
+ if issue.component != ticket.component
+ attrs["component"] = issue.component
+ end
+ if DTYPE_TTYPE[issue.type] != ticket.type
+ attrs["type"] = DTYPE_TTYPE[issue.type]
+ end
+ end
+ end
+ end
+
+ ############################################################################
+ # Utility methods used internal to the class
+ private
+
+ def copy_comments( a, b )
+ if a.instance_of? Issue
+ issue, ticket = a, b
+ else
+ ticket, issue = a, b
+ end
+
+ 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] }
+ if m
+ el_comments.delete(m)
+ true
+ else
+ 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] )
+ 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] )
+ end
+ end
+ end
+
+ def equal?( ticket, issue )
+ ticket.id == issue.id || ticket.summary == issue.title
+ end
+
def change_status(status, issue)
if issue.status != status
old_status = issue.status
@@ 239,23 322,23 @@ EOS
# Creates a ditz release, IFF it doesn't exist
# milestone: the String name of the release to create
def maybe_create_release( milestone )
- release = @project.releases.find { |r| r.name == milestone }
- unless release
- puts "Creating release #{milestone}"
- release = Ditz::Release.create({:name=>milestone}, [@config, @project])
- @project.add_release(release)
- end
+ release = @project.releases.find { |r| r.name == milestone }
+ unless release
+ puts "Creating release #{milestone}"
+ release = Ditz::Release.create({:name=>milestone}, [@config, @project])
+ @project.add_release(release)
+ end
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 )
- component = @project.components.find { |r| r.name == comp }
- unless component
- puts "Creating component #{comp}"
- component = Ditz::Component.create({:name=>comp}, [@config, @project])
- @project.add_component(component)
- end
+ component = @project.components.find { |r| r.name == comp }
+ unless component
+ puts "Creating component #{comp}"
+ component = Ditz::Component.create({:name=>comp}, [@config, @project])
+ @project.add_component(component)
+ end
end
def group_by_time(changelog)
@@ 268,106 351,45 @@ EOS
rv.sort
end
-
- # TODO this need beaucoup more error checking
- def update_tickets( pairs, trac )
- pairs.each do |ticket, issue|
- puts "Working on #{ticket.id}/#{issue.id[0,4]}"
- ticket_last_updated = ticket.updated_at.to_time
- issue.log_events.each do |event|
- if event[0] > ticket_last_updated
- attrs = {}
- case event[2]
- when "created"
- # This should have been done by create_tickets
- # How did we get here, anyway?
- STDERR.puts( "This is strange. Sean could have sworn that you'd never get here." )
- STDERR.puts( "It indicates that there's something wrong with the time stamps." )
- raise "Suspicious 'create' event encountered, which could mess up the ticket, so aborting."
- when /^assigned to release (.*?) from .*/
- attrs[ "milestone" ] = $1
- when /^changed status from \w to (.*)$/
- attrs[ "milestone" ] = newstat
- when "commented"
- # NOOP. The comment is event[3]
- when /^closed with disposition (.*)$/
- attrs[ "resolution" ] = DISPO_RES[ issue.disposition ]
- attrs[ "status" ] = DSTATUS_TSTATUS[ issue.status ]
- else
- raise "Found unknown event #{event[2]}! Aborting. Needs code fix."
- end
- puts "Updating ticket #{ticket.id} with #{attrs.inspect} and comment #{event[3].inspect}"
- trac.query( "ticket.update", ticket.id, event[3], attrs )
- end
- end
+ # Creates Trac components for all of the ditz components that are missing
+ # in Trac
+ # trac: the trac4r object to create the components in
+ def maybe_create_components
+ components = @trac.query("ticket.component.getAll")
+ @project.components.each do |component|
+ next if components.include? component.name
+ @trac.query("ticket.component.create", component.name, {})
end
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[4] != issue.description
- whats << "edited description"
- issue.desc = new_change[4]
- end
- when "milestone"
- if new_change[4] != issue.release
- whats << "assigned to release #{new_change[4]} from #{issue.release || 'unassigned'}"
- maybe_create_release( new_change[4] )
- issue.release = new_change[4]
- end
- when "owner"
- whats << change_status( :in_progress, issue )
- when "resolution"
- issue.disposition = RES_DISPO[ new_change[4] ]
- when "status"
- whats << change_status( TSTATUS_DSTATUS[ new_change[4] ], issue )
- when "type"
- new_t = TTYPE_DTYPE[ new_change[4] ]
- whats << "type changed to #{new_t} from #{issue.type}"
- issue.type = new_t
- when "component"
- new_c = new_change[4]
- whats << "component changed to #{new_t} from #{issue.component}"
- maybe_create_component( new_change[4] )
- issue.component = new_t
-
- when "attachment", "cc", "os", "priority", "severity", "version"
- # NOOP
- else
- # NOOP
- end
- end
- # All changes were at the same time, and therefore by the same person
- whats.each do |what|
- issue.log_at( time, what, array_of_changes[0][1], comment )
- end
+ # Creates Trac milestones for all of the ditz releases that are missing in
+ # Trac
+ # trac: the trac4r object to create the milestones in
+ def maybe_create_milestones
+ milestones = @trac.query("ticket.milestone.getAll")
+ @project.releases.each do |release|
+ next if milestones.include? release.name
+ desc = rel = ""
+ release.log_events.each do |ev|
+ case ev[2]
+ when "created"
+ desc = ev[3]
+ when "released"
+ rel = ev[0]
end
end
+ @trac.query("ticket.milestone.create", release.name, {
+ "completed" => rel,
+ "description" => desc
+ })
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"
@@ 376,6 398,8 @@ EOS
+
+
class Operator
operation :trac, "Sync with a Trac repository"
def trac( project, config )
@@ 411,7 435,7 @@ EOS
# 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, trac )
+ new_tickets = util.create_tickets( tickets_only )
tickets_only.each { |m| m[0] = new_tickets[m[1]] }
# Now, update the issues and tickets with any changes that have occurred since
@@ 421,48 445,10 @@ EOS
#
# 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, trac )
+ 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? }
- util.update_tickets( tickets_to_update, trac )
+ util.update_tickets( tickets_to_update )
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