Project

General

Profile

« Previous | Next » 

Revision 947

Added Annotate/Blame view for Subversion, CVS and Mercurial repositories.

View differences:

trunk/app/controllers/repositories_controller.rb
95 95
    end
96 96
  end
97 97
  
98
  def annotate
99
    @annotate = @repository.scm.annotate(@path, @rev)
100
    show_error and return if @annotate.nil? || @annotate.empty?
101
  end
102
  
98 103
  def revision
99 104
    @changeset = @repository.changesets.find_by_revision(@rev)
100 105
    raise ChangesetNotFound unless @changeset
trunk/app/models/repository.rb
33 33
  def supports_cat?
34 34
    scm.supports_cat?
35 35
  end
36

  
37
  def supports_annotate?
38
    scm.supports_annotate?
39
  end
36 40
  
37 41
  def entries(path=nil, identifier=nil)
38 42
    scm.entries(path, identifier)
trunk/app/views/repositories/annotate.rhtml
1
<h2><%= render :partial => 'navigation', :locals => { :path => @path, :kind => 'file', :revision => @rev } %></h2>
2

  
3
<% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
4

  
5
<div class="autoscroll">
6
<table class="filecontent annotate CodeRay">
7
  <tbody>
8
    <% line_num = 1 %>
9
    <% syntax_highlight(@path, to_utf8(@annotate.content)).each_line do |line| %>
10
    <% revision = @annotate.revisions[line_num-1] %>
11
    <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>">
12
      <th class="line-num"><%= line_num %></th>
13
      <td class="revision">
14
      <%= (revision.identifier ? link_to(revision.identifier, :action => 'revision', :id => @project, :rev => revision.identifier) : revision.revision) if revision %></td>
15
      <td class="author"><%= h(revision.author) if revision %></td>
16
      <td class="line-code"><pre><%= line %></pre></td>
17
    </tr>
18
    <% line_num += 1 %>
19
    <% end %>
20
  <tbody>
21
</table>
22
</div>
23

  
24
<% content_for :header_tags do %>
25
<%= stylesheet_link_tag 'scm' %>
26
<% end %>
0 27

  
trunk/app/views/repositories/changes.rhtml
2 2

  
3 3
<h3><%=h @entry.name %></h3>
4 4

  
5
<% if @repository.supports_cat? %>
6 5
<p>
7 6
<% if @entry.is_text? %>
8
<%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
7
    <% if @repository.supports_cat? %>
8
        <%= link_to l(:button_view), {:action => 'entry', :id => @project, :path => @path, :rev => @rev } %> |
9
    <% end %>
10
    <% if @repository.supports_annotate? %>
11
        <%= link_to l(:button_annotate), {:action => 'annotate', :id => @project, :path => @path, :rev => @rev } %> |
12
    <% end %>
9 13
<% end %>
10
<%= link_to l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' } %>
14
<%= link_to(l(:button_download), {:action => 'entry', :id => @project, :path => @path, :rev => @rev, :format => 'raw' }) if @repository.supports_cat? %>
11 15
<%= "(#{number_to_human_size(@entry.size)})" if @entry.size %>
12 16
</p>
13
<% end %>
14 17

  
15 18
<%= render :partial => 'revisions', :locals => {:project => @project, :path => @path, :revisions => @changesets, :entry => @entry }%>
trunk/app/views/repositories/entry.rhtml
2 2

  
3 3
<div class="autoscroll">
4 4
<table class="filecontent CodeRay">
5
  <thead>
6
    <tr>
7
      <th colspan="2" class="filename"><%= @path %></th>
8
    </tr>
9
  </thead>
10 5
  <tbody>
11 6
    <% line_num = 1 %>
12 7
    <% syntax_highlight(@path, to_utf8(@content)).each_line do |line| %>
trunk/config/routes.rb
24 24
    omap.repositories_changes 'repositories/changes/:id/*path', :action => 'changes'
25 25
    omap.repositories_diff 'repositories/diff/:id/*path', :action => 'diff'
26 26
    omap.repositories_entry 'repositories/entry/:id/*path', :action => 'entry'
27
    omap.repositories_entry 'repositories/annotate/:id/*path', :action => 'annotate'
27 28
  end
28 29
  
29 30
  # Allow downloading Web Service WSDL as a file with an extension
trunk/lang/bg.yml
547 547
field_time_zone: Time zone
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/cs.yml
547 547
field_time_zone: Time zone
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/de.yml
547 547
field_time_zone: Zeitzone
548 548
text_caracters_minimum: Muss mindestens %d Zeichen lang sein.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/en.yml
489 489
button_rename: Rename
490 490
button_change_password: Change password
491 491
button_copy: Copy
492
button_annotate: Annotate
492 493

  
493 494
status_active: active
494 495
status_registered: registered
trunk/lang/es.yml
550 550
notice_account_pending: "Su cuenta ha sido creada y está pendiende de la aprobación por parte de administrador"
551 551
setting_time_format: Formato de hora
552 552
setting_bcc_recipients: Blind carbon copy recipients (bcc)
553
button_annotate: Annotate
trunk/lang/fr.yml
489 489
button_rename: Renommer
490 490
button_change_password: Changer de mot de passe
491 491
button_copy: Copier
492
button_annotate: Annoter
492 493

  
493 494
status_active: actif
494 495
status_registered: enregistré
trunk/lang/he.yml
547 547
field_time_zone: Time zone
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/it.yml
547 547
field_time_zone: Time zone
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/ja.yml
548 548
field_time_zone: Time zone
549 549
text_caracters_minimum: Must be at least %d characters long.
550 550
setting_bcc_recipients: Blind carbon copy recipients (bcc)
551
button_annotate: Annotate
trunk/lang/ko.yml
547 547
field_time_zone: Time zone
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/nl.yml
548 548
field_time_zone: Time zone
549 549
text_caracters_minimum: Must be at least %d characters long.
550 550
setting_bcc_recipients: Blind carbon copy recipients (bcc)
551
button_annotate: Annotate
trunk/lang/pl.yml
547 547
field_time_zone: Strefa czasowa
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/pt-br.yml
547 547
field_time_zone: Time zone
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/pt.yml
547 547
field_time_zone: Time zone
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/ro.yml
547 547
field_time_zone: Time zone
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/ru.yml
547 547
field_time_zone: Часовой пояс
548 548
text_caracters_minimum: Must be at least %d characters long.
549 549
setting_bcc_recipients: Blind carbon copy recipients (bcc)
550
button_annotate: Annotate
trunk/lang/sr.yml
548 548
field_time_zone: Time zone
549 549
text_caracters_minimum: Must be at least %d characters long.
550 550
setting_bcc_recipients: Blind carbon copy recipients (bcc)
551
button_annotate: Annotate
trunk/lang/sv.yml
548 548
field_time_zone: Time zone
549 549
text_caracters_minimum: Must be at least %d characters long.
550 550
setting_bcc_recipients: Blind carbon copy recipients (bcc)
551
button_annotate: Annotate
trunk/lang/zh.yml
550 550
field_time_zone: Time zone
551 551
text_caracters_minimum: Must be at least %d characters long.
552 552
setting_bcc_recipients: Blind carbon copy recipients (bcc)
553
button_annotate: Annotate
trunk/lib/redmine/scm/adapters/abstract_adapter.rb
38 38
        def supports_cat?
39 39
          true
40 40
        end
41

  
42
        def supports_annotate?
43
          respond_to?('annotate')
44
        end
41 45
        
42 46
        def root_url
43 47
          @root_url
......
76 80
        def cat(path, identifier=nil)
77 81
          return nil
78 82
        end
79

  
83
        
80 84
        def with_leading_slash(path)
81 85
          path ||= ''
82 86
          (path[0,1]!="/") ? "/#{path}" : path
......
237 241
    
238 242
        # Initialize with a Diff file and the type of Diff View
239 243
        # The type view must be inline or sbs (side_by_side)
240
        def initialize (type="inline")
244
        def initialize(type="inline")
241 245
          @parsing = false
242 246
          @nb_line = 1
243 247
          @start = false
......
312 316
            CGI.escapeHTML(line)
313 317
        end
314 318
    
315
        def parse_line (line, type="inline")
319
        def parse_line(line, type="inline")
316 320
          if line[0, 1] == "+"
317 321
            diff = sbs? type, 'add'
318 322
            @before = 'add'
......
348 352
          end
349 353
        end
350 354
      end
355
    
356
      class Annotate
357
        attr_reader :lines, :revisions
358
        
359
        def initialize
360
          @lines = []
361
          @revisions = []
362
        end
363
        
364
        def add_line(line, revision)
365
          @lines << line
366
          @revisions << revision
367
        end
368
        
369
        def content
370
          content = lines.join("\n")
371
        end
372
        
373
        def empty?
374
          lines.empty?
375
        end
376
      end
351 377
    end
352 378
  end
353 379
end
trunk/lib/redmine/scm/adapters/cvs_adapter.rb
268 268
        rescue Errno::ENOENT => e
269 269
          raise CommandFailed    
270 270
        end  
271
        
271

  
272
        def annotate(path, identifier=nil)
273
          identifier = (identifier) ? identifier : "HEAD"
274
          logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
275
          path_with_project="#{url}#{with_leading_slash(path)}"
276
          cmd = "#{CVS_BIN} -d #{root_url} rannotate -r#{identifier} #{path_with_project}"
277
          blame = Annotate.new
278
          shellout(cmd) do |io|
279
            io.each_line do |line|
280
              next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
281
              blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
282
            end
283
          end
284
          return nil if $? && $?.exitstatus != 0
285
          blame
286
        rescue Errno::ENOENT => e
287
          raise CommandFailed    
288
        end
289
         
272 290
        private
273 291

  
274 292
        # convert a date/time into the CVS-format
trunk/lib/redmine/scm/adapters/mercurial_adapter.rb
157 157
        rescue Errno::ENOENT => e
158 158
          raise CommandFailed
159 159
        end
160
        
161
        def annotate(path, identifier=nil)
162
          path ||= ''
163
          cmd = "#{HG_BIN} -R #{target('')}"
164
          cmd << " annotate -n -u"
165
          cmd << " -r #{identifier.to_i}" if identifier
166
          cmd << " #{target(path)}"
167
          blame = Annotate.new
168
          shellout(cmd) do |io|
169
            io.each_line do |line|
170
              next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
171
              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
172
            end
173
          end
174
          return nil if $? && $?.exitstatus != 0
175
          blame
176
        rescue Errno::ENOENT => e
177
          raise CommandFailed
178
        end
160 179
      end
161 180
    end
162 181
  end
trunk/lib/redmine/scm/adapters/subversion_adapter.rb
173 173
          raise CommandFailed    
174 174
        end
175 175
        
176
        def annotate(path, identifier=nil)
177
          identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
178
          cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
179
          cmd << credentials_string
180
          blame = Annotate.new
181
          shellout(cmd) do |io|
182
            io.each_line do |line|
183
              next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
184
              blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
185
            end
186
          end
187
          return nil if $? && $?.exitstatus != 0
188
          blame
189
        rescue Errno::ENOENT => e
190
          raise CommandFailed
191
        end
192
        
176 193
        private
177 194
        
178 195
        def credentials_string
trunk/lib/redmine.rb
76 76
    
77 77
  map.project_module :repository do |map|
78 78
    map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
79
    map.permission :browse_repository, :repositories => [:show, :browse, :entry, :changes, :diff, :stats, :graph]
79
    map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
80 80
    map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
81 81
  end
82 82

  
trunk/public/stylesheets/scm.css
2 2
table.filecontent { border: 1px solid #ccc;  border-collapse: collapse; width:98%; }
3 3
table.filecontent th { border: 1px solid #ccc; background-color: #eee; }
4 4
table.filecontent th.filename {	background-color: #ddc;	text-align: left; }
5
div.action_M { background: #fd8 }
6
div.action_D { background: #f88 }
7
div.action_A { background: #bfb }
8

  
9 5
table.filecontent tr.spacing { border: 1px solid #d7d7d7; }
10

  
11
table.filecontent .line-num {
6
table.filecontent th.line-num {
12 7
    border: 1px solid #d7d7d7;
13 8
	font-size: 0.8em;
14 9
	text-align: right;
15
	width: 3em;
10
	width: 2%;
16 11
	padding-right: 3px;
17 12
}
18 13

  
14
/* 12 different colors for the annonate view */
15
table.annotate tr.bloc-0 {background: #FFFFBF;}
16
table.annotate tr.bloc-1 {background: #EABFFF;}
17
table.annotate tr.bloc-2 {background: #BFFFFF;}
18
table.annotate tr.bloc-3 {background: #FFD9BF;}
19
table.annotate tr.bloc-4 {background: #E6FFBF;}
20
table.annotate tr.bloc-5 {background: #BFCFFF;}
21
table.annotate tr.bloc-6 {background: #FFBFEF;}
22
table.annotate tr.bloc-7 {background: #FFE6BF;}
23
table.annotate tr.bloc-8 {background: #FFE680;}
24
table.annotate tr.bloc-9 {background: #AA80FF;}
25
table.annotate tr.bloc-10 {background: #FFBFDC;}
26
table.annotate tr.bloc-11 {background: #BFE4FF;}
27

  
28
table.annotate td.revision {
29
    text-align: center;
30
    width: 2%;
31
    padding-left: 1em;
32
    background: inherit;
33
}
34
    
35
table.annotate td.author {
36
    text-align: center;
37
    border-right: 1px solid #d7d7d7;
38
    white-space: nowrap;
39
    padding-left: 1em;
40
    padding-right: 1em;
41
    width: 3%;
42
    background: inherit;
43
}
44

  
45
table.annotate td.line-code { background-color: #fafafa; }
46

  
47
div.action_M { background: #fd8 }
48
div.action_D { background: #f88 }
49
div.action_A { background: #bfb }
50

  
19 51
/************* Coderay styles *************/
20 52

  
21 53
table.CodeRay {

Also available in: Unified diff