]> source.dussan.org Git - redmine.git/commitdiff
Merged Git support branch (r1200 to r1226).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Wed, 12 Mar 2008 20:28:49 +0000 (20:28 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Wed, 12 Mar 2008 20:28:49 +0000 (20:28 +0000)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1236 e93f8b46-1217-0410-a6f0-8f06a7374b81

28 files changed:
app/controllers/repositories_controller.rb
app/helpers/application_helper.rb
app/helpers/repositories_helper.rb
app/models/changeset.rb
app/models/repository.rb
app/models/repository/bazaar.rb
app/models/repository/cvs.rb
app/models/repository/darcs.rb
app/models/repository/git.rb [new file with mode: 0644]
app/models/repository/subversion.rb
app/views/repositories/_dir_list_content.rhtml
app/views/repositories/_revisions.rhtml
app/views/repositories/annotate.rhtml
app/views/repositories/diff.rhtml
app/views/repositories/revision.rhtml
db/migrate/091_change_changesets_revision_to_string.rb [new file with mode: 0644]
db/migrate/092_change_changes_from_revision_to_string.rb [new file with mode: 0644]
doc/RUNNING_TESTS
lib/redmine.rb
lib/redmine/scm/adapters/darcs_adapter.rb
lib/redmine/scm/adapters/git_adapter.rb [new file with mode: 0644]
test/fixtures/repositories/darcs_repository.tar.gz [new file with mode: 0644]
test/fixtures/repositories/git_repository.tar.gz [new file with mode: 0644]
test/functional/repositories_darcs_controller_test.rb [new file with mode: 0644]
test/functional/repositories_git_controller_test.rb [new file with mode: 0644]
test/unit/repository_cvs_test.rb
test/unit/repository_darcs_test.rb [new file with mode: 0644]
test/unit/repository_git_test.rb [new file with mode: 0644]

index 13d3eaa321c9070c250084494b42c3e2e0a5e051..bce5f66a9aab1574d2519557a1bb06ecb5ed5c43 100644 (file)
@@ -134,7 +134,7 @@ class RepositoriesController < ApplicationController
   end
   
   def diff
-    @rev_to = params[:rev_to] ? params[:rev_to].to_i : (@rev - 1)
+    @rev_to = params[:rev_to]
     @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
     @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
     
@@ -185,7 +185,7 @@ private
     render_404 and return false unless @repository
     @path = params[:path].join('/') unless params[:path].nil?
     @path ||= ''
-    @rev = params[:rev].to_i if params[:rev]
+    @rev = params[:rev]
   rescue ActiveRecord::RecordNotFound
     render_404
   end
index f21b43a231b64f0ba780400db018df57df515c30..be0b808d2c0c6697b64565a6c7892b056e8a6913 100644 (file)
@@ -270,6 +270,7 @@ module ApplicationHelper
     #     #52 -> Link to issue #52
     #   Changesets:
     #     r52 -> Link to revision 52
+    #     commit:a85130f -> Link to scmid starting with a85130f
     #   Documents:
     #     document#17 -> Link to document with id 17
     #     document:Greetings -> Link to the document with title "Greetings"
@@ -280,7 +281,7 @@ module ApplicationHelper
     #     version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
     #   Attachments:
     #     attachment:file.zip -> Link to the attachment of the current object named file.zip
-    text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version)?((#|r)(\d+)|(:)([^"][^\s<>]+|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
+    text = text.gsub(%r{([\s\(,-^])(!)?(attachment|document|version|commit)?((#|r)(\d+)|(:)([^"][^\s<>]+|"[^"]+"))(?=[[:punct:]]|\s|<|$)}) do |m|
       leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
       link = nil
       if esc.nil?
@@ -325,6 +326,10 @@ module ApplicationHelper
               link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                               :class => 'version'
             end
+          when 'commit'
+            if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
+              link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project.id, :rev => changeset.revision}, :class => 'changeset', :title => truncate(changeset.comments, 100)
+            end
           when 'attachment'
             if attachments && attachment = attachments.detect {|a| a.filename == name }
               link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
index d7d7f43499a34231c10a9bf3a07072f40f4088c0..31daf1bd86a1d5ba4cd56ba00bed81f38b3920fc 100644 (file)
@@ -25,6 +25,10 @@ module RepositoriesHelper
     type ? CodeRay.scan(content, type).html : h(content)
   end
   
+  def format_revision(txt)
+    txt.to_s[0,8]
+  end
+  
   def to_utf8(str)
     return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
     @encodings ||= Setting.repositories_encodings.split(',').collect(&:strip)
@@ -76,6 +80,10 @@ module RepositoriesHelper
       content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
   end
 
+  def git_field_tags(form, repository)
+      content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?)))
+  end
+
   def cvs_field_tags(form, repository)
       content_tag('p', form.text_field(:root_url, :label => 'CVSROOT', :size => 60, :required => true, :disabled => !repository.new_record?)) +
       content_tag('p', form.text_field(:url, :label => 'Module', :size => 30, :required => true, :disabled => !repository.new_record?))
index dbe06935dc8126d4533407001e4e903a0b7007e8..ce9ea28ca2bfea4d6f0b28c0e1e450fa3076939f 100644 (file)
@@ -32,7 +32,6 @@ class Changeset < ActiveRecord::Base
                      :date_column => 'committed_on'
   
   validates_presence_of :repository_id, :revision, :committed_on, :commit_date
-  validates_numericality_of :revision, :only_integer => true
   validates_uniqueness_of :revision, :scope => :repository_id
   validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
   
@@ -89,7 +88,11 @@ class Changeset < ActiveRecord::Base
           # don't change the status is the issue is closed
           next if issue.status.is_closed?
           user = committer_user || User.anonymous
-          journal = issue.init_journal(user, l(:text_status_changed_by_changeset, "r#{self.revision}"))
+          csettext = "r#{self.revision}"
+          if self.scmid && (! (csettext =~ /^r[0-9]+$/))
+            csettext = "commit:\"#{self.scmid}\""
+          end
+          journal = issue.init_journal(user, l(:text_status_changed_by_changeset, csettext))
           issue.status = fix_status
           issue.done_ratio = done_ratio if done_ratio
           issue.save
@@ -114,11 +117,11 @@ class Changeset < ActiveRecord::Base
   
   # Returns the previous changeset
   def previous
-    @previous ||= Changeset.find(:first, :conditions => ['revision < ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision DESC')
+    @previous ||= Changeset.find(:first, :conditions => ['id < ? AND repository_id = ?', self.id, self.repository_id], :order => 'id DESC')
   end
 
   # Returns the next changeset
   def next
-    @next ||= Changeset.find(:first, :conditions => ['revision > ? AND repository_id = ?', self.revision, self.repository_id], :order => 'revision ASC')
+    @next ||= Changeset.find(:first, :conditions => ['id > ? AND repository_id = ?', self.id, self.repository_id], :order => 'id ASC')
   end
 end
index be31ac2e5b3b10e268f771be5ef0147602c03dd6..229c8dae42bfe01a8b5218dfa7d82ef0a2cf09c7 100644 (file)
@@ -17,7 +17,7 @@
 
 class Repository < ActiveRecord::Base
   belongs_to :project
-  has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.revision DESC"
+  has_many :changesets, :dependent => :destroy, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
   has_many :changes, :through => :changesets
     
   def scm
@@ -51,7 +51,7 @@ class Repository < ActiveRecord::Base
     path = "/#{path}" unless path.starts_with?('/')
     Change.find(:all, :include => :changeset, 
       :conditions => ["repository_id = ? AND path = ?", id, path],
-      :order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
+      :order => "committed_on DESC, #{Changeset.table_name}.id DESC").collect(&:changeset)
   end
   
   def latest_changeset
index 6e387f9570b1decf9cefaa4ecd2e79398c246aef..1b75066c2c537f88a02e400f25d027590c580d15 100644 (file)
@@ -51,7 +51,7 @@ class Repository::Bazaar < Repository
     scm_info = scm.info
     if scm_info
       # latest revision found in database
-      db_revision = latest_changeset ? latest_changeset.revision : 0
+      db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
       # latest revision in the repository
       scm_revision = scm_info.lastrev.identifier.to_i
       if db_revision < scm_revision
index 16d9063160ede87fe2163b3be9539e50d4874a2d..a78b60806a4e58af40da82a6714032a82ad58274 100644 (file)
@@ -82,9 +82,6 @@ class Repository::Cvs < Repository
   end
   
   def fetch_changesets
-    #not the preferred way with CVS. maybe we should introduce always a cron-job for this
-    last_commit = changesets.maximum(:committed_on)
-    
     # some nifty bits to introduce a commit-id with cvs
     # natively cvs doesn't provide any kind of changesets, there is only a revision per file.
     # we now take a guess using the author, the commitlog and the commit-date.
@@ -94,8 +91,10 @@ class Repository::Cvs < Repository
     # we use a small delta here, to merge all changes belonging to _one_ changeset
     time_delta=10.seconds
     
+    fetch_since = latest_changeset ? latest_changeset.committed_on : nil
     transaction do
-      scm.revisions('', last_commit, nil, :with_paths => true) do |revision|
+      tmp_rev_num = 1
+      scm.revisions('', fetch_since, nil, :with_paths => true) do |revision|
         # only add the change to the database, if it doen't exists. the cvs log
         # is not exclusive at all. 
         unless changes.find_by_path_and_revision(scm.with_leading_slash(revision.paths[0][:path]), revision.paths[0][:revision])
@@ -107,18 +106,16 @@ class Repository::Cvs < Repository
           })
         
           # create a new changeset.... 
-          unless cs 
-            # we use a negative changeset-number here (just for inserting)
+          unless cs
+            # we use a temporaray revision number here (just for inserting)
             # later on, we calculate a continous positive number
-            next_rev = changesets.minimum(:revision)            
-            next_rev = 0 if next_rev.nil? or next_rev > 0 
-            next_rev = next_rev - 1
-            
-            cs=Changeset.create(:repository => self,
-            :revision => next_rev, 
-            :committer => revision.author, 
-            :committed_on => revision.time,
-            :comments => revision.message)
+            latest = changesets.find(:first, :order => 'id DESC')
+            cs = Changeset.create(:repository => self,
+                                  :revision => "_#{tmp_rev_num}", 
+                                  :committer => revision.author, 
+                                  :committed_on => revision.time,
+                                  :comments => revision.message)
+            tmp_rev_num += 1
           end
         
           #convert CVS-File-States to internal Action-abbrevations
@@ -139,12 +136,13 @@ class Repository::Cvs < Repository
         end
       end
       
-      next_rev = [changesets.maximum(:revision) || 0, 0].max
-      changesets.find(:all, :conditions=>["revision < 0"], :order=>"committed_on ASC").each() do |changeset|
-        next_rev = next_rev + 1
-        changeset.revision = next_rev
-        changeset.save!
+      # Renumber new changesets in chronological order
+      c = changesets.find(:first, :order => 'committed_on DESC, id DESC', :conditions => "revision NOT LIKE '_%'")
+      next_rev = c.nil? ? 1 : (c.revision.to_i + 1)
+      changesets.find(:all, :order => 'committed_on ASC, id ASC', :conditions => "revision LIKE '_%'").each do |changeset|
+        changeset.update_attribute :revision, next_rev
+        next_rev += 1
       end
-    end
+    end # transaction
   end
 end
index 48cc246fb3f816b1175aa7c7b285b0f8c925ca80..cc608d3701e6c24ccd0a845c77a48909565ba434 100644 (file)
@@ -47,18 +47,19 @@ class Repository::Darcs < Repository
   
   def diff(path, rev, rev_to, type)
     patch_from = changesets.find_by_revision(rev)
+    return nil if patch_from.nil?
     patch_to = changesets.find_by_revision(rev_to) if rev_to
     if path.blank?
       path = patch_from.changes.collect{|change| change.path}.join(' ')
     end
-    scm.diff(path, patch_from.scmid, patch_to.scmid, type)
+    patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil, type) : nil
   end
   
   def fetch_changesets
     scm_info = scm.info
     if scm_info
       db_last_id = latest_changeset ? latest_changeset.scmid : nil
-      next_rev = latest_changeset ? latest_changeset.revision + 1 : 1      
+      next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1      
       # latest revision in the repository
       scm_revision = scm_info.lastrev.scmid      
       unless changesets.find_by_scmid(scm_revision)
@@ -71,9 +72,7 @@ class Repository::Darcs < Repository
                                          :committer => revision.author, 
                                          :committed_on => revision.time,
                                          :comments => revision.message)
-            
-            next if changeset.new_record?
-            
+                                         
             revision.paths.each do |change|
               Change.create(:changeset => changeset,
                             :action => change[:action],
diff --git a/app/models/repository/git.rb b/app/models/repository/git.rb
new file mode 100644 (file)
index 0000000..7213588
--- /dev/null
@@ -0,0 +1,70 @@
+# redMine - project management software
+# Copyright (C) 2006-2007  Jean-Philippe Lang
+# Copyright (C) 2007  Patrick Aljord patcito@Å‹mail.com
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require 'redmine/scm/adapters/git_adapter'
+
+class Repository::Git < Repository
+  attr_protected :root_url
+  validates_presence_of :url
+
+  def scm_adapter
+    Redmine::Scm::Adapters::GitAdapter
+  end
+  
+  def self.scm_name
+    'Git'
+  end
+
+  def changesets_for_path(path)
+    Change.find(:all, :include => :changeset, 
+                :conditions => ["repository_id = ? AND path = ?", id, path],
+                :order => "committed_on DESC, #{Changeset.table_name}.revision DESC").collect(&:changeset)
+  end
+
+  def fetch_changesets
+    scm_info = scm.info
+    if scm_info
+      # latest revision found in database
+      db_revision = latest_changeset ? latest_changeset.revision : nil
+      # latest revision in the repository
+      scm_revision = scm_info.lastrev.scmid
+
+      unless changesets.find_by_scmid(scm_revision)
+
+        revisions = scm.revisions('', db_revision, nil)
+        transaction do
+          revisions.reverse_each do |revision|
+            changeset = Changeset.create(:repository => self,
+                                         :revision => revision.identifier,
+                                         :scmid => revision.scmid,
+                                         :committer => revision.author, 
+                                         :committed_on => revision.time,
+                                         :comments => revision.message)
+            
+            revision.paths.each do |change|
+              Change.create(:changeset => changeset,
+                            :action => change[:action],
+                            :path => change[:path],
+                            :from_path => change[:from_path],
+                            :from_revision => change[:from_revision])
+            end
+          end
+        end
+      end
+    end
+  end
+end
index a0485608d867b3c537df872bc20339f4ee688672..0c2239c433d7fd91efd9773a27e4c1e0e7d93452 100644 (file)
@@ -39,7 +39,7 @@ class Repository::Subversion < Repository
     scm_info = scm.info
     if scm_info
       # latest revision found in database
-      db_revision = latest_changeset ? latest_changeset.revision : 0
+      db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
       # latest revision in the repository
       scm_revision = scm_info.lastrev.identifier.to_i
       if db_revision < scm_revision
index a7b83e817cfcde0a58b07beb0ef3af2c574778af..3564e52abfb85621c93b349aba1ad2f9954c1abf 100644 (file)
@@ -23,7 +23,7 @@ else
 end %>
 </td>
 <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td>
-<td class="revision"><%= link_to(entry.lastrev.name, :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
+<td class="revision"><%= link_to(format_revision(entry.lastrev.name), :action => 'revision', :id => @project, :rev => entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %></td>
 <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td>
 <td class="author"><%=h(entry.lastrev.author.to_s.split('<').first) if entry.lastrev %></td>
 <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev %>
index 52992bb89c0f537db11ff4b7e576dafd9b25417c..1bcf0208cbd65f2901e0f6765e089675cd5ceb08 100644 (file)
@@ -13,7 +13,7 @@
 <% line_num = 1 %>
 <% revisions.each do |changeset| %>
 <tr class="changeset <%= cycle 'odd', 'even' %>">
-<td class="id"><%= link_to changeset.revision, :action => 'revision', :id => project, :rev => changeset.revision %></td>
+<td class="id"><%= link_to format_revision(changeset.revision), :action => 'revision', :id => project, :rev => changeset.revision %></td>
 <td class="checkbox"><%= radio_button_tag('rev', changeset.revision, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('cbto-#{line_num+1}').checked=true;") if show_diff && (line_num < revisions.size) %></td>
 <td class="checkbox"><%= radio_button_tag('rev_to', changeset.revision, (line_num==2), :id => "cbto-#{line_num}", :onclick => "if ($('cb-#{line_num}').checked==true) {$('cb-#{line_num-1}').checked=true;}") if show_diff && (line_num > 1) %></td>
 <td class="committed_on"><%= format_time(changeset.committed_on) %></td>
index 28d99a39346f30b9c40bb695c4a7ec611193ea3c..b5669ef76b315c1809e8b1c84d24c0b8853824e5 100644 (file)
@@ -11,7 +11,7 @@
     <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
       <th class="line-num"><%= line_num %></th>
       <td class="revision">
-      <%= (revision.identifier ? link_to(revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier) : revision.revision) if revision %></td>
+      <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td>
       <td class="author"><%= h(revision.author.to_s.split('<').first) if revision %></td>
       <td class="line-code"><pre><%= line %></pre></td>
     </tr>
index 88c5f17a03dc3d876724010ac17d4aa149bcebbc..eaef1abf54dd0ce9a0c8e2c93c7c5c80bca6783b 100644 (file)
@@ -1,4 +1,4 @@
-<h2><%= l(:label_revision) %> <%= @rev %>: <%= @path.gsub(/^.*\//, '') %></h2>
+<h2><%= l(:label_revision) %> <%= format_revision(@rev) %> <%= @path.gsub(/^.*\//, '') %></h2>
 
 <!-- Choose view type -->
 <% form_tag({ :controller => 'repositories', :action => 'diff'}, :method => 'get') do %>
@@ -23,8 +23,8 @@
           </th>
         </tr>
         <tr>
-          <th colspan="2">@<%= @rev %></th>
-          <th colspan="2">@<%= @rev_to %></th>
+          <th colspan="2">@<%= format_revision @rev %></th>
+          <th colspan="2">@<%= format_revision @rev_to %></th>
         </tr>
       </thead>
       <tbody>
@@ -56,8 +56,8 @@
           </th>
         </tr>
         <tr>
-          <th>@<%= @rev %></th>
-          <th>@<%= @rev_to %></th>
+          <th>@<%= format_revision @rev %></th>
+          <th>@<%= format_revision @rev_to %></th>
           <th></th>
         </tr>
       </thead>
index d60c0b0b75acdbd82eb364906c3a547d43b9fd09..5a7ef1fd534552b8e5c9d38b7ba9bf0f5642f4d1 100644 (file)
@@ -19,7 +19,7 @@
   <% end %>
 </div>
 
-<h2><%= l(:label_revision) %> <%= @changeset.revision %></h2>
+<h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2>
 
 <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %>
 <em><%= @changeset.committer.to_s.split('<').first %>, <%= format_time(@changeset.committed_on) %></em></p>
diff --git a/db/migrate/091_change_changesets_revision_to_string.rb b/db/migrate/091_change_changesets_revision_to_string.rb
new file mode 100644 (file)
index 0000000..e621a39
--- /dev/null
@@ -0,0 +1,9 @@
+class ChangeChangesetsRevisionToString < ActiveRecord::Migration
+  def self.up
+    change_column :changesets, :revision, :string, :null => false
+  end
+
+  def self.down
+    change_column :changesets, :revision, :integer, :null => false
+  end
+end
diff --git a/db/migrate/092_change_changes_from_revision_to_string.rb b/db/migrate/092_change_changes_from_revision_to_string.rb
new file mode 100644 (file)
index 0000000..b298a3f
--- /dev/null
@@ -0,0 +1,9 @@
+class ChangeChangesFromRevisionToString < ActiveRecord::Migration
+  def self.up
+    change_column :changes, :from_revision, :string
+  end
+
+  def self.down
+    change_column :changes, :from_revision, :integer
+  end
+end
index fde24413bd19e4a42bf5fd35c3b11e7e5dfea4fa..7a5e2b9929986089235097277e8691212896d72f 100644 (file)
@@ -19,3 +19,19 @@ gunzip < test/fixtures/repositories/bazaar_repository.tar.gz | tar -xv -C tmp/te
 Mercurial
 ---------
 gunzip < test/fixtures/repositories/mercurial_repository.tar.gz | tar -xv -C tmp/test
+
+Git
+---
+gunzip < test/fixtures/repositories/git_repository.tar.gz | tar -xv -C tmp/test
+
+
+Running Tests
+=============
+
+Run 
+
+  rake --tasks | grep test
+
+to see available tests.
+
+RAILS_ENV=test rake test will run tests.
index e76d77e9e2340746fba939a1b82bee0d9667973c..4c5cbdaae7ce3750000208bde5f0b7d396080328 100644 (file)
@@ -10,7 +10,7 @@ rescue LoadError
   # RMagick is not available
 end
 
-REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar )
+REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git )
 
 # Permissions
 Redmine::AccessControl.map do |map|
index 2955b26dc652d3e1f0799e2cf78311f56e303b0f..cd8610121629ddabdd285265048327238da5954c 100644 (file)
@@ -102,8 +102,12 @@ module Redmine
         def diff(path, identifier_from, identifier_to=nil, type="inline")
           path = '*' if path.blank?
           cmd = "#{DARCS_BIN} diff --repodir #{@url}"
-          cmd << " --to-match \"hash #{identifier_from}\""
-          cmd << " --from-match \"hash #{identifier_to}\"" if identifier_to
+          if identifier_to.nil?
+            cmd << " --match \"hash #{identifier_from}\""
+          else
+            cmd << " --to-match \"hash #{identifier_from}\""
+            cmd << " --from-match \"hash #{identifier_to}\""
+          end
           cmd << " -u #{path}"
           diff = []
           shellout(cmd) do |io|
diff --git a/lib/redmine/scm/adapters/git_adapter.rb b/lib/redmine/scm/adapters/git_adapter.rb
new file mode 100644 (file)
index 0000000..b6b1b85
--- /dev/null
@@ -0,0 +1,261 @@
+# redMine - project management software
+# Copyright (C) 2006-2007  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require 'redmine/scm/adapters/abstract_adapter'
+
+module Redmine
+  module Scm
+    module Adapters    
+      class GitAdapter < AbstractAdapter
+        
+        # Git executable name
+        GIT_BIN = "git"
+
+        # Get the revision of a particuliar file
+        def get_rev (rev,path)
+          cmd="git --git-dir #{target('')} show #{shell_quote rev} -- #{shell_quote path}" if rev!='latest' and (! rev.nil?)
+          cmd="git --git-dir #{target('')} log -1 master -- #{shell_quote path}" if 
+            rev=='latest' or rev.nil?
+          rev=[]
+          i=0
+          shellout(cmd) do |io|
+            files=[]
+            changeset = {}
+            parsing_descr = 0  #0: not parsing desc or files, 1: parsing desc, 2: parsing files
+
+            io.each_line do |line|
+              if line =~ /^commit ([0-9a-f]{40})$/
+                key = "commit"
+                value = $1
+                if (parsing_descr == 1 || parsing_descr == 2)
+                  parsing_descr = 0
+                  rev = Revision.new({:identifier => changeset[:commit],
+                                      :scmid => changeset[:commit],
+                                      :author => changeset[:author],
+                                      :time => Time.parse(changeset[:date]),
+                                      :message => changeset[:description],
+                                      :paths => files
+                                     })
+                  changeset = {}
+                  files = []
+                end
+                changeset[:commit] = $1
+              elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
+                key = $1
+                value = $2
+                if key == "Author"
+                  changeset[:author] = value
+                elsif key == "Date"
+                  changeset[:date] = value
+                end
+              elsif (parsing_descr == 0) && line.chomp.to_s == ""
+                parsing_descr = 1
+                changeset[:description] = ""
+              elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
+                parsing_descr = 2
+                fileaction = $1
+                filepath = $2
+                files << {:action => fileaction, :path => filepath}
+              elsif (parsing_descr == 1) && line.chomp.to_s == ""
+                parsing_descr = 2
+              elsif (parsing_descr == 1)
+                changeset[:description] << line
+              end
+            end        
+            rev = Revision.new({:identifier => changeset[:commit],
+                                :scmid => changeset[:commit],
+                                :author => changeset[:author],
+                                :time => Time.parse(changeset[:date]),
+                                :message => changeset[:description],
+                                :paths => files
+                               })
+
+          end
+
+          get_rev('latest',path) if rev == []
+
+          return nil if $? && $?.exitstatus != 0
+          return rev
+        end
+
+
+        def info
+          root_url = target('')
+          info = Info.new({:root_url => target(''),
+                            :lastrev => revisions(root_url,nil,nil,{:limit => 1}).first
+                          })
+          info
+        rescue Errno::ENOENT => e
+          return nil
+        end
+        
+        def entries(path=nil, identifier=nil)
+          path ||= ''
+          entries = Entries.new
+          cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
+          cmd << shell_quote("HEAD:" + path) if identifier.nil?
+          cmd << shell_quote(identifier + ":" + path) if identifier
+          shellout(cmd)  do |io|
+            io.each_line do |line|
+              e = line.chomp.to_s
+              if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\s+(.+)$/
+                type = $1
+                sha = $2
+                size = $3
+                name = $4
+                entries << Entry.new({:name => name,
+                                       :path => (path.empty? ? name : "#{path}/#{name}"),
+                                       :kind => ((type == "tree") ? 'dir' : 'file'),
+                                       :size => ((type == "tree") ? nil : size),
+                                       :lastrev => get_rev(identifier,(path.empty? ? name : "#{path}/#{name}")) 
+                                                                  
+                                     }) unless entries.detect{|entry| entry.name == name}
+              end
+            end
+          end
+          return nil if $? && $?.exitstatus != 0
+          entries.sort_by_name
+        end
+        
+        def entry(path=nil, identifier=nil)
+          path ||= ''
+          search_path = path.split('/')[0..-2].join('/')
+          entry_name = path.split('/').last
+          e = entries(search_path, identifier)
+          e ? e.detect{|entry| entry.name == entry_name} : nil
+        end
+        
+        def revisions(path, identifier_from, identifier_to, options={})
+          revisions = Revisions.new
+          cmd = "#{GIT_BIN} --git-dir #{target('')} log --raw "
+          cmd << " -n #{options[:limit].to_i} " if (!options.nil?) && options[:limit]
+          cmd << " #{shell_quote(identifier_from + '..')} " if identifier_from
+          cmd << " #{shell_quote identifier_to} " if identifier_to
+          #cmd << " HEAD " if !identifier_to
+          shellout(cmd) do |io|
+            files=[]
+            changeset = {}
+            parsing_descr = 0  #0: not parsing desc or files, 1: parsing desc, 2: parsing files
+            revno = 1
+
+            io.each_line do |line|
+              if line =~ /^commit ([0-9a-f]{40})$/
+                key = "commit"
+                value = $1
+                if (parsing_descr == 1 || parsing_descr == 2)
+                  parsing_descr = 0
+                  revisions << Revision.new({:identifier => changeset[:commit],
+                                             :scmid => changeset[:commit],
+                                             :author => changeset[:author],
+                                             :time => Time.parse(changeset[:date]),
+                                             :message => changeset[:description],
+                                             :paths => files
+                                            })
+                  changeset = {}
+                  files = []
+                  revno = revno + 1
+                end
+                changeset[:commit] = $1
+              elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
+                key = $1
+                value = $2
+                if key == "Author"
+                  changeset[:author] = value
+                elsif key == "Date"
+                  changeset[:date] = value
+                end
+              elsif (parsing_descr == 0) && line.chomp.to_s == ""
+                parsing_descr = 1
+                changeset[:description] = ""
+              elsif (parsing_descr == 1 || parsing_descr == 2) && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\s+(.+)$/
+                parsing_descr = 2
+                fileaction = $1
+                filepath = $2
+                files << {:action => fileaction, :path => filepath}
+              elsif (parsing_descr == 1) && line.chomp.to_s == ""
+                parsing_descr = 2
+              elsif (parsing_descr == 1)
+                changeset[:description] << line[4..-1]
+              end
+            end        
+
+            revisions << Revision.new({:identifier => changeset[:commit],
+                                       :scmid => changeset[:commit],
+                                       :author => changeset[:author],
+                                       :time => Time.parse(changeset[:date]),
+                                       :message => changeset[:description],
+                                       :paths => files
+                                      }) if changeset[:commit]
+
+          end
+
+          return nil if $? && $?.exitstatus != 0
+          revisions
+        end
+        
+        def diff(path, identifier_from, identifier_to=nil, type="inline")
+          path ||= ''
+          if !identifier_to
+            identifier_to = nil
+          end
+          
+          cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote identifier_from}" if identifier_to.nil?
+          cmd = "#{GIT_BIN} --git-dir #{target('')} diff #{shell_quote identifier_to} #{shell_quote identifier_from}" if !identifier_to.nil?
+          cmd << " -- #{shell_quote path}" unless path.empty?
+          diff = []
+          shellout(cmd) do |io|
+            io.each_line do |line|
+              diff << line
+            end
+          end
+          return nil if $? && $?.exitstatus != 0
+          DiffTableList.new diff, type
+        end
+        
+        def annotate(path, identifier=nil)
+          identifier = 'HEAD' if identifier.blank?
+          cmd = "#{GIT_BIN} --git-dir #{target('')} blame -l #{shell_quote identifier} -- #{shell_quote path}"
+          blame = Annotate.new
+          shellout(cmd) do |io|
+            io.each_line do |line|
+              next unless line =~ /([0-9a-f]{39,40})\s\((\w*)[^\)]*\)(.*)$/
+              blame.add_line($3.rstrip, Revision.new(:identifier => $1, :author => $2.strip))
+            end
+          end
+          return nil if $? && $?.exitstatus != 0
+          blame
+        end
+        
+        def cat(path, identifier=nil)
+          if identifier.nil?
+            identifier = 'HEAD'
+          end
+          cmd = "#{GIT_BIN} --git-dir #{target('')} show #{shell_quote(identifier + ':' + path)}"
+          cat = nil
+          shellout(cmd) do |io|
+            io.binmode
+            cat = io.read
+          end
+          return nil if $? && $?.exitstatus != 0
+          cat
+        end
+      end
+    end
+  end
+
+end
+
diff --git a/test/fixtures/repositories/darcs_repository.tar.gz b/test/fixtures/repositories/darcs_repository.tar.gz
new file mode 100644 (file)
index 0000000..ba4d8d0
Binary files /dev/null and b/test/fixtures/repositories/darcs_repository.tar.gz differ
diff --git a/test/fixtures/repositories/git_repository.tar.gz b/test/fixtures/repositories/git_repository.tar.gz
new file mode 100644 (file)
index 0000000..84de88a
Binary files /dev/null and b/test/fixtures/repositories/git_repository.tar.gz differ
diff --git a/test/functional/repositories_darcs_controller_test.rb b/test/functional/repositories_darcs_controller_test.rb
new file mode 100644 (file)
index 0000000..fc77b87
--- /dev/null
@@ -0,0 +1,94 @@
+# redMine - project management software
+# Copyright (C) 2006-2008  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'repositories_controller'
+
+# Re-raise errors caught by the controller.
+class RepositoriesController; def rescue_action(e) raise e end; end
+
+class RepositoriesDarcsControllerTest < Test::Unit::TestCase
+  fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
+
+  # No '..' in the repository path
+  REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
+
+  def setup
+    @controller = RepositoriesController.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+    User.current = nil
+    Repository::Darcs.create(:project => Project.find(3), :url => REPOSITORY_PATH)
+  end
+  
+  if File.directory?(REPOSITORY_PATH)
+    def test_show
+      get :show, :id => 3
+      assert_response :success
+      assert_template 'show'
+      assert_not_nil assigns(:entries)
+      assert_not_nil assigns(:changesets)
+    end
+    
+    def test_browse_root
+      get :browse, :id => 3
+      assert_response :success
+      assert_template 'browse'
+      assert_not_nil assigns(:entries)
+      assert_equal 3, assigns(:entries).size
+      assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+      assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+      assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+    end
+    
+    def test_browse_directory
+      get :browse, :id => 3, :path => ['images']
+      assert_response :success
+      assert_template 'browse'
+      assert_not_nil assigns(:entries)
+      assert_equal 2, assigns(:entries).size
+      entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+      assert_not_nil entry
+      assert_equal 'file', entry.kind
+      assert_equal 'images/edit.png', entry.path
+    end
+    
+    def test_changes
+      get :changes, :id => 3, :path => ['images', 'edit.png']
+      assert_response :success
+      assert_template 'changes'
+      assert_tag :tag => 'h2', :content => 'edit.png'
+    end
+  
+    def test_diff
+      Project.find(3).repository.fetch_changesets
+      # Full diff of changeset 5
+      get :diff, :id => 3, :rev => 5
+      assert_response :success
+      assert_template 'diff'
+      # Line 22 removed
+      assert_tag :tag => 'th',
+                 :content => /22/,
+                 :sibling => { :tag => 'td', 
+                               :attributes => { :class => /diff_out/ },
+                               :content => /def remove/ }
+    end
+  else
+    puts "Darcs test repository NOT FOUND. Skipping functional tests !!!"
+    def test_fake; assert true end
+  end
+end
diff --git a/test/functional/repositories_git_controller_test.rb b/test/functional/repositories_git_controller_test.rb
new file mode 100644 (file)
index 0000000..fec0bba
--- /dev/null
@@ -0,0 +1,123 @@
+# redMine - project management software
+# Copyright (C) 2006-2007  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+require 'repositories_controller'
+
+# Re-raise errors caught by the controller.
+class RepositoriesController; def rescue_action(e) raise e end; end
+
+class RepositoriesGitControllerTest < Test::Unit::TestCase
+  fixtures :projects, :users, :roles, :members, :repositories, :enabled_modules
+
+  # No '..' in the repository path
+  REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
+  REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
+
+  def setup
+    @controller = RepositoriesController.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+    User.current = nil
+    Repository::Git.create(:project => Project.find(3), :url => REPOSITORY_PATH)
+  end
+  
+  if File.directory?(REPOSITORY_PATH)
+    def test_show
+      get :show, :id => 3
+      assert_response :success
+      assert_template 'show'
+      assert_not_nil assigns(:entries)
+      assert_not_nil assigns(:changesets)
+    end
+    
+    def test_browse_root
+      get :browse, :id => 3
+      assert_response :success
+      assert_template 'browse'
+      assert_not_nil assigns(:entries)
+      assert_equal 3, assigns(:entries).size
+      assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'}
+      assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'}
+      assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'}
+    end
+    
+    def test_browse_directory
+      get :browse, :id => 3, :path => ['images']
+      assert_response :success
+      assert_template 'browse'
+      assert_not_nil assigns(:entries)
+      assert_equal 2, assigns(:entries).size
+      entry = assigns(:entries).detect {|e| e.name == 'edit.png'}
+      assert_not_nil entry
+      assert_equal 'file', entry.kind
+      assert_equal 'images/edit.png', entry.path
+    end
+    
+    def test_changes
+      get :changes, :id => 3, :path => ['images', 'edit.png']
+      assert_response :success
+      assert_template 'changes'
+      assert_tag :tag => 'h2', :content => 'edit.png'
+    end
+    
+    def test_entry_show
+      get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb']
+      assert_response :success
+      assert_template 'entry'
+      # Line 19
+      assert_tag :tag => 'th',
+                 :content => /10/,
+                 :attributes => { :class => /line-num/ },
+                 :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ }
+    end
+    
+    def test_entry_download
+      get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw'
+      assert_response :success
+      # File content
+      assert @response.body.include?('WITHOUT ANY WARRANTY')
+    end
+  
+    def test_diff
+      # Full diff of changeset 2f9c0091
+      get :diff, :id => 3, :rev => '2f9c0091c754a91af7a9c478e36556b4bde8dcf7'
+      assert_response :success
+      assert_template 'diff'
+      # Line 22 removed
+      assert_tag :tag => 'th',
+                 :content => /22/,
+                 :sibling => { :tag => 'td', 
+                               :attributes => { :class => /diff_out/ },
+                               :content => /def remove/ }
+    end
+
+    def test_annotate
+      get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb']
+      assert_response :success
+      assert_template 'annotate'
+      # Line 23, changeset 2f9c0091
+      assert_tag :tag => 'th', :content => /23/,
+                 :sibling => { :tag => 'td', :child => { :tag => 'a', :content => /2f9c0091/ } },
+                 :sibling => { :tag => 'td', :content => /jsmith/ },
+                 :sibling => { :tag => 'td', :content => /watcher =/ }
+    end
+  else
+    puts "Git test repository NOT FOUND. Skipping functional tests !!!"
+    def test_fake; assert true end
+  end
+end
index 3f6db06ebb9a8f22d33a4870616d87014233f14f..b14d9d96476c6be0806ab5964e4719465bc8bbe1 100644 (file)
@@ -40,13 +40,13 @@ class RepositoryCvsTest < Test::Unit::TestCase
       
       assert_equal 5, @repository.changesets.count
       assert_equal 14, @repository.changes.count
-      assert_equal 'Two files changed', @repository.changesets.find_by_revision(3).comments
+      assert_not_nil @repository.changesets.find_by_comments('Two files changed')
     end
     
     def test_fetch_changesets_incremental
       @repository.fetch_changesets
-      # Remove changesets with revision > 2
-      @repository.changesets.find(:all, :conditions => 'revision > 2').each(&:destroy)
+      # Remove the 3 latest changesets
+      @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy)
       @repository.reload
       assert_equal 2, @repository.changesets.count
       
diff --git a/test/unit/repository_darcs_test.rb b/test/unit/repository_darcs_test.rb
new file mode 100644 (file)
index 0000000..1228976
--- /dev/null
@@ -0,0 +1,55 @@
+# redMine - project management software
+# Copyright (C) 2006-2008  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RepositoryDarcsTest < Test::Unit::TestCase
+  fixtures :projects
+  
+  # No '..' in the repository path
+  REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/darcs_repository'
+  
+  def setup
+    @project = Project.find(1)
+    assert @repository = Repository::Darcs.create(:project => @project, :url => REPOSITORY_PATH)
+  end
+  
+  if File.directory?(REPOSITORY_PATH)  
+    def test_fetch_changesets_from_scratch
+      @repository.fetch_changesets
+      @repository.reload
+      
+      assert_equal 6, @repository.changesets.count
+      assert_equal 13, @repository.changes.count
+      assert_equal "Initial commit.", @repository.changesets.find_by_revision(1).comments
+    end
+    
+    def test_fetch_changesets_incremental
+      @repository.fetch_changesets
+      # Remove changesets with revision > 3
+      @repository.changesets.find(:all, :conditions => 'revision > 3').each(&:destroy)
+      @repository.reload
+      assert_equal 3, @repository.changesets.count
+      
+      @repository.fetch_changesets
+      assert_equal 6, @repository.changesets.count
+    end
+  else
+    puts "Darcs test repository NOT FOUND. Skipping unit tests !!!"
+    def test_fake; assert true end
+  end
+end
diff --git a/test/unit/repository_git_test.rb b/test/unit/repository_git_test.rb
new file mode 100644 (file)
index 0000000..c7bd84a
--- /dev/null
@@ -0,0 +1,56 @@
+# redMine - project management software
+# Copyright (C) 2006-2007  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RepositoryGitTest < Test::Unit::TestCase
+  fixtures :projects
+  
+  # No '..' in the repository path
+  REPOSITORY_PATH = RAILS_ROOT.gsub(%r{config\/\.\.}, '') + '/tmp/test/git_repository'
+  REPOSITORY_PATH.gsub!(/\//, "\\") if RUBY_PLATFORM =~ /mswin/
+  
+  def setup
+    @project = Project.find(1)
+    assert @repository = Repository::Git.create(:project => @project, :url => REPOSITORY_PATH)
+  end
+  
+  if File.directory?(REPOSITORY_PATH)  
+    def test_fetch_changesets_from_scratch
+      @repository.fetch_changesets
+      @repository.reload
+      
+      assert_equal 6, @repository.changesets.count
+      assert_equal 11, @repository.changes.count
+      assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find(:first, :order => 'id ASC').comments
+    end
+    
+    def test_fetch_changesets_incremental
+      @repository.fetch_changesets
+      # Remove the 3 latest changesets
+      @repository.changesets.find(:all, :order => 'id DESC', :limit => 3).each(&:destroy)
+      @repository.reload
+      assert_equal 3, @repository.changesets.count
+      
+      @repository.fetch_changesets
+      assert_equal 6, @repository.changesets.count
+    end
+  else
+    puts "Git test repository NOT FOUND. Skipping unit tests !!!"
+    def test_fake; assert true end
+  end
+end