]> source.dussan.org Git - redmine.git/commitdiff
Added Annotate/Blame view for Subversion, CVS and Mercurial repositories.
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 2 Dec 2007 20:58:02 +0000 (20:58 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 2 Dec 2007 20:58:02 +0000 (20:58 +0000)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@947 e93f8b46-1217-0410-a6f0-8f06a7374b81

31 files changed:
app/controllers/repositories_controller.rb
app/models/repository.rb
app/views/repositories/annotate.rhtml [new file with mode: 0644]
app/views/repositories/changes.rhtml
app/views/repositories/entry.rhtml
config/routes.rb
lang/bg.yml
lang/cs.yml
lang/de.yml
lang/en.yml
lang/es.yml
lang/fr.yml
lang/he.yml
lang/it.yml
lang/ja.yml
lang/ko.yml
lang/nl.yml
lang/pl.yml
lang/pt-br.yml
lang/pt.yml
lang/ro.yml
lang/ru.yml
lang/sr.yml
lang/sv.yml
lang/zh.yml
lib/redmine.rb
lib/redmine/scm/adapters/abstract_adapter.rb
lib/redmine/scm/adapters/cvs_adapter.rb
lib/redmine/scm/adapters/mercurial_adapter.rb
lib/redmine/scm/adapters/subversion_adapter.rb
public/stylesheets/scm.css

index b332c72131c1c8fa84f0ad7a9fb5644e6ca25748..dfd5d0a6f852c42ca6be1bc7fa8793006bca1c51 100644 (file)
@@ -95,6 +95,11 @@ class RepositoriesController < ApplicationController
     end
   end
   
+  def annotate
+    @annotate = @repository.scm.annotate(@path, @rev)
+    show_error and return if @annotate.nil? || @annotate.empty?
+  end
+  
   def revision
     @changeset = @repository.changesets.find_by_revision(@rev)
     raise ChangesetNotFound unless @changeset
index 35dd6803fcb47275caeb78d76c5c29567ce727bd..be31ac2e5b3b10e268f771be5ef0147602c03dd6 100644 (file)
@@ -33,6 +33,10 @@ class Repository < ActiveRecord::Base
   def supports_cat?
     scm.supports_cat?
   end
+
+  def supports_annotate?
+    scm.supports_annotate?
+  end
   
   def entries(path=nil, identifier=nil)
     scm.entries(path, identifier)
diff --git a/app/views/repositories/annotate.rhtml b/app/views/repositories/annotate.rhtml
new file mode 100644 (file)
index 0000000..b8f481a
--- /dev/null
@@ -0,0 +1,26 @@
+<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
+
+<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
+
+<div class="autoscroll">
+<table class="filecontent annotate CodeRay">
+  <tbody>
+    <% line_num = 1 %>
+    <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %>
+    <% revision = @annotate.revisions[line_num-1] %>
+    <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>
+      <td class="author"><%= h(revision.author) if revision %></td>
+      <td class="line-code"><pre><%= line %></pre></td>
+    </tr>
+    <% line_num += 1 %>
+    <% end %>
+  <tbody>
+</table>
+</div>
+
+<% content_for :header_tags do %>
+<%= stylesheet_link_tag 'scm' %>
+<% end %>
index 5d1db96ba3140936dd006d1796c854ea3145a6d5..f843983db24424d270350bcd3958d7cd26fade38 100644 (file)
@@ -2,14 +2,17 @@
 
 <h3><%=h @entry.name %></h3>
 
-<% if @repository.supports_cat? %>
 <p>
 <% if @entry.is_text? %>
-<%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
+    <% if @repository.supports_cat? %>
+        <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
+    <% end %>
+    <% if @repository.supports_annotate? %>
+        <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => @path, :rev => @rev } %> |
+    <% end %>
 <% end %>
-<%= link_to l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' } %>
+<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
 <%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
 </p>
-<% end %>
 
 <%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%>
index 94db240abc48febff91b2d0a271139bd069e4c4e..9927601d7d81d4bf33b51245b92473c7949a0cbc 100644 (file)
@@ -2,11 +2,6 @@
 
 <div class="autoscroll">
 <table class="filecontent CodeRay">
-  <thead>
-    <tr>
-      <th colspan="2" class="filename"><%= @path %></th>
-    </tr>
-  </thead>
   <tbody>
     <% line_num = 1 %>
     <% syntax_highlight(@path, to_utf8(@content)).each_line do |line| %>
index c3637304a7e20a1cdea2f41ebfc1795f0b36b501..5048da2f5b21c0055ebf17fb8c5bd23b81a86722 100644 (file)
@@ -24,6 +24,7 @@ ActionController::Routing::Routes.draw do |map|
     omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
     omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
     omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
+    omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
   end
   
   # Allow downloading Web Service WSDL as a file with an extension
index 6f5f527c77017c2645eab416353a7c90b37071af..5f2dc6a8dd4f57cb32eda447828a0b65ae3e8071 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index 1bc80c9dc40bd5394a818725679a870d5eeda1af..49697dda8e5dae951b6e8a5acdd01db273db8614 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index b261292633552d9f96c089a45a14daf4957fb00c..870f45014803ab0dd34fee38275cd89f854c1ac0 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Ihr Konto wurde erstellt und wartet jetzt auf die Geneh
 field_time_zone: Zeitzone
 text_caracters_minimum: Muss mindestens %d Zeichen lang sein.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index fb99bbb82c6e92e0cab6b37ecdedca82a03603dd..cdecb6693a407ba80f0c94bb2724c5439ee618e4 100644 (file)
@@ -489,6 +489,7 @@ button_reset: Reset
 button_rename: Rename
 button_change_password: Change password
 button_copy: Copy
+button_annotate: Annotate
 
 status_active: active
 status_registered: registered
index 1b5638eb56bb165565228ef00e668b7737055dc7..5f25de7cfe53c9c20e8d9f7820512d7e3e5ff181 100644 (file)
@@ -550,3 +550,4 @@ label_registration_manual_activation: activación manual de cuenta
 notice_account_pending: "Su cuenta ha sido creada y está pendiende de la aprobación por parte de administrador"
 setting_time_format: Formato de hora
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index b2466f1e13262d66fb8cfed71401e78b69fcb848..63f367f0623f1885dc796c2d05cbb0e5896242ed 100644 (file)
@@ -489,6 +489,7 @@ button_reset: Réinitialiser
 button_rename: Renommer
 button_change_password: Changer de mot de passe
 button_copy: Copier
+button_annotate: Annoter
 
 status_active: actif
 status_registered: enregistré
index 0f75a6ec10076d399671a7d2f854b92c2c5e5803..4ebbecf7afef134e22c3aadcf341938c9abe0402 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index a3a858cef69c22aec5646e6caa6762a63ea7aa95..fa13513cb914e15fe62ec542aff46203cf23fd30 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index 2233c2a386b685e026fc3152394dba0358f0e141..d2f7d579f680900c56622bbaec940f54bf885723 100644 (file)
@@ -548,3 +548,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index 8962d8cd544929115bd036dc9d92397c4b90006a..dd50ce2bc4fc972b74ff710b2af6d253cdeb6587 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index ca6ad66f386591254a80c703d8ee93be1379fdf5..895feb5ede3f020f84bbdc99c1f07d400a2d0547 100644 (file)
@@ -548,3 +548,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index f478df8fb9621fa767d62f91f9fafef1d0a3c4d9..381dc5e26b3238518141aa624b6cf9df4c8c0f93 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Twoje konto zostało utworzone i oczekuje na zatwierdze
 field_time_zone: Strefa czasowa
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index df893ff6d413a6f2026e0ae89278512994da42db..4c4d862d35e0ef12cfe7dabba6d38440b3cd925c 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone\r
 text_caracters_minimum: Must be at least %d characters long.\r
 setting_bcc_recipients: Blind carbon copy recipients (bcc)\r
+button_annotate: Annotate\r
index 1a499a2c58bc764e88fd5b0bcd494e11495839b6..1aabc8ac76b39d0f4953aa155cceb457a4e47159 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index a57330cc0b8c3853b7a79baa4981fdf20c130c42..0c10c7727ed54be8664b05760e5fe4dd7a3b8d58 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index 2f73a43b909a0320e1aefbc0e0f495f919bb7738..c6e27ca9fbbdd3f31b20107df14b93d10854a33a 100644 (file)
@@ -547,3 +547,4 @@ notice_account_pending: "Ваш аккаунт уже создан и ожида
 field_time_zone: Часовой пояс
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index 49e2d5f3a828ff6fe1093555f2fea23accda70ee..42aa3cc3e8f8c138e82a9d869b25e5bd99a7aa1b 100644 (file)
@@ -548,3 +548,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index 11a8ce059f83825dc908574e6480b514a3f9c058..7cbcc7a5fcd2c8ec30ddceb08e9319f9c31af712 100644 (file)
@@ -548,3 +548,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index 48176ceba156972c41f022889685dac18b5ff71d..ad18cecc3ec0f888a57d501e35d55b8f148af4d5 100644 (file)
@@ -550,3 +550,4 @@ notice_account_pending: "Your account was created and is now pending administrat
 field_time_zone: Time zone
 text_caracters_minimum: Must be at least %d characters long.
 setting_bcc_recipients: Blind carbon copy recipients (bcc)
+button_annotate: Annotate
index ffc1cc27e13617ec1a11b4aa0547069c4f760841..b74f00aec420964f90e9e3650fe3b27b2f3cb42c 100644 (file)
@@ -76,7 +76,7 @@ Redmine::AccessControl.map do |map|
     
   map.project_module :repository do |map|
     map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
-    map.permission :browse_repository, :repositories => [:show, :browse, :entry, :changes, :diff, :stats, :graph]
+    map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
     map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
   end
 
index 4b524c5388c77b56b19180bec4d0ffe67e8cf031..720a4e9d9c350cdf88e09c93b7ec93f6443a3e12 100644 (file)
@@ -38,6 +38,10 @@ module Redmine
         def supports_cat?
           true
         end
+
+        def supports_annotate?
+          respond_to?('annotate')
+        end
         
         def root_url
           @root_url
@@ -76,7 +80,7 @@ module Redmine
         def cat(path, identifier=nil)
           return nil
         end
-
+        
         def with_leading_slash(path)
           path ||= ''
           (path[0,1]!="/") ? "/#{path}" : path
@@ -237,7 +241,7 @@ module Redmine
     
         # Initialize with a Diff file and the type of Diff View
         # The type view must be inline or sbs (side_by_side)
-        def initialize (type="inline")
+        def initialize(type="inline")
           @parsing = false
           @nb_line = 1
           @start = false
@@ -312,7 +316,7 @@ module Redmine
             CGI.escapeHTML(line)
         end
     
-        def parse_line (line, type="inline")
+        def parse_line(line, type="inline")
           if line[0, 1] == "+"
             diff = sbs? type, 'add'
             @before = 'add'
@@ -348,6 +352,28 @@ module Redmine
           end
         end
       end
+    
+      class Annotate
+        attr_reader :lines, :revisions
+        
+        def initialize
+          @lines = []
+          @revisions = []
+        end
+        
+        def add_line(line, revision)
+          @lines << line
+          @revisions << revision
+        end
+        
+        def content
+          content = lines.join("\n")
+        end
+        
+        def empty?
+          lines.empty?
+        end
+      end
     end
   end
 end
index e84c1eea3c8d424c99c3d5c3c3793d27ed04b9bc..5c6c1775bea3d2672ffa6f4b216fc5b003c943d0 100644 (file)
@@ -268,7 +268,25 @@ module Redmine
         rescue Errno::ENOENT => e
           raise CommandFailed    
         end  
-        
+
+        def annotate(path, identifier=nil)
+          identifier = (identifier) ? identifier : "HEAD"
+          logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
+          path_with_project="#{url}#{with_leading_slash(path)}"
+          cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{path_with_project}"
+          blame = Annotate.new
+          shellout(cmd) do |io|
+            io.each_line do |line|
+              next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
+              blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
+            end
+          end
+          return nil if $? && $?.exitstatus != 0
+          blame
+        rescue Errno::ENOENT => e
+          raise CommandFailed    
+        end
+         
         private
 
         # convert a date/time into the CVS-format
index a631916c50d51ca92986348c4e9bd90c535cc0ed..3cbf01f9112ff23033d410e7d0cd7aa077542d10 100644 (file)
@@ -157,6 +157,25 @@ module Redmine
         rescue Errno::ENOENT => e
           raise CommandFailed
         end
+        
+        def annotate(path, identifier=nil)
+          path ||= ''
+          cmd = "#{HG_BIN} -R #{target('')}"
+          cmd << " annotate -n -u"
+          cmd << " -r #{identifier.to_i}" if identifier
+          cmd << " #{target(path)}"
+          blame = Annotate.new
+          shellout(cmd) do |io|
+            io.each_line do |line|
+              next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
+              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
+            end
+          end
+          return nil if $? && $?.exitstatus != 0
+          blame
+        rescue Errno::ENOENT => e
+          raise CommandFailed
+        end
       end
     end
   end
index 9e8acce4c2aae434357c875e818b1aff04c372cd..d55b8712e745233edd396f2502b7a4295713c192 100644 (file)
@@ -173,6 +173,23 @@ module Redmine
           raise CommandFailed    \r
         end\r
         \r
+        def annotate(path, identifier=nil)\r
+          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"\r
+          cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"\r
+          cmd << credentials_string\r
+          blame = Annotate.new\r
+          shellout(cmd) do |io|\r
+            io.each_line do |line|\r
+              next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}\r
+              blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))\r
+            end\r
+          end\r
+          return nil if $? && $?.exitstatus != 0\r
+          blame\r
+        rescue Errno::ENOENT => e\r
+          raise CommandFailed\r
+        end\r
+        \r
         private\r
         \r
         def credentials_string\r
index 3794db366314ccb1b3e75fd63aa3a350b87d9e29..c3dc307d66510e9f72cde6409473883b47d026cd 100644 (file)
@@ -2,20 +2,52 @@
 table.filecontent { border: 1px solid #ccc;  border-collapse: collapse; width:98%; }
 table.filecontent th { border: 1px solid #ccc; background-color: #eee; }
 table.filecontent th.filename {        background-color: #ddc; text-align: left; }
-div.action_M { background: #fd8 }
-div.action_D { background: #f88 }
-div.action_A { background: #bfb }
-
 table.filecontent tr.spacing { border: 1px solid #d7d7d7; }
-
-table.filecontent .line-num {
+table.filecontent th.line-num {
     border: 1px solid #d7d7d7;
        font-size: 0.8em;
        text-align: right;
-       width: 3em;
+       width: 2%;
        padding-right: 3px;
 }
 
+/* 12 different colors for the annonate view */
+table.annotate tr.bloc-0 {background: #FFFFBF;}
+table.annotate tr.bloc-1 {background: #EABFFF;}
+table.annotate tr.bloc-2 {background: #BFFFFF;}
+table.annotate tr.bloc-3 {background: #FFD9BF;}
+table.annotate tr.bloc-4 {background: #E6FFBF;}
+table.annotate tr.bloc-5 {background: #BFCFFF;}
+table.annotate tr.bloc-6 {background: #FFBFEF;}
+table.annotate tr.bloc-7 {background: #FFE6BF;}
+table.annotate tr.bloc-8 {background: #FFE680;}
+table.annotate tr.bloc-9 {background: #AA80FF;}
+table.annotate tr.bloc-10 {background: #FFBFDC;}
+table.annotate tr.bloc-11 {background: #BFE4FF;}
+
+table.annotate td.revision {
+    text-align: center;
+    width: 2%;
+    padding-left: 1em;
+    background: inherit;
+}
+    
+table.annotate td.author {
+    text-align: center;
+    border-right: 1px solid #d7d7d7;
+    white-space: nowrap;
+    padding-left: 1em;
+    padding-right: 1em;
+    width: 3%;
+    background: inherit;
+}
+
+table.annotate td.line-code { background-color: #fafafa; }
+
+div.action_M { background: #fd8 }
+div.action_D { background: #f88 }
+div.action_A { background: #bfb }
+
 /************* Coderay styles *************/
 
 table.CodeRay {