diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index b332c72..dfd5d0a 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -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 diff --git a/app/models/repository.rb b/app/models/repository.rb index 35dd680..be31ac2 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -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 index 0000000..b8f481a --- /dev/null +++ b/app/views/repositories/annotate.rhtml @@ -0,0 +1,26 @@ +

<%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %>

+ +<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %> + +
+ + + <% line_num = 1 %> + <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %> + <% revision = @annotate.revisions[line_num-1] %> + + + + + + + <% line_num += 1 %> + <% end %> + +
<%= line_num %> + <%= (revision.identifier ? link_to(revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier) : revision.revision) if revision %><%= h(revision.author) if revision %>
<%= line %>
+
+ +<% content_for :header_tags do %> +<%= stylesheet_link_tag 'scm' %> +<% end %> diff --git a/app/views/repositories/changes.rhtml b/app/views/repositories/changes.rhtml index 5d1db96..f843983 100644 --- a/app/views/repositories/changes.rhtml +++ b/app/views/repositories/changes.rhtml @@ -2,14 +2,17 @@

<%=h @entry.name %>

-<% if @repository.supports_cat? %>

<% 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 %>

-<% end %> <%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%> diff --git a/app/views/repositories/entry.rhtml b/app/views/repositories/entry.rhtml index 94db240..9927601 100644 --- a/app/views/repositories/entry.rhtml +++ b/app/views/repositories/entry.rhtml @@ -2,11 +2,6 @@
- - - - - <% line_num = 1 %> <% syntax_highlight(@path, to_utf8(@content)).each_line do |line| %> diff --git a/config/routes.rb b/config/routes.rb index c363730..5048da2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/lang/bg.yml b/lang/bg.yml index 6f5f527..5f2dc6a 100644 --- a/lang/bg.yml +++ b/lang/bg.yml @@ -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 diff --git a/lang/cs.yml b/lang/cs.yml index 1bc80c9..49697dd 100644 --- a/lang/cs.yml +++ b/lang/cs.yml @@ -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 diff --git a/lang/de.yml b/lang/de.yml index b261292..870f450 100644 --- a/lang/de.yml +++ b/lang/de.yml @@ -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 diff --git a/lang/en.yml b/lang/en.yml index fb99bbb..cdecb66 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -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 diff --git a/lang/es.yml b/lang/es.yml index 1b5638e..5f25de7 100644 --- a/lang/es.yml +++ b/lang/es.yml @@ -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 diff --git a/lang/fr.yml b/lang/fr.yml index b2466f1..63f367f 100644 --- a/lang/fr.yml +++ b/lang/fr.yml @@ -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é diff --git a/lang/he.yml b/lang/he.yml index 0f75a6e..4ebbecf 100644 --- a/lang/he.yml +++ b/lang/he.yml @@ -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 diff --git a/lang/it.yml b/lang/it.yml index a3a858c..fa13513 100644 --- a/lang/it.yml +++ b/lang/it.yml @@ -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 diff --git a/lang/ja.yml b/lang/ja.yml index 2233c2a..d2f7d57 100644 --- a/lang/ja.yml +++ b/lang/ja.yml @@ -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 diff --git a/lang/ko.yml b/lang/ko.yml index 8962d8c..dd50ce2 100644 --- a/lang/ko.yml +++ b/lang/ko.yml @@ -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 diff --git a/lang/nl.yml b/lang/nl.yml index ca6ad66..895feb5 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -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 diff --git a/lang/pl.yml b/lang/pl.yml index f478df8..381dc5e 100644 --- a/lang/pl.yml +++ b/lang/pl.yml @@ -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 diff --git a/lang/pt-br.yml b/lang/pt-br.yml index df893ff..4c4d862 100644 --- a/lang/pt-br.yml +++ b/lang/pt-br.yml @@ -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 diff --git a/lang/pt.yml b/lang/pt.yml index 1a499a2..1aabc8a 100644 --- a/lang/pt.yml +++ b/lang/pt.yml @@ -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 diff --git a/lang/ro.yml b/lang/ro.yml index a57330c..0c10c77 100644 --- a/lang/ro.yml +++ b/lang/ro.yml @@ -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 diff --git a/lang/ru.yml b/lang/ru.yml index 2f73a43..c6e27ca 100644 --- a/lang/ru.yml +++ b/lang/ru.yml @@ -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 diff --git a/lang/sr.yml b/lang/sr.yml index 49e2d5f..42aa3cc 100644 --- a/lang/sr.yml +++ b/lang/sr.yml @@ -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 diff --git a/lang/sv.yml b/lang/sv.yml index 11a8ce0..7cbcc7a 100644 --- a/lang/sv.yml +++ b/lang/sv.yml @@ -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 diff --git a/lang/zh.yml b/lang/zh.yml index 48176ce..ad18cec 100644 --- a/lang/zh.yml +++ b/lang/zh.yml @@ -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 diff --git a/lib/redmine.rb b/lib/redmine.rb index ffc1cc2..b74f00a 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -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 diff --git a/lib/redmine/scm/adapters/abstract_adapter.rb b/lib/redmine/scm/adapters/abstract_adapter.rb index 4b524c5..720a4e9 100644 --- a/lib/redmine/scm/adapters/abstract_adapter.rb +++ b/lib/redmine/scm/adapters/abstract_adapter.rb @@ -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 diff --git a/lib/redmine/scm/adapters/cvs_adapter.rb b/lib/redmine/scm/adapters/cvs_adapter.rb index e84c1ee..5c6c177 100644 --- a/lib/redmine/scm/adapters/cvs_adapter.rb +++ b/lib/redmine/scm/adapters/cvs_adapter.rb @@ -268,7 +268,25 @@ module Redmine rescue Errno::ENOENT => e raise CommandFailed end - + + def annotate(path, identifier=nil) + identifier = (identifier) ? identifier : "HEAD" + logger.debug " 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 diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index a631916..3cbf01f 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -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 diff --git a/lib/redmine/scm/adapters/subversion_adapter.rb b/lib/redmine/scm/adapters/subversion_adapter.rb index 9e8acce..d55b871 100644 --- a/lib/redmine/scm/adapters/subversion_adapter.rb +++ b/lib/redmine/scm/adapters/subversion_adapter.rb @@ -173,6 +173,23 @@ module Redmine raise CommandFailed end + def annotate(path, identifier=nil) + identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD" + cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}" + cmd << credentials_string + blame = Annotate.new + shellout(cmd) do |io| + io.each_line do |line| + next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$} + blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip)) + end + end + return nil if $? && $?.exitstatus != 0 + blame + rescue Errno::ENOENT => e + raise CommandFailed + end + private def credentials_string diff --git a/public/stylesheets/scm.css b/public/stylesheets/scm.css index 3794db3..c3dc307 100644 --- a/public/stylesheets/scm.css +++ b/public/stylesheets/scm.css @@ -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 {
<%= @path %>