Fixes 8a82d (ticket #13), circular dependencies.

There is no good solution that doesn't ultimately involve merging the history
manually.  Even with time stamps, there could be conflicts. So, comments
are preserved in both Trac and ditz, and history is preserved on the ditz side.
Unless we get the ability to write log comments directly in Trac (as we can do
in ditz), then history can't be preserved.
M .ditz/issue-346223318c35bdac90b33437bc0d266e05475d9f.yaml +1 -1
@@ 16,4 16,4 @@ log_events:
   - Sean Russell <ser@ser1.net>
   - created
   - ""
-trac_id: 
+trac_id: 12

          
M .ditz/issue-837b4acc500d7835adfecfc75b948d57b6e2337e.yaml +5 -1
@@ 3,7 3,7 @@ title: Add per-project configuration
 desc: ""
 type: :bugfix
 component: ditz-trac
-release: 
+release: "2010.1"
 reporter: Sean Russell <ser@ser1.net>
 status: :closed
 disposition: :wontfix

          
@@ 22,4 22,8 @@ log_events:
   - |-
     Duh.  Ditz already supports this -- put the .ditz-config in the project
     directory, and it overrides the global one.
+- - 2010-01-28 18:33:44.704277 Z
+  - Sean Russell <ser@ser1.net>
+  - assigned to release 2010.1 from unassigned
+  - ""
 trac_id: 6

          
M .ditz/issue-8a82d727a91a667548b847a1e22d2ceb42f3709d.yaml +14 -3
@@ 9,8 9,8 @@ type: :bugfix
 component: ditz-trac
 release: "2010.1"
 reporter: Sean Russell <ser@ser1.net>
-status: :unstarted
-disposition: 
+status: :closed
+disposition: :fixed
 creation_time: 2010-01-28 03:21:04.750402 Z
 references: []
 

          
@@ 20,4 20,15 @@ log_events:
   - Sean Russell <ser@ser1.net>
   - created
   - ""
-trac_id: 
+- - 2010-01-28 18:37:40.140358 Z
+  - Sean Russell <ser@ser1.net>
+  - changed status from unstarted to in_progress
+  - ""
+- - 2010-01-29 03:11:20.274137 Z
+  - Sean Russell <ser@ser1.net>
+  - closed with disposition fixed
+  - |-
+    Until I get the ability to make change logs directly, they're not
+    supported.  ditz-trac will copy comments, but it loses the event
+    history when copying to Trac.  Trac to ditz history .is preserved.
+trac_id: 13

          
A => .ditz/issue-8c9c852850fc96f084dcec9204b31431deca5abe.yaml +26 -0
@@ 0,0 1,26 @@ 
+--- !ditz.rubyforge.org,2008-03-06/issue 
+title: Syncing is inefficient
+desc: |-
+  There's no need to re-update things that have already been updated
+  and I'm re-pairing too much.  The create methods should just pair
+  on creation, and return the pairs.
+type: :bugfix
+component: ditz-trac
+release: "2010.1"
+reporter: Sean Russell <ser@ser1.net>
+status: :closed
+disposition: :fixed
+creation_time: 2010-01-27 13:33:06.649023 Z
+references: []
+
+id: 8c9c852850fc96f084dcec9204b31431deca5abe
+log_events: 
+- - 2010-01-27 13:33:06.649673 Z
+  - Sean Russell <ser@ser1.net>
+  - created
+  - ""
+- - 2010-01-28 18:29:15.892404 Z
+  - Sean Russell <ser@ser1.net>
+  - closed with disposition fixed
+  - Fixed by r2c584fd394e
+trac_id: 9

          
M .ditz/issue-9618638bbe9059ffa431f5edc8d7b9c9a938dbab.yaml +5 -1
@@ 3,7 3,7 @@ title: Add support in ditz for component
 desc: ""
 type: :feature
 component: trac to ditz
-release: 
+release: "2010.1"
 reporter: Sean Russell <ser@ser1.net>
 status: :closed
 disposition: :wontfix

          
@@ 20,4 20,8 @@ log_events:
   - Sean Russell <ser@ser1.net>
   - closed with disposition wontfix
   - No need for this.
+- - 2010-01-28 18:34:40.368414 Z
+  - Sean Russell <ser@ser1.net>
+  - assigned to release 2010.1 from unassigned
+  - ""
 trac_id: 7

          
A => .ditz/issue-c293f6db629b1b62aa4ea6e4706525f5a045eb4f.yaml +25 -0
@@ 0,0 1,25 @@ 
+--- !ditz.rubyforge.org,2008-03-06/issue 
+title: Extract milestone and component creation
+desc: |-
+  It shouldn't do this during ticket creation time; it should
+  do it prior to syncing the tickets.
+type: :bugfix
+component: ditz-trac
+release: "2010.1"
+reporter: Sean Russell <ser@ser1.net>
+status: :closed
+disposition: :fixed
+creation_time: 2010-01-27 13:32:04.453013 Z
+references: []
+
+id: c293f6db629b1b62aa4ea6e4706525f5a045eb4f
+log_events: 
+- - 2010-01-27 13:32:04.453661 Z
+  - Sean Russell <ser@ser1.net>
+  - created
+  - ""
+- - 2010-01-28 18:31:17.064377 Z
+  - Sean Russell <ser@ser1.net>
+  - closed with disposition fixed
+  - Fixed by 69a95f48069e
+trac_id: 11

          
A => .ditz/issue-e9972f258450d6aca85514f2a8d91b3240b66a41.yaml +19 -0
@@ 0,0 1,19 @@ 
+--- !ditz.rubyforge.org,2008-03-06/issue 
+title: In pair(), do soemthing useful with exceptions
+desc: pair() is pretty dumb about error cases.
+type: :bugfix
+component: ditz-trac
+release: "2010.1"
+reporter: Sean Russell <ser@ser1.net>
+status: :unstarted
+disposition: 
+creation_time: 2010-01-27 13:34:08.089011 Z
+references: []
+
+id: e9972f258450d6aca85514f2a8d91b3240b66a41
+log_events: 
+- - 2010-01-27 13:34:08.089654 Z
+  - Sean Russell <ser@ser1.net>
+  - created
+  - ""
+trac_id: 10

          
M trac-sync.rb +189 -203
@@ 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