Mercurial > hg > soundsoftware-site
changeset 15:9c6c72729d91 yuya
* Merge from default (for SVN trunk update)
author | Chris Cannam |
---|---|
date | Wed, 25 Aug 2010 16:33:28 +0100 |
parents | 80433603a2cd (diff) 1d32c0a0efbf (current diff) |
children | 020926a36823 |
files | app/helpers/application_helper.rb app/models/changeset.rb app/views/issues/.svn/prop-base/auto_complete.html.erb.svn-base app/views/issues/.svn/prop-base/changes.rxml.svn-base app/views/issues/.svn/prop-base/context_menu.rhtml.svn-base app/views/issues/.svn/prop-base/move.rhtml.svn-base app/views/issues/.svn/prop-base/preview.html.erb.svn-base app/views/issues/.svn/text-base/auto_complete.html.erb.svn-base app/views/issues/.svn/text-base/changes.rxml.svn-base app/views/issues/.svn/text-base/context_menu.rhtml.svn-base app/views/issues/.svn/text-base/move.rhtml.svn-base app/views/issues/.svn/text-base/preview.html.erb.svn-base app/views/issues/auto_complete.html.erb app/views/issues/changes.rxml app/views/issues/context_menu.rhtml app/views/issues/move.rhtml app/views/issues/preview.html.erb config/locales/.svn/text-base/sr-CY.yml.svn-base config/locales/sr-CY.yml public/javascripts/calendar/lang/.svn/text-base/calendar-sr-CY.js.svn-base public/javascripts/calendar/lang/calendar-sr-CY.js |
diffstat | 20 files changed, 868 insertions(+), 225 deletions(-) [+] |
line wrap: on
line diff
--- a/app/helpers/application_helper.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/app/helpers/application_helper.rb Wed Aug 25 16:33:28 2010 +0100 @@ -99,8 +99,10 @@ # * :text - Link text (default to the formatted revision) def link_to_revision(revision, project, options={}) text = options.delete(:text) || format_revision(revision) + rev = revision.respond_to?(:identifier) ? revision.identifier : revision - link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision)) + link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev}, + :title => l(:label_revision_id, format_revision(revision))) end # Generates a link to a project if active @@ -641,7 +643,7 @@ 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, :rev => changeset.revision}, + link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier}, :class => 'changeset', :title => truncate_single_line(changeset.comments, :length => 100) end
--- a/app/helpers/repositories_helper.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/app/helpers/repositories_helper.rb Wed Aug 25 16:33:28 2010 +0100 @@ -18,9 +18,24 @@ require 'iconv' module RepositoriesHelper - def format_revision(txt) - txt.to_s[0,8] + # truncate rev to 8 chars if it's quite long + def truncate_long_revision_name(rev) + rev.to_s.size <= 12 ? rev.to_s : rev.to_s[0, 8] end + private :truncate_long_revision_name + + def format_revision(revision) + if [:identifier, :revision, :scmid].all? { |e| revision.respond_to? e } + if revision.scmid and revision.revision != revision.scmid and /[^\d]/ !~ revision.revision + "#{revision.revision}:#{revision.scmid}" # number:hashid + else + truncate_long_revision_name(revision.identifier) + end + else + truncate_long_revision_name(revision) + end + end + module_function :format_revision # callable as RepositoriesHelper.format_revision def truncate_at_line_break(text, length = 255) if text @@ -87,7 +102,7 @@ :action => 'show', :id => @project, :path => path_param, - :rev => @changeset.revision) + :rev => @changeset.identifier) output << "<li class='#{style}'>#{text}</li>" output << render_changes_tree(s) elsif c = tree[file][:c] @@ -97,13 +112,13 @@ :action => 'entry', :id => @project, :path => path_param, - :rev => @changeset.revision) unless c.action == 'D' + :rev => @changeset.identifier) unless c.action == 'D' text << " - #{c.revision}" unless c.revision.blank? text << ' (' + link_to('diff', :controller => 'repositories', :action => 'diff', :id => @project, :path => path_param, - :rev => @changeset.revision) + ') ' if c.action == 'M' + :rev => @changeset.identifier) + ') ' if c.action == 'M' text << ' ' + content_tag('span', c.from_path, :class => 'copied-from') unless c.from_path.blank? output << "<li class='#{style}'>#{text}</li>" end
--- a/app/models/changeset.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/app/models/changeset.rb Wed Aug 25 16:33:28 2010 +0100 @@ -23,10 +23,10 @@ has_many :changes, :dependent => :delete_all has_and_belongs_to_many :issues - acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.revision}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, + acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{RepositoriesHelper.format_revision(o)}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))}, :description => :long_comments, :datetime => :committed_on, - :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}} + :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}} acts_as_searchable :columns => 'comments', :include => {:repository => :project}, @@ -47,6 +47,22 @@ def revision=(r) write_attribute :revision, (r.nil? ? nil : r.to_s) end + + # Returns the identifier of this changeset. + # e.g. revision number for centralized system; hash id for DVCS + def identifier + scmid || revision + end + + # Returns the wiki identifier, "rN" or "commit:ABCDEF" + def wiki_identifier + if scmid # hash-like + "commit:#{scmid}" + else # numeric + "r#{revision}" + end + end + private :wiki_identifier def comments=(comment) write_attribute(:comments, Changeset.normalize_comments(comment)) @@ -108,11 +124,7 @@ issue.reload # don't change the status is the issue is closed next if issue.status.is_closed? - csettext = "r#{self.revision}" - if self.scmid && (! (csettext =~ /^r[0-9]+$/)) - csettext = "commit:\"#{self.scmid}\"" - end - journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, csettext)) + journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, wiki_identifier)) issue.status = fix_status unless Setting.commit_fix_done_ratio.blank? issue.done_ratio = Setting.commit_fix_done_ratio.to_i
--- a/app/models/issue.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/app/models/issue.rb Wed Aug 25 16:33:28 2010 +0100 @@ -27,7 +27,7 @@ has_many :journals, :as => :journalized, :dependent => :destroy has_many :time_entries, :dependent => :delete_all - has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" + has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.id ASC" has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
--- a/app/models/repository.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/app/models/repository.rb Wed Aug 25 16:33:28 2010 +0100 @@ -17,7 +17,7 @@ class Repository < ActiveRecord::Base belongs_to :project - has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" + has_many :changesets, :order => "#{Changeset.table_name}.id DESC" has_many :changes, :through => :changesets # Raw SQL to delete changesets and changes in the database @@ -94,7 +94,10 @@ # Finds and returns a revision with a number or the beginning of a hash def find_changeset_by_name(name) - changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%'])) + # TODO: is this query efficient enough? can we write as single query? + e = changesets.find(:first, :conditions => ['revision = ? OR scmid = ?', name.to_s, name.to_s]) + return e if e + changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"]) end def latest_changeset @@ -106,12 +109,11 @@ def latest_changesets(path, rev, limit=10) if path.blank? changesets.find(:all, :include => :user, - :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", :limit => limit) else changes.find(:all, :include => {:changeset => :user}, :conditions => ["path = ?", path.with_leading_slash], - :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", + :order => "#{Changeset.table_name}.id DESC", :limit => limit).collect(&:changeset) end end @@ -172,7 +174,11 @@ def self.fetch_changesets Project.active.has_module(:repository).find(:all, :include => :repository).each do |project| if project.repository - project.repository.fetch_changesets + begin + project.repository.fetch_changesets + rescue Redmine::Scm::Adapters::CommandFailed => e + logger.error "Repository: error during fetching changesets: #{e.message}" + end end end end
--- a/app/models/repository/git.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/app/models/repository/git.rb Wed Aug 25 16:33:28 2010 +0100 @@ -49,7 +49,7 @@ c = changesets.find(:first, :order => 'committed_on DESC') since = (c ? c.committed_on - 7.days : nil) - revisions = scm.revisions('', nil, nil, :all => true, :since => since) + revisions = scm.revisions('', nil, nil, :all => true, :since => since, :reverse => true) return if revisions.nil? || revisions.empty? recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since]) @@ -75,7 +75,7 @@ "scmid IN (?)", revisions.map!{|c| c.scmid} ], - :order => 'committed_on DESC' + :order => 'id DESC' ) end end
--- a/app/models/repository/mercurial.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/app/models/repository/mercurial.rb Wed Aug 25 16:33:28 2010 +0100 @@ -21,6 +21,8 @@ attr_protected :root_url validates_presence_of :url + FETCH_AT_ONCE = 100 # number of changesets to fetch at once + def scm_adapter Redmine::Scm::Adapters::MercurialAdapter end @@ -30,61 +32,64 @@ end def entries(path=nil, identifier=nil) - entries=scm.entries(path, identifier) - if entries - entries.each do |entry| - next unless entry.is_file? - # Set the filesize unless browsing a specific revision - if identifier.nil? - full_path = File.join(root_url, entry.path) - entry.size = File.stat(full_path).size if File.file?(full_path) - end - # Search the DB for the entry's last change - change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC") - if change - entry.lastrev.identifier = change.changeset.revision - entry.lastrev.name = change.changeset.revision - entry.lastrev.author = change.changeset.committer - entry.lastrev.revision = change.revision + scm.entries(path, identifier) + end + + def branches + bras = scm.branches + bras.sort unless bras == %w|default| + end + + # Returns the latest changesets for +path+ + def latest_changesets(path, rev, limit=10) + changesets.find(:all, :include => :user, + :conditions => latest_changesets_cond(path, rev, limit), + :limit => limit) + end + + def latest_changesets_cond(path, rev, limit) + cond, args = [], [] + + if scm.branchmap.member? rev + # dirty hack to filter by branch. branch name should be in database. + cond << "#{Changeset.table_name}.scmid IN (?)" + args << scm.nodes_in_branch(rev, path, rev, 0, :limit => limit) + elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil + cond << "#{Changeset.table_name}.id <= ?" + args << last.id + end + + unless path.blank? + # TODO: there must be a better way to build sub-query + cond << "EXISTS (SELECT * FROM #{Change.table_name} + WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id + AND (#{Change.table_name}.path = ? OR #{Change.table_name}.path LIKE ?))" + args << path.with_leading_slash << "#{path.with_leading_slash}/%" + end + + [cond.join(' AND '), *args] unless cond.empty? + end + private :latest_changesets_cond + + def fetch_changesets + scm_rev = scm.info.lastrev.revision.to_i + db_rev = latest_changeset ? latest_changeset.revision.to_i : -1 + return unless db_rev < scm_rev # already up-to-date + + logger.debug "Fetching changesets for repository #{url}" if logger + (db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i| + transaction do + scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re| + cs = Changeset.create(:repository => self, + :revision => re.revision, + :scmid => re.scmid, + :committer => re.author, + :committed_on => re.time, + :comments => re.message) + re.paths.each { |e| cs.create_change(e) } end end end - entries - end - - def fetch_changesets - scm_info = scm.info - if scm_info - # latest revision found in database - db_revision = latest_changeset ? latest_changeset.revision.to_i : -1 - # latest revision in the repository - latest_revision = scm_info.lastrev - return if latest_revision.nil? - scm_revision = latest_revision.identifier.to_i - if db_revision < scm_revision - logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug? - identifier_from = db_revision + 1 - while (identifier_from <= scm_revision) - # loads changesets by batches of 100 - identifier_to = [identifier_from + 99, scm_revision].min - revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true) - transaction do - revisions.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| - changeset.create_change(change) - end - end - end unless revisions.nil? - identifier_from = identifier_to + 1 - end - end - end + self end end
--- a/app/views/repositories/_dir_list_content.rhtml Wed Aug 25 16:30:24 2010 +0100 +++ b/app/views/repositories/_dir_list_content.rhtml Wed Aug 25 16:33:28 2010 +0100 @@ -17,7 +17,7 @@ </td> <td class="size"><%= (entry.size ? number_to_human_size(entry.size) : "?") unless entry.is_dir? %></td> <% changeset = @project.repository.changesets.find_by_revision(entry.lastrev.identifier) if entry.lastrev && entry.lastrev.identifier %> -<td class="revision"><%= link_to_revision(changeset.revision, @project) if changeset %></td> +<td class="revision"><%= link_to_revision(changeset, @project) if changeset %></td> <td class="age"><%= distance_of_time_in_words(entry.lastrev.time, Time.now) if entry.lastrev && entry.lastrev.time %></td> <td class="author"><%= changeset.nil? ? h(entry.lastrev.author.to_s.split('<').first) : changeset.author if entry.lastrev %></td> <td class="comments"><%=h truncate(changeset.comments, :length => 50) unless changeset.nil? %></td>
--- a/app/views/repositories/_revisions.rhtml Wed Aug 25 16:30:24 2010 +0100 +++ b/app/views/repositories/_revisions.rhtml Wed Aug 25 16:33:28 2010 +0100 @@ -13,9 +13,9 @@ <% line_num = 1 %> <% revisions.each do |changeset| %> <tr class="changeset <%= cycle 'odd', 'even' %>"> -<td class="id"><%= link_to_revision(changeset.revision, project) %></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="id"><%= link_to_revision(changeset, project) %></td> +<td class="checkbox"><%= radio_button_tag('rev', changeset.identifier, (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.identifier, (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> <td class="author"><%=h changeset.author %></td> <td class="comments"><%= textilizable(truncate_at_line_break(changeset.comments)) %></td>
--- a/app/views/repositories/annotate.rhtml Wed Aug 25 16:30:24 2010 +0100 +++ b/app/views/repositories/annotate.rhtml Wed Aug 25 16:33:28 2010 +0100 @@ -19,7 +19,7 @@ <tr class="bloc-<%= revision.nil? ? 0 : colors[revision.identifier || revision.revision] %>"> <th class="line-num" id="L<%= line_num %>"><a href="#L<%= line_num %>"><%= line_num %></a></th> <td class="revision"> - <%= (revision.identifier ? link_to(format_revision(revision.identifier), :action => 'revision', :id => @project, :rev => revision.identifier) : format_revision(revision.revision)) if revision %></td> + <%= (revision.identifier ? link_to_revision(revision, @project) : format_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>
--- a/app/views/repositories/revision.rhtml Wed Aug 25 16:30:24 2010 +0100 +++ b/app/views/repositories/revision.rhtml Wed Aug 25 16:33:28 2010 +0100 @@ -1,25 +1,25 @@ <div class="contextual"> « <% unless @changeset.previous.nil? -%> - <%= link_to_revision(@changeset.previous.revision, @project, :text => l(:label_previous)) %> + <%= link_to_revision(@changeset.previous, @project, :text => l(:label_previous)) %> <% else -%> <%= l(:label_previous) %> <% end -%> | <% unless @changeset.next.nil? -%> - <%= link_to_revision(@changeset.next.revision, @project, :text => l(:label_next)) %> + <%= link_to_revision(@changeset.next, @project, :text => l(:label_next)) %> <% else -%> <%= l(:label_next) %> <% end -%> » <% form_tag({:controller => 'repositories', :action => 'revision', :id => @project, :rev => nil}, :method => :get) do %> - <%= text_field_tag 'rev', @rev[0,8], :size => 8 %> + <%= text_field_tag 'rev', @rev, :size => 8 %> <%= submit_tag 'OK', :name => nil %> <% end %> </div> -<h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2> +<h2><%= l(:label_revision) %> <%= format_revision(@changeset) %></h2> <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %> <span class="author"><%= authoring(@changeset.committed_on, @changeset.author) %></span></p> @@ -45,7 +45,7 @@ <li class="change change-D"><%= l(:label_deleted) %></li> </ul> -<p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.revision) if @changeset.changes.any? %></p> +<p><%= link_to(l(:label_view_diff), :action => 'diff', :id => @project, :path => "", :rev => @changeset.identifier) if @changeset.changes.any? %></p> <div class="changeset-changes"> <%= render_changeset_changes %>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/mercurial/redminehelper.py Wed Aug 25 16:33:28 2010 +0100 @@ -0,0 +1,91 @@ +# redminehelper: Redmine helper extension for Mercurial +# it's a draft to show a possible way to explore repository by the Redmine overhaul patch +# see: http://www.redmine.org/issues/4455 +# +# Copyright 2010 Alessio Franceschelli (alefranz.net) +# Copyright 2010 Yuya Nishihara <yuya@tcha.org> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''command to list revision of each file +''' + +import re, time +from mercurial import cmdutil, commands, node, error + +SPECIAL_TAGS = ('tip',) + +def rhsummary(ui, repo, **opts): + """output the summary of the repository""" + # see mercurial/commands.py:tip + ui.write(':tip: rev node\n') + tipctx = repo[len(repo) - 1] + ui.write('%d %s\n' % (tipctx.rev(), tipctx)) + + # see mercurial/commands.py:root + ui.write(':root: path\n') + ui.write(repo.root + '\n') + + # see mercurial/commands.py:tags + ui.write(':tags: rev node name\n') + for t, n in reversed(repo.tagslist()): + if t in SPECIAL_TAGS: + continue + try: + r = repo.changelog.rev(n) + except error.LookupError: + r = -1 + ui.write('%d %s %s\n' % (r, node.short(n), t)) + + # see mercurial/commands.py:branches + def iterbranches(): + for t, n in repo.branchtags().iteritems(): + yield t, n, repo.changelog.rev(n) + + ui.write(':branches: rev node name\n') + for t, n, r in sorted(iterbranches(), key=lambda e: e[2], reverse=True): + if repo.lookup(r) in repo.branchheads(t, closed=False): + ui.write('%d %s %s\n' % (r, node.short(n), t)) # only open branch + +def rhentries(ui, repo, path='', **opts): + """output the entries of the specified directory""" + rev = opts.get('rev') + pathprefix = (path.rstrip('/') + '/').lstrip('/') + + # TODO: clean up + dirs, files = {}, {} + mf = repo[rev].manifest() + for f in repo[rev]: + if not f.startswith(pathprefix): + continue + + name = re.sub(r'/.*', '', f[len(pathprefix):]) + if '/' in f[len(pathprefix):]: + dirs[name] = (name,) + else: + try: + fctx = repo.filectx(f, fileid=mf[f]) + ctx = fctx.changectx() + tm, tzoffset = ctx.date() + localtime = int(tm) + tzoffset - time.timezone + files[name] = (ctx.rev(), node.short(ctx.node()), localtime, + fctx.size(), name) + except LookupError: # TODO: when this occurs? + pass + + ui.write(':dirs: name\n') + for n, v in sorted(dirs.iteritems(), key=lambda e: e[0]): + ui.write(' '.join(v) + '\n') + + ui.write(':files: rev node time size name\n') + for n, v in sorted(files.iteritems(), key=lambda e: e[0]): + ui.write(' '.join(str(e) for e in v) + '\n') + + +cmdtable = { + 'rhsummary': (rhsummary, [], 'hg rhsummary'), + 'rhentries': (rhentries, + [('r', 'rev', '', 'show the specified revision')], + 'hg rhentries [path]'), +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/svn/SoundSoftware.pm Wed Aug 25 16:33:28 2010 +0100 @@ -0,0 +1,422 @@ +package Apache::Authn::SoundSoftware; + +=head1 Apache::Authn::SoundSoftware + +SoundSoftware - a mod_perl module for Apache authentication against a +Redmine database and optional LDAP implementing the access control +rules required for the SoundSoftware.ac.uk repository site. + +=head1 SYNOPSIS + +This module is closely based on the Redmine.pm authentication module +provided with Redmine. It is intended to be used for authentication +in front of a repository service such as hgwebdir. + +Requirements: + +1. Clone/pull from repo for public project: Any user, no +authentication required + +2. Clone/pull from repo for private project: Project members only + +3. Push to repo for public project: "Permitted" users only (this +probably means project members who are also identified in the hgrc web +section for the repository and so will be approved by hgwebdir?) + +4. Push to repo for private project: "Permitted" users only (as above) + +=head1 INSTALLATION + +Debian/ubuntu: + + apt-get install libapache-dbi-perl libapache2-mod-perl2 \ + libdbd-mysql-perl libauthen-simple-ldap-perl libio-socket-ssl-perl + +Note that LDAP support is hardcoded "on" in this script (it is +optional in the original Redmine.pm). + +=head1 CONFIGURATION + + ## This module has to be in your perl path + ## eg: /usr/local/lib/site_perl/Apache/Authn/SoundSoftware.pm + PerlLoadModule Apache::Authn::SoundSoftware + + # Example when using hgwebdir + ScriptAlias / "/var/hg/hgwebdir.cgi/" + + <Location /> + AuthName "Mercurial" + AuthType Basic + Require valid-user + PerlAccessHandler Apache::Authn::SoundSoftware::access_handler + PerlAuthenHandler Apache::Authn::SoundSoftware::authen_handler + SoundSoftwareDSN "DBI:mysql:database=redmine;host=localhost" + SoundSoftwareDbUser "redmine" + SoundSoftwareDbPass "password" + Options +ExecCGI + AddHandler cgi-script .cgi + ## Optional where clause (fulltext search would be slow and + ## database dependant). + # SoundSoftwareDbWhereClause "and members.role_id IN (1,2)" + ## Optional prefix for local repository URLs + # SoundSoftwareRepoPrefix "/var/hg/" + </Location> + +See the original Redmine.pm for further configuration notes. + +=cut + +use strict; +use warnings FATAL => 'all', NONFATAL => 'redefine'; + +use DBI; +use Digest::SHA1; +use Authen::Simple::LDAP; +use Apache2::Module; +use Apache2::Access; +use Apache2::ServerRec qw(); +use Apache2::RequestRec qw(); +use Apache2::RequestUtil qw(); +use Apache2::Const qw(:common :override :cmd_how); +use APR::Pool (); +use APR::Table (); + +my @directives = ( + { + name => 'SoundSoftwareDSN', + req_override => OR_AUTHCFG, + args_how => TAKE1, + errmsg => 'Dsn in format used by Perl DBI. eg: "DBI:Pg:dbname=databasename;host=my.db.server"', + }, + { + name => 'SoundSoftwareDbUser', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, + { + name => 'SoundSoftwareDbPass', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, + { + name => 'SoundSoftwareDbWhereClause', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, + { + name => 'SoundSoftwareRepoPrefix', + req_override => OR_AUTHCFG, + args_how => TAKE1, + }, +); + +sub SoundSoftwareDSN { + my ($self, $parms, $arg) = @_; + $self->{SoundSoftwareDSN} = $arg; + my $query = "SELECT + hashed_password, auth_source_id, permissions + FROM members, projects, users, roles, member_roles + WHERE + projects.id=members.project_id + AND member_roles.member_id=members.id + AND users.id=members.user_id + AND roles.id=member_roles.role_id + AND users.status=1 + AND login=? + AND identifier=? "; + $self->{SoundSoftwareQuery} = trim($query); +} + +sub SoundSoftwareDbUser { set_val('SoundSoftwareDbUser', @_); } +sub SoundSoftwareDbPass { set_val('SoundSoftwareDbPass', @_); } +sub SoundSoftwareDbWhereClause { + my ($self, $parms, $arg) = @_; + $self->{SoundSoftwareQuery} = trim($self->{SoundSoftwareQuery}.($arg ? $arg : "")." "); +} + +sub SoundSoftwareRepoPrefix { + my ($self, $parms, $arg) = @_; + if ($arg) { + $self->{SoundSoftwareRepoPrefix} = $arg; + } +} + +sub trim { + my $string = shift; + $string =~ s/\s{2,}/ /g; + return $string; +} + +sub set_val { + my ($key, $self, $parms, $arg) = @_; + $self->{$key} = $arg; +} + +Apache2::Module::add(__PACKAGE__, \@directives); + + +my %read_only_methods = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/; + +sub access_handler { + my $r = shift; + + print STDERR "SoundSoftware.pm: In access handler\n"; + + unless ($r->some_auth_required) { + $r->log_reason("No authentication has been configured"); + return FORBIDDEN; + } + + my $method = $r->method; + + print STDERR "SoundSoftware.pm: Method: $method, uri " . $r->uri . ", location " . $r->location . "\n"; + print STDERR "SoundSoftware.pm: Accept: " . $r->headers_in->{Accept} . "\n"; + + if (!defined $read_only_methods{$method}) { + print STDERR "SoundSoftware.pm: Method is not read-only, authentication handler required\n"; + return OK; + } + + my $dbh = connect_database($r); + + my $project_id = get_project_identifier($dbh, $r); + my $status = get_project_status($dbh, $project_id, $r); + + $dbh->disconnect(); + undef $dbh; + + if ($status == 0) { # nonexistent + print STDERR "SoundSoftware.pm: Project does not exist, refusing access\n"; + return FORBIDDEN; + } elsif ($status == 1) { # public + print STDERR "SoundSoftware.pm: Project is public, no restriction here\n"; + $r->set_handlers(PerlAuthenHandler => [\&OK]) + } else { # private + print STDERR "SoundSoftware.pm: Project is private, authentication handler required\n"; + } + + return OK +} + +sub authen_handler { + my $r = shift; + + print STDERR "SoundSoftware.pm: In authentication handler\n"; + + my $dbh = connect_database($r); + + my $project_id = get_project_identifier($dbh, $r); + my $realm = get_realm($dbh, $project_id, $r); + $r->auth_name($realm); + + my ($res, $redmine_pass) = $r->get_basic_auth_pw(); + unless ($res == OK) { + $dbh->disconnect(); + undef $dbh; + return $res; + } + + print STDERR "SoundSoftware.pm: User is " . $r->user . ", got password\n"; + + my $permitted = is_permitted($dbh, $project_id, $r->user, $redmine_pass, $r); + + $dbh->disconnect(); + undef $dbh; + + if ($permitted) { + return OK; + } else { + print STDERR "SoundSoftware.pm: Not permitted\n"; + $r->note_auth_failure(); + return AUTH_REQUIRED; + } +} + +sub get_project_status { + my $dbh = shift; + my $project_id = shift; + my $r = shift; + + if (!defined $project_id or $project_id eq '') { + return 0; # nonexistent + } + + my $sth = $dbh->prepare( + "SELECT is_public FROM projects WHERE projects.identifier = ?;" + ); + + $sth->execute($project_id); + my $ret = 0; # nonexistent + if (my @row = $sth->fetchrow_array) { + if ($row[0] eq "1" || $row[0] eq "t") { + $ret = 1; # public + } else { + $ret = 2; # private + } + } + $sth->finish(); + undef $sth; + + $ret; +} + +sub is_permitted { + my $dbh = shift; + my $project_id = shift; + my $redmine_user = shift; + my $redmine_pass = shift; + my $r = shift; + + my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass); + + my $cfg = Apache2::Module::get_config + (__PACKAGE__, $r->server, $r->per_dir_config); + + my $query = $cfg->{SoundSoftwareQuery}; + my $sth = $dbh->prepare($query); + $sth->execute($redmine_user, $project_id); + + my $ret; + while (my ($hashed_password, $auth_source_id, $permissions) = $sth->fetchrow_array) { + + # Test permissions for this user before we verify credentials + # -- if the user is not permitted this action anyway, there's + # not much point in e.g. contacting the LDAP + + my $method = $r->method; + + if ((defined $read_only_methods{$method} && $permissions =~ /:browse_repository/) + || $permissions =~ /:commit_access/) { + + # User would be permitted this action, if their + # credentials checked out -- test those now + + print STDERR "SoundSoftware.pm: User $redmine_user has required role, checking credentials\n"; + + unless ($auth_source_id) { + if ($hashed_password eq $pass_digest) { + print STDERR "SoundSoftware.pm: User $redmine_user authenticated via password\n"; + $ret = 1; + last; + } + } else { + my $sthldap = $dbh->prepare( + "SELECT host,port,tls,account,account_password,base_dn,attr_login FROM auth_sources WHERE id = ?;" + ); + $sthldap->execute($auth_source_id); + while (my @rowldap = $sthldap->fetchrow_array) { + my $ldap = Authen::Simple::LDAP->new( + host => ($rowldap[2] eq "1" || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0], + port => $rowldap[1], + basedn => $rowldap[5], + binddn => $rowldap[3] ? $rowldap[3] : "", + bindpw => $rowldap[4] ? $rowldap[4] : "", + filter => "(".$rowldap[6]."=%s)" + ); + if ($ldap->authenticate($redmine_user, $redmine_pass)) { + print STDERR "SoundSoftware.pm: User $redmine_user authenticated via LDAP\n"; + $ret = 1; + } + } + $sthldap->finish(); + undef $sthldap; + } + } else { + print STDERR "SoundSoftware.pm: User $redmine_user lacks required role for this project\n"; + } + } + + $sth->finish(); + undef $sth; + + $ret; +} + +sub get_project_identifier { + my $dbh = shift; + my $r = shift; + + my $location = $r->location; + my ($repo) = $r->uri =~ m{$location/*([^/]+)}; + + return $repo if (!$repo); + + $repo =~ s/[^a-zA-Z0-9\._-]//g; + + # The original Redmine.pm returns the string just calculated as + # the project identifier. That won't do for us -- we may have + # (and in fact already do have, in our test instance) projects + # whose repository names differ from the project identifiers. + + # This is a rather fundamental change because it means that almost + # every request needs more than one database query -- which + # prompts us to start passing around $dbh instead of connecting + # locally within each function as is done in Redmine.pm. + + my $sth = $dbh->prepare( + "SELECT projects.identifier FROM projects, repositories WHERE repositories.project_id = projects.id AND repositories.url LIKE ?;" + ); + + my $cfg = Apache2::Module::get_config + (__PACKAGE__, $r->server, $r->per_dir_config); + + my $prefix = $cfg->{SoundSoftwareRepoPrefix}; + if (!defined $prefix) { $prefix = '%/'; } + + my $identifier = ''; + + $sth->execute($prefix . $repo); + my $ret = 0; + if (my @row = $sth->fetchrow_array) { + $identifier = $row[0]; + } + $sth->finish(); + undef $sth; + + print STDERR "SoundSoftware.pm: Repository '$repo' belongs to project '$identifier'\n"; + + $identifier; +} + +sub get_realm { + my $dbh = shift; + my $project_id = shift; + my $r = shift; + + my $sth = $dbh->prepare( + "SELECT projects.name FROM projects WHERE projects.identifier = ?;" + ); + + my $name = $project_id; + + $sth->execute($project_id); + my $ret = 0; + if (my @row = $sth->fetchrow_array) { + $name = $row[0]; + } + $sth->finish(); + undef $sth; + + # be timid about characters not permitted in auth realm and revert + # to project identifier if any are found + if ($name =~ m/[^\w\d\s\._-]/) { + $name = $project_id; + } + + my $realm = '"Mercurial repository for ' . "'$name'" . '"'; + + $realm; +} + +sub connect_database { + my $r = shift; + + my $cfg = Apache2::Module::get_config + (__PACKAGE__, $r->server, $r->per_dir_config); + + return DBI->connect($cfg->{SoundSoftwareDSN}, + $cfg->{SoundSoftwareDbUser}, + $cfg->{SoundSoftwareDbPass}); +} + +1;
--- a/extra/svn/reposman.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/extra/svn/reposman.rb Wed Aug 25 16:33:28 2010 +0100 @@ -48,6 +48,8 @@ # kind. # This command override the default creation for git # and subversion. +# --http-user=USER User for HTTP Basic authentication with Redmine WS +# --http-pass=PASSWORD Password for Basic authentication with Redmine WS # -f, --force force repository creation even if the project # repository is already declared in Redmine # -t, --test only show what should be done @@ -78,6 +80,8 @@ ['--url', '-u', GetoptLong::REQUIRED_ARGUMENT], ['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT], ['--scm', GetoptLong::REQUIRED_ARGUMENT], + ['--http-user', GetoptLong::REQUIRED_ARGUMENT], + ['--http-pass', GetoptLong::REQUIRED_ARGUMENT], ['--test', '-t', GetoptLong::NO_ARGUMENT], ['--force', '-f', GetoptLong::NO_ARGUMENT], ['--verbose', '-v', GetoptLong::NO_ARGUMENT], @@ -90,6 +94,8 @@ $quiet = false $redmine_host = '' $repos_base = '' +$http_user = '' +$http_pass = '' $svn_owner = 'root' $svn_group = 'root' $use_groupid = true @@ -138,6 +144,8 @@ when '--group'; $svn_group = arg.dup; $use_groupid = false; when '--url'; $svn_url = arg.dup when '--scm'; $scm = arg.dup.capitalize; log("Invalid SCM: #{$scm}", :exit => true) unless SUPPORTED_SCM.include?($scm) + when '--http-user'; $http_user = arg.dup + when '--http-pass'; $http_pass = arg.dup when '--command'; $command = arg.dup when '--verbose'; $verbose += 1 when '--test'; $test = true @@ -188,6 +196,8 @@ $redmine_host.gsub!(/\/$/, '') Project.site = "#{$redmine_host}/sys"; +Project.user = $http_user; +Project.password = $http_pass; begin # Get all active projects that have the Repository module enabled @@ -307,4 +317,4 @@ end end - \ No newline at end of file +
--- a/lib/redmine/scm/adapters/abstract_adapter.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/lib/redmine/scm/adapters/abstract_adapter.rb Wed Aug 25 16:33:28 2010 +0100 @@ -271,7 +271,8 @@ end class Revision - attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch + attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch + attr_writer :identifier def initialize(attributes={}) self.identifier = attributes[:identifier] @@ -285,6 +286,12 @@ self.branch = attributes[:branch] end + # Returns the identifier of this revision. + # e.g. revision number for centralized system; hash id for DVCS + def identifier + @identifier || scmid || revision + end + def save(repo) Changeset.transaction do changeset = Changeset.new(
--- a/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl Wed Aug 25 16:30:24 2010 +0100 +++ b/lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl Wed Aug 25 16:33:28 2010 +0100 @@ -9,4 +9,4 @@ file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n' tag = '<tag>{tag|escape}</tag>\n' header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n' -# footer="</log>" \ No newline at end of file +footer='</log>'
--- a/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl Wed Aug 25 16:30:24 2010 +0100 +++ b/lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl Wed Aug 25 16:33:28 2010 +0100 @@ -9,4 +9,4 @@ file_copy = '<path-copied copyfrom-path="{source|escape}">{name|urlescape}</path-copied>\n' tag = '<tag>{tag|escape}</tag>\n' header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n' -# footer="</log>" +footer='</log>'
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb Wed Aug 25 16:30:24 2010 +0100 +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb Wed Aug 25 16:33:28 2010 +0100 @@ -16,6 +16,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'redmine/scm/adapters/abstract_adapter' +require 'rexml/document' module Redmine module Scm @@ -24,31 +25,34 @@ # Mercurial executable name HG_BIN = "hg" + HG_HELPER_EXT = "#{RAILS_ROOT}/extra/mercurial/redminehelper.py" TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial" TEMPLATE_NAME = "hg-template" TEMPLATE_EXTENSION = "tmpl" + # raised if hg command exited with error, e.g. unknown revision. + class HgCommandAborted < CommandFailed; end + class << self def client_version - @@client_version ||= (hgversion || []) + @client_version ||= hgversion end def hgversion # The hg version is expressed either as a # release number (eg 0.9.5 or 1.0) or as a revision # id composed of 12 hexa characters. - theversion = hgversion_from_command_line - if theversion.match(/^\d+(\.\d+)+/) - theversion.split(".").collect(&:to_i) - end + hgversion_str.to_s.split('.').map { |e| e.to_i } end + private :hgversion - def hgversion_from_command_line - %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] + def hgversion_str + shellout("#{HG_BIN} --version") { |io| io.gets }.to_s[/\d+(\.\d+)+/] end + private :hgversion_str def template_path - @@template_path ||= template_path_for(client_version) + template_path_for(client_version) end def template_path_for(version) @@ -59,146 +63,202 @@ end "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" end + private :template_path_for end def info - cmd = "#{HG_BIN} -R #{target('')} root" - root_url = nil - shellout(cmd) do |io| - root_url = io.read - end - return nil if $? && $?.exitstatus != 0 - info = Info.new({:root_url => root_url.chomp, - :lastrev => revisions(nil,nil,nil,{:limit => 1}).last - }) - info - rescue CommandFailed - return nil + tip = summary['tip'].first + Info.new(:root_url => summary['root'].first['path'], + :lastrev => Revision.new(:identifier => tip['rev'].to_i, + :revision => tip['rev'], + :scmid => tip['node'])) + end + + def tags + summary['tags'].map { |e| e['name'] } end + # Returns map of {'tag' => 'nodeid', ...} + def tagmap + alist = summary['tags'].map { |e| e.values_at('name', 'node') } + Hash[*alist.flatten] + end + + def branches + summary['branches'].map { |e| e['name'] } + end + + # Returns map of {'branch' => 'nodeid', ...} + def branchmap + alist = summary['branches'].map { |e| e.values_at('name', 'node') } + Hash[*alist.flatten] + end + + # NOTE: DO NOT IMPLEMENT default_branch !! + # It's used as the default revision by RepositoriesController. + + def summary + @summary ||= fetchg 'rhsummary' + end + private :summary + def entries(path=nil, identifier=nil) - path ||= '' entries = Entries.new - cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate" - cmd << " -r " + (identifier ? identifier.to_s : "tip") - cmd << " " + shell_quote("path:#{path}") unless path.empty? - shellout(cmd) do |io| - io.each_line do |line| - # HG uses antislashs as separator on Windows - line = line.gsub(/\\/, "/") - if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'') - e ||= line - e = e.chomp.split(%r{[\/\\]}) - entries << Entry.new({:name => e.first, - :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"), - :kind => (e.size > 1 ? 'dir' : 'file'), - :lastrev => Revision.new - }) unless e.empty? || entries.detect{|entry| entry.name == e.first} + fetched_entries = fetchg('rhentries', '-r', hgrev(identifier), + without_leading_slash(path.to_s)) + + fetched_entries['dirs'].each do |e| + entries << Entry.new(:name => e['name'], + :path => "#{with_trailling_slash(path)}#{e['name']}", + :kind => 'dir') + end + + fetched_entries['files'].each do |e| + entries << Entry.new(:name => e['name'], + :path => "#{with_trailling_slash(path)}#{e['name']}", + :kind => 'file', + :size => e['size'].to_i, + :lastrev => Revision.new(:identifier => e['rev'].to_i, + :time => Time.at(e['time'].to_i))) + end + + entries + rescue HgCommandAborted + nil # means not found + end + + # TODO: is this api necessary? + def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) + revisions = Revisions.new + each_revision { |e| revisions << e } + revisions + end + + # Iterates the revisions by using a template file that + # makes Mercurial produce a xml output. + def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={}) + hg_args = ['log', '--debug', '-C', '--style', self.class.template_path] + hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" + hg_args << '--limit' << options[:limit] if options[:limit] + hg_args << without_leading_slash(path) unless path.blank? + doc = hg(*hg_args) { |io| REXML::Document.new(io.read) } + # TODO: ??? HG doesn't close the XML Document... + + doc.each_element('log/logentry') do |le| + cpalist = le.get_elements('paths/path-copied').map do |e| + [e.text, e.attributes['copyfrom-path']] + end + cpmap = Hash[*cpalist.flatten] + + paths = le.get_elements('paths/path').map do |e| + {:action => e.attributes['action'], :path => with_leading_slash(e.text), + :from_path => (cpmap.member?(e.text) ? with_leading_slash(cpmap[e.text]) : nil), + :from_revision => (cpmap.member?(e.text) ? le.attributes['revision'] : nil)} + end.sort { |a, b| a[:path] <=> b[:path] } + + yield Revision.new(:identifier => le.attributes['revision'], + :revision => le.attributes['revision'], + :scmid => le.attributes['node'], + :author => (le.elements['author'].text rescue ''), + :time => Time.parse(le.elements['date'].text).localtime, + :message => le.elements['msg'].text, + :paths => paths) + end + self + end + + # Returns list of nodes in the specified branch + def nodes_in_branch(branch, path=nil, identifier_from=nil, identifier_to=nil, options={}) + hg_args = ['log', '--template', '{node|short}\n', '-b', branch] + hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}" + hg_args << '--limit' << options[:limit] if options[:limit] + hg_args << without_leading_slash(path) unless path.blank? + hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } } + end + + def diff(path, identifier_from, identifier_to=nil) + hg_args = ['diff', '--nodates'] + if identifier_to + hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from) + else + hg_args << '-c' << hgrev(identifier_from) + end + hg_args << without_leading_slash(path) unless path.blank? + + hg *hg_args do |io| + io.collect + end + rescue HgCommandAborted + nil # means not found + end + + def cat(path, identifier=nil) + hg 'cat', '-r', hgrev(identifier), without_leading_slash(path) do |io| + io.binmode + io.read + end + rescue HgCommandAborted + nil # means not found + end + + def annotate(path, identifier=nil) + blame = Annotate.new + hg 'annotate', '-ncu', '-r', hgrev(identifier), without_leading_slash(path) do |io| + io.each do |line| + next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):(.*)$} + r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3) + blame.add_line($4.rstrip, r) + end + end + blame + rescue HgCommandAborted + nil # means not found or cannot be annotated + end + + # Runs 'hg' command with the given args + def hg(*args, &block) + full_args = [HG_BIN, '--cwd', url] + full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}" + full_args += args + ret = shellout(full_args.map { |e| shell_quote e.to_s }.join(' '), &block) + if $? && $?.exitstatus != 0 + raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}" + end + ret + end + private :hg + + # Runs 'hg' helper, then parses output to return + def fetchg(*args) + # command output example: + # :tip: rev node + # 100 abcdef012345 + # :tags: rev node name + # 100 abcdef012345 tip + # ... + data = Hash.new { |h, k| h[k] = [] } + hg(*args) do |io| + key, attrs = nil, nil + io.each do |line| + next if line.chomp.empty? + if /^:(\w+): ([\w ]+)/ =~ line + key = $1 + attrs = $2.split(/ /) + elsif key + alist = attrs.zip(line.chomp.split(/ /, attrs.size)) + data[key] << Hash[*alist.flatten] end end end - return nil if $? && $?.exitstatus != 0 - entries.sort_by_name + data end - - # Fetch the revisions by using a template file that - # makes Mercurial produce a xml output. - def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) - revisions = Revisions.new - cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}" - if identifier_from && identifier_to - cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}" - elsif identifier_from - cmd << " -r #{identifier_from.to_i}:" - end - cmd << " --limit #{options[:limit].to_i}" if options[:limit] - cmd << " #{path}" if path - shellout(cmd) do |io| - begin - # HG doesn't close the XML Document... - doc = REXML::Document.new(io.read << "</log>") - doc.elements.each("log/logentry") do |logentry| - paths = [] - copies = logentry.get_elements('paths/path-copied') - logentry.elements.each("paths/path") do |path| - # Detect if the added file is a copy - if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text } - from_path = c.attributes['copyfrom-path'] - from_rev = logentry.attributes['revision'] - end - paths << {:action => path.attributes['action'], - :path => "/#{path.text}", - :from_path => from_path ? "/#{from_path}" : nil, - :from_revision => from_rev ? from_rev : nil - } - end - paths.sort! { |x,y| x[:path] <=> y[:path] } - - revisions << Revision.new({:identifier => logentry.attributes['revision'], - :scmid => logentry.attributes['node'], - :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""), - :time => Time.parse(logentry.elements['date'].text).localtime, - :message => logentry.elements['msg'].text, - :paths => paths - }) - end - rescue - logger.debug($!) - end - end - return nil if $? && $?.exitstatus != 0 - revisions + private :fetchg + + # Returns correct revision identifier + def hgrev(identifier) + identifier.blank? ? 'tip' : identifier.to_s end - - def diff(path, identifier_from, identifier_to=nil) - path ||= '' - if identifier_to - identifier_to = identifier_to.to_i - else - identifier_to = identifier_from.to_i - 1 - end - cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates" - cmd << " -I #{target(path)}" unless path.empty? - diff = [] - shellout(cmd) do |io| - io.each_line do |line| - diff << line - end - end - return nil if $? && $?.exitstatus != 0 - diff - end - - def cat(path, identifier=nil) - cmd = "#{HG_BIN} -R #{target('')} cat" - cmd << " -r " + (identifier ? identifier.to_s : "tip") - cmd << " #{target(path)}" - cat = nil - shellout(cmd) do |io| - io.binmode - cat = io.read - end - return nil if $? && $?.exitstatus != 0 - cat - end - - def annotate(path, identifier=nil) - path ||= '' - cmd = "#{HG_BIN} -R #{target('')}" - cmd << " annotate -n -u" - cmd << " -r " + (identifier ? identifier.to_s : "tip") - 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 - end + private :hgrev end end end
--- a/public/themes/ssamr/stylesheets/application.css Wed Aug 25 16:30:24 2010 +0100 +++ b/public/themes/ssamr/stylesheets/application.css Wed Aug 25 16:33:28 2010 +0100 @@ -20,6 +20,19 @@ src: url('DroidSans-Bold.ttf'); } +@font-face +{ + font-family: Gillius; + src: url('GilliusADFNo2-Regular.otf'); +} + +@font-face +{ + font-family: Gillius; + font-weight: bold; + src: url('GilliusADFNo2-Bold.otf'); +} + body { background: #ffffff; color: #404040;