Mercurial > hg > soundsoftware-site
changeset 443:350acce374a2 cannam
Merge from branch "cannam-pre-20110113-merge"
line wrap: on
line diff
--- a/app/controllers/application_controller.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/controllers/application_controller.rb Mon Jun 06 14:55:38 2011 +0100 @@ -267,12 +267,17 @@ uri = URI.parse(back_url) # do not redirect user to another host or to the login or register page if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)}) + # soundsoftware: if back_url is the home page, + # change it to My Page (#125) + if (uri.path == home_path) + uri.path = uri.path + "/my" + end # soundsoftware: if login page is https but back_url http, # switch back_url to https to ensure cookie validity (#83) if (uri.scheme == "http") && (URI.parse(request.url).scheme == "https") uri.scheme = "https" - back_url = uri.to_s end + back_url = uri.to_s redirect_to(back_url) return end
--- a/app/controllers/attachments_controller.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/controllers/attachments_controller.rb Mon Jun 06 14:55:38 2011 +0100 @@ -16,9 +16,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. class AttachmentsController < ApplicationController + before_filter :find_project before_filter :file_readable, :read_authorize, :except => :destroy before_filter :delete_authorize, :only => :destroy + before_filter :active_authorize, :only => :toggle_active verify :method => :post, :only => :destroy @@ -54,6 +56,12 @@ redirect_to :controller => 'projects', :action => 'show', :id => @project end + def toggle_active + @attachment.active = !@attachment.active? + @attachment.save! + render :layout => false + end + private def find_project @attachment = Attachment.find(params[:id]) @@ -77,6 +85,10 @@ @attachment.deletable? ? true : deny_access end + def active_authorize + true + end + def detect_content_type(attachment) content_type = attachment.content_type if content_type.blank?
--- a/app/controllers/files_controller.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/controllers/files_controller.rb Mon Jun 06 14:55:38 2011 +0100 @@ -8,8 +8,9 @@ include SortHelper def index - sort_init 'filename', 'asc' + sort_init 'active', 'desc' sort_update 'filename' => "#{Attachment.table_name}.filename", + 'active' => "#{Attachment.table_name}.active", 'created_on' => "#{Attachment.table_name}.created_on", 'size' => "#{Attachment.table_name}.filesize", 'downloads' => "#{Attachment.table_name}.downloads" @@ -33,4 +34,5 @@ end redirect_to project_files_path(@project) end + end
--- a/app/controllers/members_controller.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/controllers/members_controller.rb Mon Jun 06 14:55:38 2011 +0100 @@ -28,12 +28,24 @@ attrs = params[:member].dup if (user_ids = attrs.delete(:user_ids)) user_ids.each do |user_id| - members << Member.new(attrs.merge(:user_id => user_id)) + @new_member = Member.new(attrs.merge(:user_id => user_id)) + members << @new_member + + # send notification to member + Mailer.deliver_added_to_project(@new_member, @project) + end else - members << Member.new(attrs) + @new_member = Member.new(attrs) + members << @new_member + + # send notification to member + Mailer.deliver_added_to_project(@new_member, @project) + end + @project.members << members + end respond_to do |format| if members.present? && members.all? {|m| m.valid? } @@ -54,8 +66,8 @@ errors = members.collect {|m| m.errors.full_messages }.flatten.uniq - - page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', '))) + + # page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', '))) } }
--- a/app/controllers/my_controller.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/controllers/my_controller.rb Mon Jun 06 14:55:38 2011 +0100 @@ -25,14 +25,16 @@ BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues, 'issuesreportedbyme' => :label_reported_issues, 'issueswatched' => :label_watched_issues, + 'activitymyprojects' => :label_activity_my_recent, 'news' => :label_news_latest, + 'tipoftheday' => :label_tipoftheday, 'calendar' => :label_calendar, 'documents' => :label_document_plural, 'timelog' => :label_spent_time }.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze - DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'], - 'right' => ['issuesreportedbyme'] + DEFAULT_LAYOUT = { 'left' => ['tipoftheday', 'activitymyprojects'], + 'right' => ['issueswatched','calendar'] }.freeze verify :xhr => true,
--- a/app/controllers/projects_controller.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/controllers/projects_controller.rb Mon Jun 06 14:55:38 2011 +0100 @@ -56,7 +56,9 @@ @offset ||= @project_pages.current.offset @projects = Project.visible_roots.all(:offset => @offset, :limit => @limit, :order => sort_clause) if User.current.logged? - @user_projects = User.current.projects.sort_by(&:name) + # seems sort_by gives us case-sensitive ordering, which we don't want +# @user_projects = User.current.projects.sort_by(&:name) + @user_projects = User.current.projects.all(:order => :name) end render :template => 'projects/index.rhtml', :layout => !request.xhr? } @@ -215,6 +217,15 @@ end verify :method => :post, :only => :modules, :render => {:nothing => true, :status => :method_not_allowed } + + def overview + @project.has_welcome_page = params[:has_welcome_page] + if @project.save + flash[:notice] = l(:notice_successful_update) + end + redirect_to :action => 'settings', :id => @project, :tab => 'overview' + end + def modules @project.enabled_module_names = params[:enabled_module_names] flash[:notice] = l(:notice_successful_update)
--- a/app/controllers/repositories_controller.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/controllers/repositories_controller.rb Mon Jun 06 14:55:38 2011 +0100 @@ -36,7 +36,11 @@ def edit @repository = @project.repository - if !@repository && !params[:repository_scm].blank? + + if !@repository + + params[:repository_scm]='Mercurial' + @repository = Repository.factory(params[:repository_scm]) @repository.project = @project if @repository end @@ -55,6 +59,7 @@ @repository.merge_extra_info(p_extra) @repository.save end + render(:update) do |page| page.replace_html "tab-content-repository", :partial => 'projects/settings/repository'
--- a/app/controllers/sys_controller.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/controllers/sys_controller.rb Mon Jun 06 14:55:38 2011 +0100 @@ -55,6 +55,30 @@ render :nothing => true, :status => 404 end + def get_external_repo_url + project = Project.find(params[:id]) + if project.repository + repo = project.repository + if repo.is_external? + render :text => repo.external_url, :status => 200 + else + render :nothing => true, :status => 200 + end + end + rescue ActiveRecord::RecordNotFound + render :nothing => true, :status => 404 + end + + def clear_repository_cache + project = Project.find(params[:id]) + if project.repository + project.repository.clear_cache + end + render :nothing => true, :status => 200 + rescue ActiveRecord::RecordNotFound + render :nothing => true, :status => 404 + end + def set_embedded_active project = Project.find(params[:id]) mods = project.enabled_modules
--- a/app/controllers/welcome_controller.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/controllers/welcome_controller.rb Mon Jun 06 14:55:38 2011 +0100 @@ -18,11 +18,16 @@ class WelcomeController < ApplicationController caches_action :robots + include ProjectsHelper + helper :projects + def index - @news = News.latest User.current + @site_project = Project.find_by_identifier "soundsoftware-site" + @site_news = [] + @site_news = News.latest_for @site_project if @site_project @projects = Project.latest User.current - # tests if user is logged in to gfenerate the tips of the day list + # tests if user is logged in to generate the tips of the day list if User.current.logged? @tipsoftheday = Setting.tipoftheday_text else
--- a/app/helpers/projects_helper.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/helpers/projects_helper.rb Mon Jun 06 14:55:38 2011 +0100 @@ -23,6 +23,7 @@ def project_settings_tabs tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural}, + {:name => 'overview', :action => :edit_project, :partial => 'projects/settings/overview', :label => :label_welcome_page}, {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural}, {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural}, {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural}, @@ -49,6 +50,16 @@ content_tag('select', options, :name => 'project[parent_id]', :id => 'project_parent_id') end + def render_project_short_description(project) + s = '' + if (project.short_description) + s << "<div class='description'>" + s << textilizable(project.short_description, :project => project).gsub(/<[^>]+>/, '') + s << "</div>" + end + s + end + # Renders a tree of projects as a nested set of unordered lists # The given collection may be a subset of the whole project tree # (eg. some intermediate nodes are private and can not be seen) @@ -73,7 +84,7 @@ classes = (ancestors.empty? ? 'root' : 'child') s << "<li class='#{classes}'><div class='#{classes}'>" + link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") - s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank? + s << render_project_short_description(project) s << "</div>\n" ancestors << project end @@ -84,68 +95,73 @@ end + def render_my_project_in_hierarchy(project) + + s = '' + + if User.current.member_of?(project) + + # set the project environment to please macros. + @project = project + + classes = (project.root? ? 'root' : 'child') + + s << "<li class='#{classes}'><div class='#{classes}'>" + + link_to_project(project, {}, :class => "project my-project") + if project.is_public? + s << " <span class='public'>" << l("field_is_public") << "</span>" + else + s << " <span class='private'>" << l("field_is_private") << "</span>" + end + s << render_project_short_description(project) + s << "</div>\n" + + cs = '' + project.children.each do |child| + cs << render_my_project_in_hierarchy(child) + end + + if cs != '' + s << "<ul class='projects'>\n" << cs << "</ul>\n"; + end + + end + + s + + end + # Renders a tree of projects where the current user belongs # as a nested set of unordered lists # The given collection may be a subset of the whole project tree # (eg. some intermediate nodes are private and can not be seen) def render_my_project_hierarchy(projects) + s = '' - a = '' + original_project = @project - # Flag to tell if user has any projects - t = FALSE - - if projects.any? - ancestors = [] - original_project = @project - projects.each do |project| - # set the project environment to please macros. - - @project = project - - if User.current.member_of?(project): - - t = TRUE - - if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) - s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n" - else - ancestors.pop - s << "</li>" - while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) - ancestors.pop - s << "</ul></li>\n" - end - end - - classes = (ancestors.empty? ? 'root' : 'child') - s << "<li class='#{classes}'><div class='#{classes}'>" + - link_to_project(project, {}, :class => "project my-project") - if project.is_public? - s << " <span class='public'>" << l("field_is_public") << "</span>" - else - s << " <span class='private'>" << l("field_is_private") << "</span>" - end - s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank? - s << "</div>\n" - ancestors << project - end - end - s << ("</li></ul>\n" * ancestors.size) - @project = original_project + projects.each do |project| + if project.root? || !projects.include?(project.parent) + s << render_my_project_in_hierarchy(project) + end end - if t == TRUE + @project = original_project + + if s != '' + a = '' a << "<h2>" a << l("label_my_project_plural") a << "</h2>" + a << "<ul class='projects root'>\n" a << s - else - a = s + a << "</ul>\n" + s = a end + + s - a end # Renders a tree of projects that the current user does not belong @@ -198,11 +214,7 @@ s << " no_description" if project.description.blank? s << "'>" << link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}"); s << "</div>" - unless project.description.blank? - s << "<div class='wiki description'>" - s << textilizable(project.short_description, :project => project) - s << "</div>" - end + s << render_project_short_description(project) s << "<td class='managers' align=top>" @@ -230,7 +242,9 @@ s << "</tr>" project.children.each do |child| - s << render_project_in_table(child, oddeven, level + 1) + if child.is_public? or User.current.member_of?(child) + s << render_project_in_table(child, oddeven, level + 1) + end end s
--- a/app/models/mailer.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/models/mailer.rb Mon Jun 06 14:55:38 2011 +0100 @@ -32,6 +32,27 @@ { :host => h, :protocol => Setting.protocol } end + + + # Builds a tmail object used to email the specified user that he was added to a project + # + # Example: + # add_to_project(user) => tmail object + # Mailer.deliver_add_to_project(user) => sends an email to the registered user + def added_to_project(member, project) + + user = User.find(member.user_id) + + set_language_if_valid user.language + recipients user.mail + subject l(:mail_subject_added_to_project, Setting.app_title) + body :project_url => url_for(:controller => 'projects', :action => 'show', :id => project.id), + :project_name => project.name + render_multipart('added_to_project', body) + end + + + # Builds a tmail object used to email recipients of the added issue. # # Example: @@ -458,3 +479,7 @@ end end end + + + +
--- a/app/models/news.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/models/news.rb Mon Jun 06 14:55:38 2011 +0100 @@ -51,4 +51,9 @@ def add_author_as_watcher Watcher.create(:watchable => self, :user => author) end + + # returns latest news for a specific project + def self.latest_for(project, count = 5) + find(:all, :limit => count, :conditions => [ "#{News.table_name}.project_id = #{project.id}", Project.allowed_to_condition(User.current, :view_news) ], :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") + end end
--- a/app/models/project.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/models/project.rb Mon Jun 06 14:55:38 2011 +0100 @@ -468,13 +468,20 @@ # Returns a short description of the projects (first lines) def short_description(length = 255) + + ## The short description is used in lists, e.g. Latest projects, + ## My projects etc. It should be no more than a line or two with + ## no text formatting. + ## Original Redmine code: this truncates to the CR that is more ## than "length" characters from the start. # description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description - ## That's too much for us, and also we want to omit images and the - ## like. Truncate instead to the first CR that follows _any_ - ## non-blank text, and to the next word break beyond "length" - ## characters if the result is still longer than that. + + ## That can leave too much text for us, and also we want to omit + ## images and the like. Truncate instead to the first CR that + ## follows _any_ non-blank text, and to the next word break beyond + ## "length" characters if the result is still longer than that. + ## description.gsub(/![^\s]+!/, '').gsub(/^(\s*[^\n\r]*).*$/m, '\1').gsub(/^(.{#{length}}\b).*$/m, '\1 ...').strip if description end
--- a/app/models/repository.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/models/repository.rb Mon Jun 06 14:55:38 2011 +0100 @@ -268,6 +268,10 @@ nil end + def clear_cache + clear_changesets + end + def self.scm_adapter_class nil end
--- a/app/models/repository/mercurial.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/models/repository/mercurial.rb Mon Jun 06 14:55:38 2011 +0100 @@ -22,7 +22,7 @@ has_many :changesets, :order => "#{Changeset.table_name}.id DESC", :foreign_key => 'repository_id' attr_protected :root_url - validates_presence_of :url + # validates_presence_of :url FETCH_AT_ONCE = 100 # number of changesets to fetch at once
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/activities/_recent.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,65 @@ +<% events = @events_by_day %> +<% max = 5 %> +<% if (events.nil?) + activity = Redmine::Activity::Fetcher.new(User.current, :project => @project) + + if @project + # Don't show news (duplicated with News box) or wiki edits (too + # tedious) in project front page + activity.scope = [ "changesets", "files", "issues", "documents" ] + end + + events = activity.events(Date.today - 28, Date.today + 1) + + if defined? user + events = events.select { |e| user.member_of? e.project } + end + + events = events.first(max) + + end +%> + +<div id="activity"> + +<% if @project.nil? %> + <%= content_tag('h3', l(:label_activity_my_recent)) %> + <div class="activity box"> +<% end %> + +<% if events.empty? %> + + <% if @project.nil? %> + <p><%= l(:label_activity_my_recent_none) %></p> + <% end %> + +<% else %> + + <% if !@project.nil? %> + <div class="activity box"> + <%= content_tag('h3', l(:label_activity_recent)) %> + <% end %> + + <dl> + <% events.sort {|x,y| y.event_datetime <=> x.event_datetime }.each do |e| -%> + <dt class="<%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>"> + <%= avatar(e.event_author, :size => "24") if e.respond_to?(:event_author) %> + <span class="time"><%= format_time(e.event_datetime) %></span> + <%= content_tag('span', link_to_project(e.project), :class => 'project') if @project.nil? || @project != e.project %> + <% if e.respond_to?(:event_author) %> + <span class="author"><%= e.event_author %></span> + <% end %> + </dt> + <dd><%= link_to format_activity_title(e.event_title), e.event_url %> + <span class="description"><%= format_activity_description(e.event_description) %></span> + </dd> + <% end -%> + </dl> + + </div> + +<% end %> + +<% if events.empty? and @project.nil? %></div><% end %> + +</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/attachments/toggle_active.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,7 @@ +<%= +file = Attachment.find(params[:id]) +active_id = "active-" + file.id.to_s +link_to_remote image_tag(file.active? ? 'fav.png' : 'fav_off.png'), + :url => {:controller => 'attachments', :action => 'toggle_active', :project_id => @project.id, :id => file}, + :update => active_id +%>
--- a/app/views/files/index.html.erb Mon Jun 06 14:41:04 2011 +0100 +++ b/app/views/files/index.html.erb Mon Jun 06 14:55:38 2011 +0100 @@ -5,29 +5,51 @@ <h2><%=l(:label_attachment_plural)%></h2> <% delete_allowed = User.current.allowed_to?(:manage_files, @project) %> +<% active_change_allowed = delete_allowed %> <table class="list files"> <thead><tr> + <%= sort_header_tag('active', :caption => l(:field_active)) %> <%= sort_header_tag('filename', :caption => l(:field_filename)) %> <%= sort_header_tag('created_on', :caption => l(:label_date), :default_order => 'desc') %> <%= sort_header_tag('size', :caption => l(:field_filesize), :default_order => 'desc') %> - <%= sort_header_tag('downloads', :caption => l(:label_downloads_abbr), :default_order => 'desc') %> + <%= sort_header_tag('downloads', :caption => l(:field_downloads), :default_order => 'desc') %> <th>MD5</th> <th></th> </tr></thead> <tbody> +<% have_file = false %> <% @containers.each do |container| %> <% next if container.attachments.empty? -%> <% if container.is_a?(Version) -%> <tr> - <th colspan="6" align="left"> + <th colspan="7" align="left"> <%= link_to(h(container), {:controller => 'versions', :action => 'show', :id => container}, :class => "icon icon-package") %> </th> </tr> <% end -%> <% container.attachments.each do |file| %> - <tr class="file <%= cycle("odd", "even") %>"> - <td class="filename"><%= link_to_attachment file, :download => true, :title => file.description %></td> + <tr class="file <%= cycle("odd", "even") %> <%= "active" if file.active? %>"> + <td class="active"> + <% have_file = true %> + <% if active_change_allowed + active_id = "active-" + file.id.to_s -%> + <div id="<%= active_id %>"> + <%= link_to_remote image_tag(file.active? ? 'fav.png' : 'fav_off.png'), + :url => {:controller => 'attachments', :action => 'toggle_active', :project_id => @project.id, :id => file}, + :update => active_id + %> + </div> + <% else -%> + <%= image_tag('fav.png') if file.active? %> + <% end -%> + </td> + <% if file.active? %> + <td class="filename active"><%= link_to_attachment file, :download => true %><br><span class="description"><%= h(file.description) %></span></td> + <% else %> + <td class="filename"><%= link_to_attachment file, :download => true, :title => file.description %> + <% end %> + </td> <td class="created_on"><%= format_time(file.created_on) %></td> <td class="filesize"><%= number_to_human_size(file.filesize) %></td> <td class="downloads"><%= file.downloads %></td> @@ -43,4 +65,6 @@ </tbody> </table> +<%= l(:text_files_active_change) if active_change_allowed and have_file %> + <% html_title(l(:label_attachment_plural)) -%>
--- a/app/views/layouts/base.rhtml Mon Jun 06 14:41:04 2011 +0100 +++ b/app/views/layouts/base.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -54,11 +54,16 @@ <h3 id="project-ancestors-title"><%= page_header_title[1] %></h3> <% end %> - <h1 id="project-title" + <h1 id="project-title" <% unless page_header_title[1].empty? %> style="margin-top: 0px; " <% end %> - ><%= page_header_title[0] %></h1> + ><% if display_main_menu?(@project) %> + <%= link_to_project(@project) %> + <% else %> + <%= page_header_title[0] %> + <% end %> + </h1> <% if display_main_menu?(@project) %> <div id="main-menu"> @@ -85,7 +90,7 @@ <div id="footer"> <div class="bgl"><div class="bgr"> - Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> © 2006-2011 Jean-Philippe Lang + <small>Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %><br>© 2006-2011 Jean-Philippe Lang</small> </div></div> </div> </div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/mailer/added_to_project.text.html.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,3 @@ +<p><%= l(:notice_added_to_project, :project_name => @project_name) %></p> +<p><%= l(:notice_project_homepage, :project_url => @project_url) %></p> +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/mailer/added_to_project.text.plain.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,2 @@ +<%= l(:notice_added_to_project, :project_name => @project_name) %> +<%= l(:notice_project_homepage, :project_url => @project_url) %>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/my/blocks/_activitymyprojects.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,4 @@ + +<%= render :partial => 'activities/recent', :locals => { :user => User.current } %> + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/my/blocks/_tipoftheday.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,4 @@ + <h3><%=l(:label_tipoftheday)%></h3> + <div class="tipoftheday box"> + <div class="tip"><%= textilizable Setting.tipoftheday_text %></div> + </div>
--- a/app/views/news/_news.rhtml Mon Jun 06 14:41:04 2011 +0100 +++ b/app/views/news/_news.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -1,6 +1,13 @@ -<p><%= link_to_project(news.project) + ': ' unless @project %> -<%= link_to h(news.title), news_path(news) %> -<%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %> -<br /> +<div id="news"> +<dt> +<span class="time"><%= format_time(news.created_on) %></span> +<% project ||= @project %> +<% if !project %> +<span class="project"><%= link_to_project(news.project) %></span> +<% end %> +<span class="headline"><%= link_to h(news.title), news_path(news) %></span> +<span class="comments"><%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></span> +</dt><dd> <% unless news.summary.blank? %><span class="summary"><%=h news.summary %></span><br /><% end %> -<span class="author"><%= authoring news.created_on, news.author %></span></p> +</dd> +</div>
--- a/app/views/projects/settings/_members.rhtml Mon Jun 06 14:41:04 2011 +0100 +++ b/app/views/projects/settings/_members.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -2,6 +2,7 @@ <% roles = Role.find_all_givable members = @project.member_principals.find(:all, :include => [:roles, :principal]).sort %> + <div class="splitcontentleft"> <% if members.any? %> <table class="list members"> @@ -73,10 +74,10 @@ <% end %> </div> - <p><%= l(:label_role_plural) %>: + <p><%= l(:label_set_role_plural) %>:</p> <% roles.each do |role| %> - <label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %></label> - <% end %></p> + <label><%= check_box_tag 'member[role_ids][]', role.id %> <%=h role %> </label><div style="margin-left: 2em; margin-bottom: 0.5em"><i><%=l( 'label_' + role.name.downcase + "_description").to_sym %></i></div> + <% end %> <p><%= submit_tag l(:button_add), :id => 'member-add-submit' %></p> </fieldset>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/app/views/projects/settings/_overview.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,30 @@ + +<% form_for :project, @project, + :url => { :action => 'overview', :id => @project }, + :html => {:id => 'overview-form'} do |f| %> + +<div class="box tabular"> + +<p><%= l(:text_has_welcome_page_info, { :overview_link => link_to(l(:label_overview), { :controller => 'projects', :action => 'show' } ) } ) %></p> + +<% if @project.module_enabled? :wiki %> + +<p><%= link_to(l(:button_welcome_page_edit), {:controller => 'wiki', :action => 'edit', :project_id => @project, :id => Wiki.titleize("Overview")}, :class => 'icon icon-edit') %> + +<% else %> + +<p><%= l(:text_has_welcome_page_wiki_disabled, { :modules_link => link_to(l(:label_module_plural), { :controller => 'projects', :action => 'settings', :tab => 'modules' } ) } ) %></p> + +<% end %> + +<p><label for="has_welcome_page"><%= l(:label_has_welcome_page) %></label> +<%= check_box_tag 'has_welcome_page', 1, @project.has_welcome_page? -%> +<br/><em><%= l(:setting_has_welcome_page) %></em> + +</p> + +</div> + +<%= submit_tag l(:button_save) %> + +<% end %>
--- a/app/views/projects/settings/_repository.rhtml Mon Jun 06 14:41:04 2011 +0100 +++ b/app/views/projects/settings/_repository.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -1,3 +1,6 @@ + +<%= javascript_include_tag 'repository' %> + <% remote_form_for :repository, @repository, :url => { :controller => 'repositories', :action => 'edit', :id => @project }, :builder => TabularFormBuilder, @@ -6,11 +9,36 @@ <%= error_messages_for 'repository' %> <div class="box tabular"> -<% if !@repository || !@repository.url %> -<ul><li><%= l(:text_settings_repo_creation) %></li></ul> -<% end %> -<p><%= label_tag('repository_scm', l(:label_scm)) %><%= scm_select_tag(@repository) %></p> -<%= repository_field_tags(f, @repository) if @repository %> + +<p> +<% if @repository %> + <%= l(:text_settings_repo_explanation) %></ br> + <% if @repository.is_external %> + <p><%= l(:text_settings_repo_is_external) %></ br> + <% else %> + <p><%= l(:text_settings_repo_is_internal) %></ br> + <% end %> +</p> + + + + + +<p> + <%= label_tag('repository_is_external', l(:label_is_external_repository)) %> + <%= check_box :repository, :is_external, :onclick => "toggle_ext_url()" %> + <br/><em><%= l(:setting_external_repository) %></em> +</p> + + +<p> + <%= label_tag('repository_external_url', l(:label_repository_external_url)) %> + <%= text_field :repository, :external_url, :disabled => !(@repository and @repository.is_external) %> + <br/><em><%= l(:setting_external_repository_url) %></em> +</p> + +<p><%= l(:text_settings_repo_need_help) %></p> + </div> <div class="contextual"> @@ -22,12 +50,13 @@ :id => @project }, :class => 'icon icon-user') %> -<%= link_to(l(:button_delete), {:controller => 'repositories', :action => 'destroy', :id => @project}, - :confirm => l(:text_are_you_sure), - :method => :post, - :class => 'icon icon-del') %> <% end %> </div> -<%= submit_tag((@repository.nil? || @repository.new_record?) ? l(:button_create) : l(:button_save), :disabled => @repository.nil?) %> +<%= submit_tag(l(:button_save), :onclick => remote_function(:url => { :controller => 'repositories', :action => 'edit', :id => @project }, :method => :get, :with => "Form.serialize(this.form)")) %> + +<% else %> + <%= l(:text_settings_repo_creation) %></ br> <% end %> + +<% end %>
--- a/app/views/projects/show.rhtml Mon Jun 06 14:41:04 2011 +0100 +++ b/app/views/projects/show.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -4,6 +4,29 @@ <% end %> </div> +<% if @project.has_welcome_page %> +<% page = @project.wiki.find_page("Overview") %> +<% end %> + +<% if page %> + +<div class="contextual" style="clear: right"> +<ul> +<% @users_by_role.keys.sort.each do |role| %> +<li><%=h role %>: <%= @users_by_role[role].sort.collect{|u| link_to_user u}.join(", ") %></li> +<% end %> +<% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= auto_link(h(@project.homepage)) %></li><% end %> +<% if @subprojects.any? %> + <li><%=l(:label_subproject_plural)%>: + <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li> +<% end %> +</ul> +</div> + +<%= render(:partial => "wiki/content", :locals => {:content => page.content_for_version()}) %> + +<% else %> + <h2><%=l(:label_overview)%></h2> <div class="splitcontentleft"> @@ -23,7 +46,8 @@ <% end %> </ul> - <% if User.current.allowed_to?(:view_issues, @project) %> + <% if User.current.allowed_to?(:view_issues, @project) and @open_issues_by_tracker.values.any? %> + <div class="issues box"> <h3><%=l(:label_issue_tracking)%></h3> <ul> @@ -46,7 +70,9 @@ <% end %> </p> </div> + <% end %> + <%= call_hook(:view_projects_show_left, :project => @project) %> </div> @@ -60,6 +86,9 @@ <p><%= link_to l(:label_news_view_all), :controller => 'news', :action => 'index', :project_id => @project %></p> </div> <% end %> + + <%= render :partial => 'activities/recent' %> + <%= call_hook(:view_projects_show_right, :project => @project) %> </div> @@ -73,6 +102,8 @@ <%= call_hook(:view_projects_show_sidebar_bottom, :project => @project) %> <% end %> +<% end %> + <% content_for :header_tags do %> <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :key => User.current.rss_key}) %> <% end %>
--- a/app/views/search/index.rhtml Mon Jun 06 14:41:04 2011 +0100 +++ b/app/views/search/index.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -19,11 +19,11 @@ </div> <% if @results %> + + <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3> <div id="search-results-counts"> <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %> </div> - - <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3> <dl id="search-results"> <% @results.each do |e| %> <dt class="<%= e.event_type %>"><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, :length => 255), @tokens), e.event_url %></dt>
--- a/app/views/users/edit.rhtml Mon Jun 06 14:41:04 2011 +0100 +++ b/app/views/users/edit.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -9,3 +9,4 @@ <%= render_tabs user_settings_tabs %> <% html_title(l(:label_user), @user.login, l(:label_administration)) -%> +
--- a/app/views/welcome/index.rhtml Mon Jun 06 14:41:04 2011 +0100 +++ b/app/views/welcome/index.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -9,37 +9,33 @@ <div class="splitcontentleft"> <%= textilizable Setting.welcome_text %> - <% if @news.any? %> - <div class="news box"> - <h3><%=l(:label_news_latest)%></h3> - <%= render :partial => 'news/news', :collection => @news %> - <%= link_to l(:label_news_view_all), :controller => 'news' %> - </div> - <% end %> <%= call_hook(:view_welcome_index_left, :projects => @projects) %> </div> <div class="splitcontentright"> - <% if not @tipsoftheday.empty? %> - <div class="newsoftheday box"> - <h3><%=l(:label_tipoftheday)%></h3> - <%= textilizable @tipsoftheday %> - </div> + <% if @site_news.any? %> + <div class="news box"> + <h3><%=l(:label_news_site_latest)%></h3> + <%= render :partial => 'news/news', :locals => { :project => @site_project }, :collection => @site_news %> + + <%= link_to l(:label_news_more), { :controller => 'projects', :action => @site_project.identifier, :id => 'news' } %> + </div> <% end %> - <% if @projects.any? %> <div class="projects box"> <h3><%=l(:label_project_latest)%></h3> <ul> <% for project in @projects %> <% @project = project %> - <li> - <%= link_to_project project %> (<%= format_time(project.created_on) %>) - <%= textilizable project.short_description, :project => project %> + <li class="latest"> + <span class="title"><%= link_to_project project %></span> + <span class="time"><%= format_time(project.created_on)%></span> + <%= render_project_short_description project %> </li> <% end %> <% @project = nil %> </ul> + <%= link_to l(:label_projects_more), :controller => 'projects' %> </div> <% end %> <%= call_hook(:view_welcome_index_right, :projects => @projects) %>
--- a/config/locales/en.yml Mon Jun 06 14:41:04 2011 +0100 +++ b/config/locales/en.yml Mon Jun 06 14:55:38 2011 +0100 @@ -1,14 +1,13 @@ en: - # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) direction: ltr date: formats: # Use the strftime parameters for formats. # When no format has been given, it uses default. # You can provide other formats here if you like! - default: "%m/%d/%Y" - short: "%b %d" - long: "%B %d, %Y" + default: "%d/%m/%Y" + short: "%d %b" + long: "%d %B, %Y" day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat] @@ -24,10 +23,10 @@ time: formats: - default: "%m/%d/%Y %I:%M %p" + default: "%d/%m/%Y %I:%M %p" time: "%I:%M %p" short: "%d %b %H:%M" - long: "%B %d, %Y %H:%M" + long: "%d %B, %Y %H:%M" am: "am" pm: "pm" @@ -70,10 +69,14 @@ number: format: - separator: "." - delimiter: "" + separator: "." + delimiter: " " precision: 3 + currency: + format: + format: "%u%n" + unit: "£" human: format: delimiter: "" @@ -89,6 +92,7 @@ gb: "GB" tb: "TB" + # Used in array.to_sentence. support: array: @@ -126,7 +130,7 @@ not_same_project: "doesn't belong to the same project" circular_dependency: "This relation would create a circular dependency" cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks" - must_accept_terms_and_conditions: "You must accept the Terms and Conditions" + must_accept_terms_and_conditions: "You must accept the Terms and Conditions" actionview_instancetag_blank_option: Please select @@ -139,7 +143,7 @@ general_csv_decimal_separator: '.' general_csv_encoding: ISO-8859-1 general_pdf_encoding: UTF-8 - general_first_day_of_week: '7' + general_first_day_of_week: '1' notice_account_updated: Account was successfully updated. notice_account_invalid_creditentials: Invalid user or password @@ -323,6 +327,9 @@ field_cvsroot: CVSROOT field_cvs_module: Module + setting_external_repository: "Select this if the project's main repository is hosted somewhere else" + setting_external_repository_url: "The URL of the existing external repository. Must be publicly accessible without a password" + label_repository_external_url: "External repository URL" field_terms_and_conditions: 'Terms and Conditions:' accept_terms_and_conditions: 'I have read and agree with the ' setting_app_title: Application title @@ -379,6 +386,7 @@ setting_rest_api_enabled: Enable REST web service setting_cache_formatted_text: Cache formatted text setting_default_notification_option: Default notification option + setting_has_welcome_page: Select this to replace the project overview page with your welcome page setting_commit_logtime_enabled: Enable time logging setting_commit_logtime_activity_id: Activity for logged time setting_gantt_items_limit: Maximum number of items displayed on the gantt chart @@ -417,8 +425,8 @@ permission_comment_news: Comment news permission_manage_documents: Manage documents permission_view_documents: View documents - permission_manage_files: Manage Downloads - permission_view_files: View Downloads + permission_manage_files: Manage downloads + permission_view_files: View downloads permission_manage_wiki: Manage wiki permission_rename_wiki_pages: Rename wiki pages permission_delete_wiki_pages: Delete wiki pages @@ -451,8 +459,12 @@ project_module_boards: Forums project_module_calendar: Calendar project_module_gantt: Gantt chart - project_module_embedded: Embedded documentation (Javadoc or Doxygen) + project_module_embedded: Embedded documentation (Javadoc, Doxygen or MATLAB) label_tipoftheday: Tip of the day + label_notifications: Important Message + field_terms_and_conditions: 'Terms and Conditions:' + accept_terms_and_conditions: 'I have read and agree with the ' + label_ssamr_description: Research description label_ssamr_details: Other Details label_ssamr_institution: Institution label_ssamr_description: Research description @@ -471,6 +483,7 @@ other: "%{count} projects" label_project_all: All Projects label_project_latest: Latest projects + label_projects_more: More projects label_managers: Managed by label_issue: Issue label_issue_new: New issue @@ -531,6 +544,9 @@ label_last_login: Last connection label_registered_on: Registered on label_activity: Activity + label_activity_recent: Recent activity + label_activity_my_recent: Recent activity in my projects + label_activity_my_recent_none: No recent activity label_overall_activity: Overall activity label_user_activity: "%{value}'s activity" label_new: New @@ -569,6 +585,8 @@ label_news_new: Add news label_news_plural: News label_news_latest: Latest news + label_news_site_latest: Site news + label_news_more: More news label_news_view_all: View all news label_news_added: News added label_news_comment_added: Comment added to a news @@ -617,7 +635,7 @@ label_internal: Internal label_last_changes: "last %{count} changes" label_change_view_all: View all changes - label_personalize_page: Personalize this page + label_personalize_page: Personalise this page label_comment: Comment label_comment_plural: Comments label_x_comments: @@ -657,6 +675,7 @@ label_not_contains: doesn't contain label_day_plural: days label_repository: Repository + label_is_external_repository: Track an external repository label_repository_plural: Repositories label_browse: Browse label_modification: "%{count} change" @@ -763,6 +782,7 @@ label_added_time_by: "Added by %{author} %{age} ago" label_updated_time_by: "Updated by %{author} %{age} ago" label_updated_time: "Updated %{value} ago" + label_time_ago: "{{age}} ago" label_jump_to_a_project: Jump to a project... label_file_plural: Downloads label_changeset_plural: Changesets @@ -831,6 +851,8 @@ label_project_copy_notifications: Send email notifications during the project copy label_principal_search: "Search by name:" label_user_search: "Search for user:" + label_welcome_page: "Welcome page" + label_has_welcome_page: "Use your own welcome page" label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee label_issues_visibility_all: All issues @@ -884,6 +906,7 @@ button_quote: Quote button_duplicate: Duplicate button_show: Show + button_welcome_page_edit: Create or edit welcome page status_active: active status_registered: registered @@ -894,6 +917,7 @@ version_status_closed: closed field_active: Active + field_current: Current text_select_mail_notifications: Select actions for which email notifications should be sent. text_regexp_info: eg. ^[A-Z0-9]+$ @@ -908,15 +932,17 @@ text_journal_set_to: "%{label} set to %{value}" text_journal_deleted: "%{label} deleted (%{old})" text_journal_added: "%{label} %{value} added" - text_tip_issue_begin_day: issue beginning this day - text_tip_issue_end_day: issue ending this day - text_tip_issue_begin_end_day: issue beginning and ending this day + text_tip_issue_begin_day: task beginning this day + text_tip_issue_end_day: task ending this day + text_tip_issue_begin_end_day: task beginning and ending this day text_project_identifier_info: 'Only lower case letters (a-z), numbers and dashes are allowed.<br />Once saved, the identifier cannot be changed.' + text_project_homepage_info: 'Link to an external project page.' text_caracters_maximum: "%{count} characters maximum." text_caracters_minimum: "Must be at least %{count} characters long." text_length_between: "Length between %{min} and %{max} characters." + text_user_ssamr_description_info: 'Please describe your current research or development interests.<br/>This information will be used at registration to determine that you are a real person – so please be descriptive, or your application may be delayed or rejected.<br/>After registration, the description is publicly visible in your profile and you can edit it at any time.' text_issue_parent_issue_info: 'If this is a subtask, please insert its parent task number or write the main task name.' - text_user_ssamr_description_info: 'Please describe your current research or development interests, within the fields of audio and music.<br/>This information is publicly visible in your profile and you can edit it at any time.' + text_tracker_no_workflow: No workflow defined for this tracker text_unallowed_characters: Unallowed characters text_comma_separated: Multiple values allowed (comma separated). @@ -949,7 +975,7 @@ text_enumeration_destroy_question: "%{count} objects are assigned to this value." text_enumeration_category_reassign_to: 'Reassign them to this value:' text_email_delivery_not_configured: "Email delivery is not configured, and notifications are disabled.\nConfigure your SMTP server in config/configuration.yml and restart the application to enable them." - text_repository_usernames_mapping: "Select or update the Redmine user mapped to each username found in the repository log.\nUsers with the same Redmine and repository username or email are automatically mapped." + text_repository_usernames_mapping: "Select the project member associated with each username found in the repository log.\nUsers whose name or email matches that in the repository are mapped automatically." text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.' text_custom_field_possible_values_info: 'One line for each value' text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?" @@ -960,7 +986,8 @@ text_zoom_in: Zoom in text_zoom_out: Zoom out text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page." - text_settings_repo_creation: The repository for a project should be set up automatically within a few minutes of the project being created.<br>You should not have to adjust any settings here.<br>Please check again in ten minutes, and <a href="/projects/soundsoftware-site/wiki/Help">contact us</a> if there is any problem. + text_files_active_change: <br>Click the star to switch active status for a download on or off.<br>Active files will be shown more prominently in the download page. + text_settings_repo_creation: <b>Creating repository...</b></p><p>The source code repository for a project will be set up automatically within a few minutes of the project being created.</p><p>Please check again in five minutes, and <a href="/projects/soundsoftware-site/wiki/Help">contact us</a> if there is any problem.</p><p>If you wish to use this project to track a repository that is already hosted somewhere else, please wait until the repository has been created here and then return to this settings page to configure it.</p><p>If you don't want a repository at all, go to the Modules tab and switch it off there. text_scm_path_encoding_note: "Default: UTF-8" text_git_repository_note: "Bare and local repository (e.g. /gitrepo, c:\gitrepo)" text_mercurial_repository_note: "Local repository (e.g. /hgrepo, c:\hgrepo)" @@ -968,6 +995,13 @@ text_scm_command_version: Version text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. + text_settings_repo_explanation: <b>External repositories</b><p>Normally your project's primary repository will be the Mercurial repository hosted at this site.<p>However, if you already have your project hosted somewhere else, you can specify your existing external repository's URL here – then this site will track that repository in a read-only “mirror” copy. External Mercurial, git and Subversion repositories can be tracked. Note that you cannot switch to an external repository if you have already made any commits to the repository hosted here. + text_settings_repo_is_internal: Currently the repository hosted at this site is the primary repository for this project. + text_settings_repo_is_external: Currently the repository hosted at this site is a read-only copy of an external repository. + text_settings_repo_need_help: Please <a href="/projects/soundsoftware-site/wiki/Help">contact us</a> if you need help deciding how best to set this up.<br>We can also import complete revision history from other systems into a new primary repository for you if you wish. + text_has_welcome_page_info: <b>Welcome page</b><p>You can replace the standard {{overview_link}} page for this project with your own welcome page.<br>This page will be editable using the project Wiki. + text_has_welcome_page_wiki_disabled: <b>Note:</b> You must enable the Wiki module in the {{modules_link}} tab before you can create or edit this page. + default_role_manager: Manager default_role_developer: Developer @@ -995,3 +1029,13 @@ enumeration_doc_categories: Document categories enumeration_activities: Activities (time tracking) enumeration_system_activity: System Activity + + label_manager_description: All powers including adding and removing members and adjusting project settings + label_developer_description: Can commit to repository and carry out most project editing tasks + label_reporter_description: Can submit bug reports; has read access for private projects + + label_set_role_plural: Choose roles for new member + + notice_added_to_project: 'You have been added to the project "{{project_name}}".' + notice_project_homepage: "You can visit the project using the following link: {{project_url}}" + mail_subject_added_to_project: "You've been added to a project on {{value}}" \ No newline at end of file
--- a/config/routes.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/config/routes.rb Mon Jun 06 14:55:38 2011 +0100 @@ -151,6 +151,7 @@ :copy => [:get, :post], :settings => :get, :modules => :post, + :overview => :post, :archive => :post, :unarchive => :post } do |project| @@ -250,7 +251,9 @@ map.with_options :controller => 'sys' do |sys| sys.connect 'sys/projects.:format', :action => 'projects', :conditions => {:method => :get} sys.connect 'sys/projects/:id/repository.:format', :action => 'create_project_repository', :conditions => {:method => :post} + sys.connect 'sys/projects/:id/external_repository.:format', :action => 'get_external_repo_url', :conditions => {:method => :get} sys.connect 'sys/projects/:id/embedded.:format', :action => 'set_embedded_active', :conditions => { :method => :post } + sys.connect 'sys/projects/:id/repository_cache.:format', :action => 'clear_repository_cache', :conditions => {:method => :post} end # Install the default route as the lowest priority.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/db/migrate/20110207142856_add_ext_rep_to_repositories.rb Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,11 @@ +class AddExtRepToRepositories < ActiveRecord::Migration + def self.up + add_column :repositories, :is_external, :bool + add_column :repositories, :external_url, :string + end + + def self.down + remove_column :repositories, :is_external + remove_column :repositories, :external_url + end +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/db/migrate/20110303152903_add_active_column_to_attachments.rb Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,9 @@ +class AddActiveColumnToAttachments < ActiveRecord::Migration + def self.up + add_column :attachments, :active, :boolean + end + + def self.down + remove_column :attachments, :active + end +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/db/migrate/20110331152140_add_has_welcome_page_to_projects.rb Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,9 @@ +class AddHasWelcomePageToProjects < ActiveRecord::Migration + def self.up + add_column :projects, :has_welcome_page, :boolean + end + + def self.down + remove_column :projects, :has_welcome_page + end +end
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/soundsoftware/SoundSoftware-salted.pm Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,470 @@ +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) + +5. Push to any repo that is tracking an external repo: Refused always + +=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, salt, 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 at " . scalar localtime() . "\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"; + + my $dbh = connect_database($r); + unless ($dbh) { + print STDERR "SoundSoftware.pm: Database connection failed!: " . $DBI::errstr . "\n"; + return FORBIDDEN; + } + + print STDERR "Connected to db, dbh is " . $dbh . "\n"; + + my $project_id = get_project_identifier($dbh, $r); + + if (!defined $read_only_methods{$method}) { + print STDERR "SoundSoftware.pm: Method is not read-only\n"; + if (project_repo_is_readonly($dbh, $project_id, $r)) { + print STDERR "SoundSoftware.pm: Project repo is read-only, refusing access\n"; + return FORBIDDEN; + } else { + print STDERR "SoundSoftware.pm: Project repo is read-write, authentication handler required\n"; + return OK; + } + } + + 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 at " . scalar localtime() . "\n"; + + my $dbh = connect_database($r); + unless ($dbh) { + print STDERR "SoundSoftware.pm: Database connection failed!: " . $DBI::errstr . "\n"; + return AUTH_REQUIRED; + } + + 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 project_repo_is_readonly { + 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 repositories.is_external FROM repositories, projects WHERE projects.identifier = ? AND repositories.project_id = projects.id;" + ); + + $sth->execute($project_id); + my $ret = 0; # nonexistent + if (my @row = $sth->fetchrow_array) { + if (defined($row[0]) && ($row[0] eq "1" || $row[0] eq "t")) { + $ret = 1; # read-only (i.e. external) + } else { + $ret = 0; # read-write + } + } + $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, $salt, $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) { + my $salted_password = Digest::SHA1::sha1_hex($salt.$pass_digest); + if ($hashed_password eq $salted_password) { + 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/soundsoftware/SoundSoftware.pm Mon Jun 06 14:41:04 2011 +0100 +++ b/extra/soundsoftware/SoundSoftware.pm Mon Jun 06 14:55:38 2011 +0100 @@ -25,6 +25,8 @@ 4. Push to repo for private project: "Permitted" users only (as above) +5. Push to any repo that is tracking an external repo: Refused always + =head1 INSTALLATION Debian/ubuntu: @@ -172,21 +174,27 @@ 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); unless ($dbh) { print STDERR "SoundSoftware.pm: Database connection failed!: " . $DBI::errstr . "\n"; return FORBIDDEN; } - -print STDERR "Connected to db, dbh is " . $dbh . "\n"; + print STDERR "Connected to db, dbh is " . $dbh . "\n"; my $project_id = get_project_identifier($dbh, $r); + + if (!defined $read_only_methods{$method}) { + print STDERR "SoundSoftware.pm: Method is not read-only\n"; + if (project_repo_is_readonly($dbh, $project_id, $r)) { + print STDERR "SoundSoftware.pm: Project repo is read-only, refusing access\n"; + return FORBIDDEN; + } else { + print STDERR "SoundSoftware.pm: Project repo is read-write, authentication handler required\n"; + return OK; + } + } + my $status = get_project_status($dbh, $project_id, $r); $dbh->disconnect(); @@ -271,6 +279,34 @@ $ret; } +sub project_repo_is_readonly { + 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 repositories.is_external FROM repositories, projects WHERE projects.identifier = ? AND repositories.project_id = projects.id;" + ); + + $sth->execute($project_id); + my $ret = 0; # nonexistent + if (my @row = $sth->fetchrow_array) { + if (defined($row[0]) && ($row[0] eq "1" || $row[0] eq "t")) { + $ret = 1; # read-only (i.e. external) + } else { + $ret = 0; # read-write + } + } + $sth->finish(); + undef $sth; + + $ret; +} + sub is_permitted { my $dbh = shift; my $project_id = shift;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/soundsoftware/convert-external-repos.rb Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,183 @@ +#!/usr/bin/env ruby + +# == Synopsis +# +# convert-external-repos: Update local Mercurial mirrors of external repos, +# by running an external command for each project requiring an update. +# +# == Usage +# +# convert-external-repos [OPTIONS...] -s [DIR] -r [HOST] +# +# == Arguments (mandatory) +# +# -s, --scm-dir=DIR use DIR as base directory for repositories +# -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples: +# -r redmine.example.net +# -r http://redmine.example.net +# -r https://example.net/redmine +# -k, --key=KEY use KEY as the Redmine API key +# -c, --command=COMMAND use this command to update each external +# repository: command is called with the name +# of the project, the path to its repo, and +# its external repo url as its three args +# +# == Options +# +# --http-user=USER User for HTTP Basic authentication with Redmine WS +# --http-pass=PASSWORD Password for Basic authentication with Redmine WS +# -t, --test only show what should be done +# -h, --help show help and exit +# -v, --verbose verbose +# -V, --version print version and exit +# -q, --quiet no log + + +require 'getoptlong' +require 'rdoc/usage' +require 'find' +require 'etc' + +Version = "1.0" + +opts = GetoptLong.new( + ['--scm-dir', '-s', GetoptLong::REQUIRED_ARGUMENT], + ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT], + ['--key', '-k', GetoptLong::REQUIRED_ARGUMENT], + ['--http-user', GetoptLong::REQUIRED_ARGUMENT], + ['--http-pass', GetoptLong::REQUIRED_ARGUMENT], + ['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT], + ['--test', '-t', GetoptLong::NO_ARGUMENT], + ['--verbose', '-v', GetoptLong::NO_ARGUMENT], + ['--version', '-V', GetoptLong::NO_ARGUMENT], + ['--help' , '-h', GetoptLong::NO_ARGUMENT], + ['--quiet' , '-q', GetoptLong::NO_ARGUMENT] + ) + +$verbose = 0 +$quiet = false +$redmine_host = '' +$repos_base = '' +$http_user = '' +$http_pass = '' +$test = false + +$mirrordir = '/var/mirror' + +def log(text, options={}) + level = options[:level] || 0 + puts text unless $quiet or level > $verbose + exit 1 if options[:exit] +end + +def system_or_raise(command) + raise "\"#{command}\" failed" unless system command +end + +begin + opts.each do |opt, arg| + case opt + when '--scm-dir'; $repos_base = arg.dup + when '--redmine-host'; $redmine_host = arg.dup + when '--key'; $api_key = arg.dup + 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 + when '--version'; puts Version; exit + when '--help'; RDoc::usage + when '--quiet'; $quiet = true + end + end +rescue + exit 1 +end + +if $test + log("running in test mode") +end + +if ($redmine_host.empty? or $repos_base.empty? or $command.empty?) + RDoc::usage +end + +unless File.directory?($repos_base) + log("directory '#{$repos_base}' doesn't exist", :exit => true) +end + +begin + require 'active_resource' +rescue LoadError + log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true) +end + +class Project < ActiveResource::Base + self.headers["User-agent"] = "SoundSoftware external repository converter/#{Version}" +end + +log("querying Redmine for projects...", :level => 1); + +$redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://") +$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 + projects = Project.find(:all, :params => {:key => $api_key}) +rescue => e + log("Unable to connect to #{Project.site}: #{e}", :exit => true) +end + +if projects.nil? + log('no project found, perhaps you forgot to "Enable WS for repository management"', :exit => true) +end + +log("retrieved #{projects.size} projects", :level => 1) + +projects.each do |project| + log("treating project #{project.name}", :level => 1) + + if project.identifier.empty? + log("\tno identifier for project #{project.name}") + next + elsif not project.identifier.match(/^[a-z0-9\-]+$/) + log("\tinvalid identifier for project #{project.name} : #{project.identifier}"); + next + end + + if !project.respond_to?(:repository) or !project.repository.is_external? + log("\tproject #{project.identifier} does not use an external repository"); + next + end + + external_url = project.repository.external_url; + log("\tproject #{project.identifier} has external repository url #{external_url}"); + + if !external_url.match(/^[a-z][a-z+]{0,8}[a-z]:\/\//) + log("\tthis doesn't look like a plausible url to me, skipping") + next + end + + repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR) + + unless File.directory?(repos_path) + log("\tproject repo directory '#{repos_path}' doesn't exist") + next + end + + system($command, project.identifier, repos_path, external_url) + + $cache_clearance_file = File.join($mirrordir, project.identifier, 'url_changed') + if File.file?($cache_clearance_file) + log("\tproject repo url has changed, requesting cache clearance") + if project.post(:repository_cache, :key => $api_key) + File.delete($cache_clearance_file) + end + end + +end +
--- a/extra/soundsoftware/extract-docs.sh Mon Jun 06 14:41:04 2011 +0100 +++ b/extra/soundsoftware/extract-docs.sh Mon Jun 06 14:55:38 2011 +0100 @@ -12,6 +12,7 @@ redgrp="redmine" apikey="" +apischeme="https" apihost="" apiuser="" apipass="" @@ -22,7 +23,7 @@ *) progdir="$(pwd)/$progdir" ;; esac -types="doxygen javadoc" # Do Doxygen first (it can be used for Java too) +types="doxygen javadoc matlabdocs" # Do Doxygen first (it can be used for Java too) for x in $types; do if [ ! -x "$progdir/extract-$x.sh" ]; then @@ -36,9 +37,9 @@ p="$1" if [ -n "$apikey" ]; then if [ -n "$apiuser" ]; then - sudo -u docgen curl -u "$apiuser":"$apipass" "http://$apihost/sys/projects/$p/embedded.xml?enable=1&key=$apikey" -d "" + sudo -u docgen curl -u "$apiuser":"$apipass" "$apischeme://$apihost/sys/projects/$p/embedded.xml?enable=1&key=$apikey" -d "" else - sudo -u docgen curl "http://$apihost/sys/projects/$p/embedded.xml?enable=1&key=$apikey" -d "" + sudo -u docgen curl "$apischeme://$apihost/sys/projects/$p/embedded.xml?enable=1&key=$apikey" -d "" fi else echo "Can't enable Embedded, API not configured" 1>&2
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/soundsoftware/extract-matlabdocs.sh Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,47 @@ +#!/bin/bash + +docdir="/var/doc" + +progdir=$(dirname $0) +case "$progdir" in + /*) ;; + *) progdir="$(pwd)/$progdir" ;; +esac + +project="$1" +projectdir="$2" +targetdir="$3" + +if [ -z "$project" ] || [ -z "$targetdir" ] || [ -z "$projectdir" ]; then + echo "Usage: $0 <project> <projectdir> <targetdir>" + exit 2 +fi + +if [ ! -d "$projectdir" ]; then + echo "Project directory $projectdir not found" + exit 1 +fi + +if [ ! -d "$targetdir" ]; then + echo "Target dir $targetdir not found" + exit 1 +fi + +if [ -f "$targetdir/index.html" ]; then + echo "Target dir $targetdir already contains index.html" + exit 1 +fi + +mfile=$(find "$projectdir" -type f -name \*.m -print0 | xargs -0 grep -l '^% ' | head -1) + +if [ -z "$mfile" ]; then + echo "No MATLAB files with comments found for project $project" + exit 1 +fi + +echo "Project $project contains at least one MATLAB file with comments" + +cd "$projectdir" || exit 1 + +perl "$progdir/matlab-docs.pl" -c "$progdir/matlab-docs.conf" -d "$targetdir" +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/soundsoftware/matlab-docs-credit.html Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,1 @@ +<div style="clear: both; float: right"><small><i>Produced by mtree2html by Hartmut Pohlheim</i></small></div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/soundsoftware/matlab-docs.conf Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,166 @@ +# configuration file for generation of html-docu from m-files +# +# Author: Hartmut Pohlheim +# History: 05.11.2000 file created (parameters for mtree2html2001) +# +# The following options/variables must be changed/adapted: +# dirmfiles +# dirhtml +# csslink +# texttitleframelayout +# texttitlefiles +# +# The following options/variables should be adapted: +# authorfile +# filenametopframe +# codeheadmeta + +#======================================================================== +# Variables (possible keywords: set) +# to use the built-in settings, comment the line using # in first column +#======================================================================== + +#------------------------------------------------------------------------ +# dirmfiles: name of directory containing Matlab m-files +# dirhtml: name of directory to place the html-files into +# exthtml: extension used for the html files (.html or .htm) +# don't forget the point in front of the extension +#------------------------------------------------------------------------ +set dirmfiles = . +set dirhtml = doc-output +set exthtml = .html + +#------------------------------------------------------------------------ +# authorfile: name of file containing info about author (in html) +# if defined, this text is included at the bottom of the +# html files +#------------------------------------------------------------------------ +set authorfile = matlab-docs-credit.html + +#------------------------------------------------------------------------ +# csslink: text for linking to css file (style sheets) +# the text defined here is directly included into the head +# of the html file +#------------------------------------------------------------------------ +#set csslink = <link rel=stylesheet type="text/css" href="CSSFILENAME.css" /> + +#------------------------------------------------------------------------ +# links2filescase: this is a bit difficult +# Matlab is case sensitive on UNIX, but case insensitive +# on Windows. Under UNIX Matlab function calls work +# only, when the case of file name and function call are +# identical, under Windows you can do what you want. +# This scripts help you, to keep an exact case in your +# project. +# exact - internal links are only generated, when case of file +# name and in source code are identical +# all - case doesn't matter +# exactupper - same as exact, additionally links are also vreated to +# all upper case function names in source code (often +# used by Mathworks) +# exactvery - same as exact, additionally info about not matching +# case is written to screen (stdout), this can be very +# helpful in cleaning up the case in a project +#------------------------------------------------------------------------ +set links2filescase = all + +#------------------------------------------------------------------------ +# texttitleframelayout: text of title for frame layout file (whole docu) +#------------------------------------------------------------------------ +set texttitleframelayout = MATLAB Function Documentation + +#------------------------------------------------------------------------ +# texttitle/headerindexalldirs: text of title and header for directory index +#------------------------------------------------------------------------ +set texttitleindexalldirs = Index of Directories +set textheaderindexalldirs = Index of Directories + +#------------------------------------------------------------------------ +# texttitle/headerindex: text of title and header for index file +#------------------------------------------------------------------------ +set texttitleindex = A-Z Index of Functions +set textheaderindex = A-Z Index of Functions + +#------------------------------------------------------------------------ +# texttitle/headerfiles: text of title and header for files +# name of file will be added at the end +#------------------------------------------------------------------------ +set texttitlefiles = Function +set textheaderfiles = Documentation of + +#------------------------------------------------------------------------ +# frames: whether to use frames in layout (yes or no) +#------------------------------------------------------------------------ +set frames = no + +#------------------------------------------------------------------------ +# filenametopframe: name of file including frame layout (highest level file) +# [default: index] +#------------------------------------------------------------------------ +set filenametopframe = index + +#------------------------------------------------------------------------ +# textjumpindexglobal: text displayed for jump to index of all files +# (global) +# textjumpindexlocal: text displayed for jump to index of files in actual +# directory (local) +#------------------------------------------------------------------------ +set textjumpindexglobal = <b>Index of</b> all files: +set textjumpindexlocal = this subdirectory only: + +#------------------------------------------------------------------------ +# includesource: include source of m-files in documentation [YES|no] +#------------------------------------------------------------------------ +set includesource = yes + +#------------------------------------------------------------------------ +# usecontentsm: use contents.m files as well for structured +# (hopefully) index [YES|no] +#------------------------------------------------------------------------ +set usecontentsm = no + +#------------------------------------------------------------------------ +# includesource: write/update contents.m files [yes|NO] +#------------------------------------------------------------------------ +set writecontentsm = no + +#------------------------------------------------------------------------ +# processtree: parse whole directory tree recursively [YES|no] +#------------------------------------------------------------------------ +set processtree = yes + +#------------------------------------------------------------------------ +# producetree: produce tree for html-files in same structure than +# tree of m-files [yes|NO] +# if no, all files are saved in the same directory, often +# easier for outside linking to files +#------------------------------------------------------------------------ +set producetree = yes + +#------------------------------------------------------------------------ +# codebodyindex/files: HTML-code for adding to BODY tag +# can be used for defining colors and +# backgroundimages of the files +# No longer recommended, use the css file +#------------------------------------------------------------------------ +set codebodyindex = +set codebodyfiles = + +#------------------------------------------------------------------------ +# codeheadmeta: HTML-code added in HEAD area, use for supplying META info +#------------------------------------------------------------------------ +set codeheadmeta = + +#------------------------------------------------------------------------ +# codehr: HTML-code used to define a <HR>, do what you want +#------------------------------------------------------------------------ +set codehr = <hr> + +#------------------------------------------------------------------------ +# codeheader: HTML-code added to <H*> tags, use for centering header text +# or changing the colour/size/font of the header text +#------------------------------------------------------------------------ +set codeheader = + + +# End of parameter file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/soundsoftware/matlab-docs.pl Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,1585 @@ +@rem = '--*-Perl-*--'; +@rem = ' +@echo off +perl -w -S %0.bat %1 %2 %3 %4 %5 %6 %7 %8 %9 +goto endofperl +@rem '; +# perl -w -S %0.bat "$@" +#!/usr/bin/perl +# +# mtree2html_2000 - produce html files from Matlab m-files. +# use configuration file for flexibility +# can process tree of directories +# +# Copyright (C) 1996-2000 Hartmut Pohlheim. All rights reserved. +# includes small parts of m2html from Jeffrey C. Kantor 1995 +# +# Author: Hartmut Pohlheim +# History: 06.03.1996 file created +# 07.03.1996 first working version +# 08.03.1996 modularized, help text only once included +# 11.03.1996 clean up, some functions rwritten +# 18.04.1996 silent output with writing in one line only +# version 0.20 fixed +# 14.05.1996 start of adding tree structure, could create tree +# 15.05.1996 creating of index files for every directory +# 17.05.1996 first working version except compact A-Z index +# 20.05.1996 cleanup of actual version, more variables and +# configurable settings +# 21.05.1996 reading, update and creation of contents.m added +# 22.05.1996 creation of short index started +# 28.05.1996 jump letters for short index, +# 3 different directory indexes (short/long/contents) +# 29.05.1996 major cleanup, short and long index created from one function +# links for HTML and Indexes from 1 function, +# version 0.9 +# 30.05.1996 contents.m changed to Contents.m (because unix likes it) +# function definition can be in first line of m file before comments +# version 0.91 fixed +# 03.06.1996 contents file can be written as wanted, the links will be correct +# cross references in help block of m-file will be found and +# converted, even if the name of the function is written upper case +# version 0.92 fixed +# 05.06.1996 construction of dependency matrix changed, is able now to process +# even the whole matlab tree (previous version needed to much memory) +# removed warning for contents files in different directories +# version 0.94 fixed +# 06.06.1996 new link name matrices for ConstructHTMLFile created, +# everything is done in ConstructDependencyMatrix, +# both dependencies (calls and called) and matrix +# with all mentioned names in this m-file, thus, much +# less scanning in html construction +# script is now (nearly) linear scalable, thus, matlab-toolbox +# tree takes less than 1 hour on a Pentium120, with source +# version 0.96 fixed +# 10.06.1996 order of creation changed, first all indexes (includes +# update/creation of contents.m) and then ConstructDepency +# thus, AutoAdd section will be linked as well +# excludenames extended, some more common word function names added +# version 0.97 fixed +# 17.02.1998 writecontentsm as command line parameter added +# error of file not found will even appear when silent +# version 1.02 +# 21.05.2000 mark comments in source code specially (no fully correct, +# can't handle % in strings) +# version 1.11 +# 05.11.2000 link also to upper and mixed case m-files +# searching for .m files now really works (doesn't find grep.com any longer) +# file renamed to mtree2html2001 +# generated html code now all lower case +# inclusion of meta-description and meta-keywords in html files +# HTML4 compliance done (should be strict HTML4.0, quite near XHTML) +# version 1.23 +# +# 29.03.2011 (Chris Cannam) add frames option. + +$VERSION = '1.23'; +($PROGRAM = $0) =~ s@.*/@@; $PROGRAM = "\U$PROGRAM\E"; +$debug = 1; + +#------------------------------------------------------------------------ +# Define platform specific things +#------------------------------------------------------------------------ +# suffix for files to search is defined twice +# the first ($suffix) is for string creation and contains the . as well +# the second ($suffixforsearch) is for regular expression, handling of . is quite special +$suffix = ".m"; +$suffixforsearch = "m"; +# the directory separator +$dirsep = "/"; +# what is the current directory +$diract = "."; + +#------------------------------------------------------------------------ +# Define all variables and their standard settings +# documentation of variables is contained in accompanying rc file +#------------------------------------------------------------------------ +%var = +( + 'authorfile', '', + 'codebodyfiles', '', + 'codebodyindex', '', + 'codeheadmeta', '<meta name="author of conversion perl script" content="Hartmut Pohlheim" />', + 'codehr', '<hr size="3" noshade="noshade" />', + 'codeheader', '', + 'configfile', 'matlab-docs.conf', + 'csslink', '', + 'dirmfiles', $diract, + 'dirhtml', $diract, + 'exthtml', '.html', + 'frames', 'yes', + 'filenametopframe', 'index', + 'filenameindexlongglobal', 'indexlg', + 'filenameindexlonglocal', 'indexll', + 'filenameindexshortglobal', 'indexsg', + 'filenameindexshortlocal', 'indexsl', + 'filenameextensionframe', 'f', + 'filenameextensionindex', 'i', + 'filenameextensionjump', 'j', + 'filenamedirshort', 'dirtops', + 'filenamedirlong', 'dirtopl', + 'filenamedircontents', 'dirtopc', + 'includesource', 'yes', + 'links2filescase', 'all', + 'processtree', 'yes', + 'producetree', 'yes', + 'textjumpindexlocal', 'Local Index', + 'textjumpindexglobal', 'Global Index', + 'texttitleframelayout', 'Documentation of Matlab Files', + 'texttitleindexalldirs', 'Index of Directories', + 'textheaderindexalldirs', 'Index of Directories', + 'texttitleindex', '', + 'textheaderindex', '', + 'texttitlefiles', 'Documentation of ', + 'textheaderfiles', 'Documentation of ', + 'usecontentsm', 'yes', + 'writecontentsm', 'no' +); + + +# define all m-file names, that should be excluded from linking +# however, files will still be converted +@excludenames = ( 'all','ans','any','are', + 'cs', + 'demo','dos', + 'echo','edit','else','elseif','end','exist', + 'flag','for','function', + 'global', + 'help', + 'i','if','inf','info', + 'j', + 'more', + 'null', + 'return', + 'script','strings', + 'what','which','while','who','whos','why', + ); + +# Text for inclusion in created HTML/Frame files: Doctype and Charset +$TextDocTypeHTML = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">'; +$TextDocTypeFrame = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN" "http://www.w3.org/TR/REC-html40/frameset.dtd">'; +$TextMetaCharset = '<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />'; + +#------------------------------------------------------------------------ +# Read the command line arguments +#------------------------------------------------------------------------ +if (@ARGV == 0) { + &DisplayHelp() if &CheckFileName($var{'configfile'}, 'configuration file'); +} + +# Print provided command line arguments on screen +foreach (@ARGV) { print " $_\n "; } + +# Get the options +use Getopt::Long; +@options = ('help|h', 'todo|t', 'version|v', + 'authorfile|a=s', 'configfile|c=s', 'dirhtml|html|d=s', + 'dirmfiles|mfiles|m=s', 'includesource|i=s', + 'processtree|r=s', 'producetree|p=s', + 'silent|quiet|q', 'writecontentsm|w=s'); +&GetOptions(@options) || die "use -h switch to display help statement\n"; + + +# Display help or todo list, when requested +&DisplayHelp() if $opt_help; +&DisplayTodo() if $opt_todo; +die "$PROGRAM v$VERSION\n" if $opt_version; + +$exit_status = 0; + +#------------------------------------------------------------------------ +# Read the config file +#------------------------------------------------------------------------ +$var{'configfile'} = $opt_configfile if $opt_configfile; +&GetConfigFile($var{'configfile'}); + + +#------------------------------------------------------------------------ +# Process/Check the command line otions +#------------------------------------------------------------------------ +$var{'dirhtml'} = $opt_dirhtml if $opt_dirhtml; +if (!(substr($var{'dirhtml'}, -1, 1) eq $dirsep)) { $var{'dirhtml'} = $var{'dirhtml'}.$dirsep; } +$var{'dirmfiles'} = $opt_dirmfiles if $opt_dirmfiles; +if (!(substr($var{'dirmfiles'}, -1, 1) eq $dirsep)) { $var{'dirmfiles'} = $var{'dirmfiles'}.$dirsep; } + +$var{'authorfile'} = $opt_author if $opt_author; +$var{'includesource'} = $opt_includesource if $opt_includesource; +if ($var{'includesource'} ne 'no') { $var{'includesource'} = 'yes'; } +$var{'processtree'} = $opt_processtree if $opt_processtree; +if ($var{'processtree'} ne 'no') { $var{'processtree'} = 'yes'; } +$var{'producetree'} = $opt_producetree if $opt_producetree; +if ($var{'producetree'} ne 'no') { $var{'producetree'} = 'yes'; } +if ($var{'processtree'} eq 'no') { $var{'producetree'} = 'no'; } +if ($var{'frames'} ne 'no') { $var{'frames'} = 'yes'; } +# if (($var{'processtree'} eq 'yes') && ($var{'producetree'} eq 'no')) { $var{'usecontentsm'} = 'no'; } + +$var{'writecontentsm'} = $opt_writecontentsm if $opt_writecontentsm; + +#------------------------------------------------------------------------ +# Do the real stuff +#------------------------------------------------------------------------ + +# Print variables on screen, when not silent +&ListVariables if !$opt_silent; + +# Check the author file +if ($var{'authorfile'} ne '') { + if (!($var{'authorfile'} =~ m,^/,)) { + # relative path: treat as relative to config file + my $cfd = $var{'configfile'}; + $cfd =~ s,/[^/]*$,/,; + $cfd =~ s,^[^/]*$,.,; + $var{'authorfile'} = "$cfd/" . $var{'authorfile'}; + } + if (&CheckFileName($var{'authorfile'}, 'author file')) { + $var{'authorfile'} = ''; + if (!$opt_silent) { print " Proceeding without author information!\n"; } + } +} + +# Call the function doing all the real work +&ConstructNameMatrix; + +&ConstructDependencyMatrix; + +&ConstructAllIndexFiles; + +&ConstructHTMLFiles; + +exit $exit_status; + +#------------------------------------------------------------------------ +# Construct list of all mfile names and initialize various data arrays. +#------------------------------------------------------------------------ +sub ConstructNameMatrix +{ + local(*MFILE); + local($file, $dirname); + local(@newdirectories); + local(%localnames); + + $RecDeep = 0; + &ParseTreeReadFiles($var{'dirmfiles'}, $RecDeep); + + foreach $dirname (@directories) { + if ($dirnumbermfiles{$dirname} > 0) { + push(@newdirectories, $dirname); + if (! defined($contentsname{$dirname})) { + $contentsname{$dirname} = 'Contents'; + if (($var{'writecontentsm'} eq 'no') && ($var{'usecontentsm'} eq 'yes')) { + print "\r ParseTree - for directory $dirname no contents file found!\n"; + print " create one or enable writing of contents file (writecontentsm = yes)!\n"; + } + } + } + } + @alldirectories = @directories; + @directories = @newdirectories; + + foreach $dirname (@directories) { + if ($debug > 0) { print "Dir: $dirname \t\t $dirnumbermfiles{$dirname} \t$contentsname{$dirname}\n"; } + } + + @names = sort(keys %mfile); + + # check, if name of directory is identical to name of file + @dirsinglenames = values(%dirnamesingle); + grep($localnames{$_}++, @dirsinglenames); + @dirandfilename = grep($localnames{$_}, @names); + if (@dirandfilename) { + print "\r Name clash between directory and file name: @dirandfilename\n"; + print " These files will be excluded from linking!\n"; + push(@excludenames, @dirandfilename); + } + + # construct names matrix for help text linking + # exclude some common words (and at the same time m-functions) from linking in help text + grep($localnames{$_}++, @excludenames); + @linknames = grep(!$localnames{$_}, @names); + + if ($debug > 2) { print "linknames (names of found m-files):\n @linknames\n"; } + +} + +#------------------------------------------------------------------------ +# Parse tree and collect all Files +#------------------------------------------------------------------------ +sub ParseTreeReadFiles +{ + local($dirname, $localRecDeep) = @_; + local($file, $name, $filewosuffix); + local($dirhtmlname, $dirmode); + local($relpath, $relpathtoindex, $replacevardir); + local(*CHECKDIR, *AKTDIR); + local(@ALLEFILES); + + opendir(AKTDIR, $dirname) || die "ParseTree - Can't open directory $dirname: $!"; + if ($debug > 1) { print "\nDirectory: $dirname\n"; } + + # create relative path + $_ = $dirname; $replacevardir = $var{'dirmfiles'}; + s/$replacevardir//; $relpath = $_; + s/[^\/]+/../g; $relpathtoindex = $_; + + # producetree no + if ($var{'producetree'} eq 'no') { $relpath = ''; $relpathtoindex = ''; } + + # names of directories (top-level and below top-level m-file-directory) + push(@directories, $dirname); + $dirnumbermfiles{$dirname} = 0; # set number of m-files for this dir to zero + # relative path from top-level directory, depends on directory name + $dirnamerelpath{$dirname} = $relpath; + # relative path from actual directory to top-level directory, depends on directory name + $dirnamerelpathtoindex{$dirname} = $relpathtoindex; + # recursion level for directory, depends on directory name + $dirnamerecdeep{$dirname} = $localRecDeep; + + # only the name of the directory, without path + $rindexprint = rindex($dirname, $dirsep, length($dirname)-2); + $rindsub = substr($dirname, $rindexprint+1, length($dirname)-$rindexprint-2); + $dirnamesingle{$dirname} = $rindsub; + + # create name of html-directories + $_ = $dirname; + s/$var{'dirmfiles'}/$var{'dirhtml'}/; + $dirhtmlname = $_; + if ($var{'producetree'} eq 'no') { $dirhtmlname = $var{'dirhtml'}; } + # try to open html directory, if error, then create directory, + # use same mode as for corresponding m-file directory + opendir(CHECKDIR,"$dirhtmlname") || do { + $dirmode = (stat($dirname))[2]; # print "$dirmode\n"; + mkdir("$dirhtmlname", $dirmode) || die ("Cannot create directory $dirhtmlname: $! !"); + }; + closedir(CHECKDIR); + + + # read everything from this directory and process them + @ALLEFILES = readdir(AKTDIR); + + foreach $file (@ALLEFILES) { + # exclude . and .. directories + next if $file eq '.'; next if $file eq '..'; + + # test for existense of entry (redundant, used for debugging) + if (-e $dirname.$file) { + # if it's a directory, call this function recursively + if (-d $dirname.$file) { + if ($var{'processtree'} eq 'yes') { + &ParseTreeReadFiles($dirname.$file.$dirsep, $localRecDeep+1); + } + } + # if it's a file - test for m-file, save name and create some arrays + elsif (-f $dirname.$file) { + if ($file =~ /\.$suffixforsearch$/i) { + # Remove the file suffix to establish the matlab identifiers + $filewosuffix = $file; + $filewosuffix =~ s/\.$suffixforsearch$//i; + # $filename = $name; + + # Contents file in unix must start with a capital letter (Contents.m) + # ensure, that m-file name is lower case, except the contents file + if (! ($filewosuffix =~ /^contents$/i)) { + # if ($var{'links2filescase'} eq 'low') { $filewosuffix = "\L$filewosuffix\E"; } + $filewosuffixlow = "\L$filewosuffix\E"; + } + else { $contentsname{$dirname} = $filewosuffix; } + + # internal handle name is always lower case + $name = $filewosuffixlow; + # file name is not lower case + $filename = $filewosuffix; + + # if don't use C|contents.m, then forget all C|contents.m + if ($var{'usecontentsm'} eq 'no') { if ($name =~ /contents/i) { next; } } + + # if m-file with this name already exists, use directory and name for name + # only the first occurence of name will be used for links + if (defined $mfile{$name}) { + if (! ($name =~ /^contents$/i) ) { + print "\r ParseTree - Name conflict: $name in $dirname already exists: $mfile{$name} !\n"; + print " $mfile{$name} will be used for links!\n"; + } + $name = $dirname.$name; + } + # mfile name with path + $mfile{$name} = $dirname.$file; + # mfile name (without path) + $mfilename{$name} = $filename; + # mfile directory + $mfiledir{$name} = $dirname; + + # html file name and full path, special extension of Contents files + if ($name =~ /contents/i) { $extrahtmlfilename = $dirnamesingle{$dirname}; } + else { $extrahtmlfilename = ''; } + $hfile{$name} = $dirhtmlname.$mfilename{$name}.$extrahtmlfilename.$var{'exthtml'}; + + # save relative html path + # if ($var{'producetree'} eq 'yes') { + $hfilerelpath{$name} = $relpath; + # } else { # if no tree to produce, relative path is empty + # $hfilerelpath{$name} = ''; + # } + + # create relative path from html file to directory with global index file + $hfileindexpath{$name} = $relpathtoindex; + + # Function declaration, if one exists, set default to script + $synopsis{$name} = ""; + $mtype{$name} = "script"; + + # First comment line + $apropos{$name} = ""; + + # count number of m-files in directories + $dirnumbermfiles{$dirname}++; + + if ($debug > 1) { + if ($opt_silent) { print "\r"; } + print " ParseTree: $name \t\t $mfile{$name} \t\t $hfile{$name}\t\t"; + if (!$opt_silent) { print "\n"; } + } + } + } + else { + print "Unknown type of file in $dirname: $file\n"; + } + } + else { print "Error: Not existing file in $dirname: $file\n"; } + } + + closedir(AKTDIR) + +} + +#------------------------------------------------------------------------ +# Construct Dependency matrix +# $dep{$x,$y} > 0 if $x includes a reference to $y. +#------------------------------------------------------------------------ +sub ConstructDependencyMatrix +{ + &ConstructDependencyMatrixReadFiles('all'); + &ConstructDependencyMatrixReally; +} + + +#------------------------------------------------------------------------ +# Construct Dependency matrix +# $dep{$x,$y} > 0 if $x includes a reference to $y. +#------------------------------------------------------------------------ +sub ConstructDependencyMatrixReadFiles +{ + local($whatstring) = @_; + local(*MFILE); + local($name, $inames); + local(%symbolsdep, %symbolsall); + + # Initialize as all zeros. + # foreach $name (@names) { grep($dep{$name,$_}=0,@names); if ($debug > 0) { print "\r DepMatrix anlegen: $name\t$#names\t"; } } + + # Compute the dependency matrix + $inames = -1; + foreach $name (@names) { + # Read each file and tabulate the distinct alphanumeric identifiers in + # an array of symbols. Also scan for: + # synopsis: The function declaration line + # apropos: The first line of the help text + + # look for whatstring, if all: process every file, if contents: process only contents files + if ($whatstring eq 'contents') { if (! ($name =~ /contents$/i) ) { next; } } + elsif ($whatstring eq 'all') { } # do nothing + else { print "\r ConstructDependency: Unknown parameter whatstring: $whatstring \n"; } + + undef %symbolsall; undef %symbolsdep; + open(MFILE,"<$mfile{$name}") || die("Can't open $mfile{$name}: $!\n"); + while (<MFILE>) { + chop; + + # Split on nonalphanumerics, then look for all words, used for links later + # this one for all references + @wordsall = grep(/[a-zA-Z]\w*/, split('\W',$_)); + # set all words to lower case for link checking + undef @wordsall2; + # do case conversion not, case checking is done later + foreach (@wordsall) { push(@wordsall2, "\L$_\E"); } + # @wordsall2 = @wordsall; + grep($symbolsall{$_}++, @wordsall2); + + # Store first comment line, skip all others. + if (/^\s*%/) { + if (!$apropos{$name}) { + s/^\s*%\s*//; # remove % and leading white spaces on line + $_ = &SubstituteHTMLEntities($_); + $apropos{$name} = $_; + } + next; + } + + # If it's the function declaration line, then store it and skip + # but only, when first function definition (multiple function lines when private subfunctions in file + if ($synopsis{$name} eq '') { + if (/^\s*function/) { + s/^\s*function\s*//; + $synopsis{$name} = $_; + $mtype{$name} = "function"; + next; + } + } + + # Split off any trailing comments + if ($_ ne '') { + # this one for references in program code only + # when syntax parsing, here is a working place + ($statement) = split('%',$_,1); + @wordsdep = grep(/[a-zA-Z]\w*/,split('\W',$statement)); + # do case conversion not, case checking is done later + undef @wordsdep2; + foreach (@wordsdep) { push(@wordsdep2, "\L$_\E"); } + grep($symbolsdep{$_}++, @wordsdep2); + } + } + close MFILE; + + # compute intersection between %symbolsall and @linknames + delete($symbolsall{$name}); + # foreach $localsumall ($symbolsall) { + # $localsumall = "\L$localsumall\E"; + # } + @{'all'.$name} = grep($symbolsall{$_}, @linknames); + + # compute intersection between %symbolsdep and @linknames + delete($symbolsdep{$name}); + @{'depcalls'.$name} = grep($symbolsdep{$_}, @linknames); + + $inames++; print "\r DepCallsMatrix: $inames/$#names\t $name\t"; + if ($debug > 2) { print "\n depnames: @{'depcalls'.$name}\n all: @{'all'.$name}\n"; } + } +} + + +#------------------------------------------------------------------------ +# Construct Dependency matrix +# $dep{$x,$y} > 0 if $x includes a reference to $y. +#------------------------------------------------------------------------ +sub ConstructDependencyMatrixReally +{ + local($inames, $name); + + $inames = -1; + foreach $name (@names) { undef %{'depint'.$name}; } + foreach $name (@names) { + grep(${'depint'.$_}{$name}++, @{'depcalls'.$name}); + $inames++; print "\r DepCalledMatrix1: $inames/$#names\t $name\t"; + } + $inames = -1; + foreach $name (@names) { + # compute intersection between %depint.name{$_} and @linknames + if (defined (%{'depint'.$name})) { @{'depcalled'.$name} = grep(${'depint'.$name}{$_}, @linknames); } + $inames++; print "\r DepCalledMatrix2: $inames/$#names\t $name\t"; + if ($debug > 2) { print "\n depcalled: @{'depcalled'.$name}\n"; } + } + +} + + +#======================================================================== +# Construct all index files +#======================================================================== +sub ConstructAllIndexFiles +{ + local(@localnames); + local($ActDir); + local($name); + + # define variables and names for frame target + $GlobalNameFrameMainLeft = 'Cont_Main'; + $GlobalNameFrameMainRight = 'Cont_Lower'; + $GlobalNameFrameAZIndexsmall = 'IndexAZindex'; + $GlobalNameFrameAZIndexjump = 'IndexAZjump'; + + $indexcreated = 0; + + &ConstructHighestIndexFile; + $indexcreated++; + + # if ($var{'producetree'} eq 'yes') { + # moved next 2 lines out of if for producetree no + # &ConstructHighestIndexFile; + # $indexcreated++; + + foreach $ActDir (@directories) { + undef @localnames; + foreach $name (@names) { + local($pathsubstr) = substr($mfile{$name}, 0, rindex($mfile{$name}, "/")+1); + if ($ActDir eq $pathsubstr) { + if ($debug > 1) { print "IndexFile: $pathsubstr ActDir: $ActDir Hfilerelpath: $hfilerelpath{$name}\n"; } + push(@localnames, $name); + } + } + if ($debug > 2) { print "localnames: @localnames\n"; } + # create contents file and short|long index of files in local directory + &ConstructContentsmFile($ActDir, @localnames); + &ConstructAZIndexFile($ActDir, 'short', 'local', @localnames); + &ConstructAZIndexFile($ActDir, 'long', 'local', @localnames); + $indexcreated+=2; + } + # } else { + # &ConstructContentsmFile($var{'dirmfiles'}, @names); + # } + + # create short|long index of files in all directory + &ConstructAZIndexFile($var{'dirmfiles'}, 'short', 'global', @names); + &ConstructAZIndexFile($var{'dirmfiles'}, 'long', 'global', @names); + $indexcreated+=2; + + # if contents.m were created or updated, the dependency matrices should + # be updated as well + if ($var{'writecontentsm'} eq 'yes') { &ConstructDependencyMatrixReadFiles('contents');; } +} + + +#======================================================================== +# Construct the highest level index file +#======================================================================== +sub ConstructHighestIndexFile +{ + local(*IFILE); + local($indexfile, $filename); + + # Build the frame layout file, this files includes the layout of the frames + # Build the frame layout file name (highest one) + $indexfile = $var{'dirhtml'}.$var{'filenametopframe'}.$var{'exthtml'}; + + if ($var{'frames'} eq 'yes') { + + open(IFILE,">$indexfile") || die("Cannot open frame layout file $indexfile\n"); + + # Write the header of frame file + print IFILE "$TextDocTypeFrame\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n"; + print IFILE " <title>$var{'texttitleframelayout'}</title>\n"; + print IFILE "</head>\n"; + + # definition of 2 frames, left the tree of directories, + # right the index of that directory or the docu of a file + print IFILE "<frameset cols=\"25%,75%\">\n"; + print IFILE " <frame src=\"$var{'filenamedirshort'}$var{'exthtml'}\" name=\"$GlobalNameFrameMainLeft\" />\n"; + print IFILE " <frame src=\"$var{'filenameindexshortglobal'}$var{'filenameextensionframe'}$var{'exthtml'}\" name=\"$GlobalNameFrameMainRight\" />\n"; print IFILE "</frameset>\n"; + + print IFILE "</html>\n"; + + close(IFILE); + + if ($opt_silent) { print "\r"; } + print " Frame layout file created: $indexfile\t"; + if (!$opt_silent) { print "\n"; } + } + + for($irun=0; $irun <= 2; $irun++) { + # Build the top directory index file, these files include the directory tree + # Build the directory tree index file name + + # Create no directory file for contents, when no contents to use + if (($irun == 2) && ($var{'usecontentsm'} eq 'no')) { next; } + + # Assign the correct index file name + if ($irun == 0) { $filename = $var{'filenamedirshort'}; } + elsif ($irun == 1) { $filename = $var{'filenamedirlong'}; } + elsif ($irun == 2) { $filename = $var{'filenamedircontents'}; } + + $indexfile = $var{'dirhtml'}.$filename.$var{'exthtml'}; + + open(IFILE,">$indexfile") || die("Cannot open directory tree index file $indexfile\n"); + # Write header of HTML file + print IFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n"; + + if ($var{'texttitleindexalldirs'} eq '') { + print IFILE "<title>Index of Directories of $var{'dirmfiles'}</title>\n"; + } else { + print IFILE "<title>$var{'texttitleindexalldirs'}</title>\n"; + } + + if ($var{'frames'} eq 'yes') { + print IFILE "<base target=\"$GlobalNameFrameMainRight\" />\n"; + } + + print IFILE "</head>\n"; + print IFILE "<body $var{'codebodyindex'}>\n"; + print IFILE "<div id=\"matlabdoc\">\n"; + if ($var{'textheaderindexalldirs'} eq '') { + print IFILE "<h1 $var{'codeheader'}>Index of Directories of <em>$var{'dirmfiles'}</em></h1>\n"; + } else { + print IFILE "<h1 $var{'codeheader'}>$var{'textheaderindexalldirs'}</h1>\n"; + } + print IFILE "<p>\n"; + + if ($var{'frames'} eq 'yes') { + if ($irun == 0) { print IFILE "<strong>short</strong>\n"; } + else { print IFILE "<a href=\"$var{'filenamedirshort'}$var{'exthtml'}\" target=\"$GlobalNameFrameMainLeft\">short</a>\n"; } + if ($irun == 1) { print IFILE " | <strong>long</strong>\n"; } + else { print IFILE " | <a href=\"$var{'filenamedirlong'}$var{'exthtml'}\" target=\"$GlobalNameFrameMainLeft\">long</a>\n"; } + if ($var{'usecontentsm'} eq 'yes') { + if ($irun == 2) { print IFILE " | <strong>contents</strong>\n"; } + else { print IFILE " | <a href=\"$var{'filenamedircontents'}$var{'exthtml'}\" target=\"$GlobalNameFrameMainLeft\">contents</a>\n"; } + } + } else { + if ($irun == 0) { print IFILE "<strong>short</strong>\n"; } + else { print IFILE "<a href=\"$var{'filenamedirshort'}$var{'exthtml'}\">short</a>\n"; } + if ($irun == 1) { print IFILE " | <strong>long</strong>\n"; } + else { print IFILE " | <a href=\"$var{'filenamedirlong'}$var{'exthtml'}\">long</a>\n"; } + if ($var{'usecontentsm'} eq 'yes') { + if ($irun == 2) { print IFILE " | <strong>contents</strong>\n"; } + else { print IFILE " | <a href=\"$var{'filenamedircontents'}$var{'exthtml'}\">contents</a>\n"; } + } + } + + print IFILE "</p><br />\n\n"; + print IFILE "<ul>\n"; + + # go through all directories and create a list entry for each one, + # depending on recursion level create sublists + $prevrecdeeplevel = 0; + foreach $name (@alldirectories) { + $actrecdeeplevel = $dirnamerecdeep{$name}; + for( ; $prevrecdeeplevel < $actrecdeeplevel; $prevrecdeeplevel++ ) { print IFILE "<ul>\n"; } + for( ; $prevrecdeeplevel > $actrecdeeplevel; $prevrecdeeplevel-- ) { print IFILE "</ul>\n"; } + if ($irun == 0) { $indexfilenameused = $var{'filenameindexshortlocal'}.$var{'filenameextensionframe'}; } + elsif ($irun == 1) { $indexfilenameused = $var{'filenameindexlonglocal'}.$var{'filenameextensionframe'}; } + elsif ($irun == 2) { $indexfilenameused = $contentsname{$name}; } + else { die "ConstructHighestIndexFile: Unknown value of irun"; } + if ($dirnumbermfiles{$name} > 0) { + # producetree no + # if ($var{'producetree'} eq 'no') { $dirnamehere = ''; } + # else { $dirnamehere = '$dirnamerelpath{$name}'; } + # print IFILE "<LI><A HREF=\"$dirnamehere$indexfilenameused_$dirnamesingle{$name}$var{'exthtml'}\">$dirnamesingle{$name}</A>\n"; + print IFILE "<li><a href=\"$dirnamerelpath{$name}$indexfilenameused$dirnamesingle{$name}$var{'exthtml'}\">$dirnamesingle{$name}</a></li>\n"; + } else { + # print directories with no m-files inside not + # print IFILE "<li>$dirnamesingle{$name}</li>\n"; + } + } + $actrecdeeplevel = 0; + for( ; $prevrecdeeplevel > $actrecdeeplevel; $prevrecdeeplevel-- ) { print IFILE "</ul>\n"; } + print IFILE "</ul>\n<br />$var{'codehr'}\n"; + + # Include info about author from authorfile + &WriteFile2Handle($var{'authorfile'}, IFILE); + + print IFILE "<!--navigate-->\n"; + print IFILE "<!--copyright-->\n"; + print IFILE "</div>\n</body>\n</html>\n"; + + close(IFILE); + + if ($opt_silent) { print "\r"; } + print " Directory - Indexfile created: $indexfile\t"; + if (!$opt_silent) { print "\n"; } + } +} + + +#======================================================================== +# Construct the A-Z index file (global/local and/or short/long) +#======================================================================== +sub ConstructAZIndexFile +{ + local($LocalActDir, $LocalShortLong, $LocalGlobalLocal, @localnames) = @_; + local(*IFILE); + local($name, $indexfilename, $dirpath); + local($firstletter, $firstone); + + if ($debug > 2) { print "localnames in AZ small: @localnames\n"; print " ActDir in A-Z: $LocalActDir\n"; } + + # extract filename of index file from parameters of function + if ($LocalShortLong eq 'short') { + if ($LocalGlobalLocal eq 'global') { $indexfilename = $var{'filenameindexshortglobal'}; } + elsif ($LocalGlobalLocal eq 'local') { $indexfilename = $var{'filenameindexshortlocal'}; } + else { die "wrong parameter for LocalGlobalLocal in ConstructAZIndexFile: $LocalGlobalLocal."; } + } elsif ($LocalShortLong eq 'long') { + if ($LocalGlobalLocal eq 'global') { $indexfilename = $var{'filenameindexlongglobal'}; } + elsif ($LocalGlobalLocal eq 'local') { $indexfilename = $var{'filenameindexlonglocal'}; } + else { die "wrong parameter for LocalGlobalLocal in ConstructAZIndexFile: $LocalGlobalLocal."; } + } else { die "wrong parameter for LocalShortLong in ConstructAZIndexFile: $LocalShortLong."; } + + # producetree no + # if ($var{'producetree'} eq 'no') { $dirnamehere = ''; } + # else { $dirnamehere = '$dirnamerelpath{$LocalActDir}'; } + # Build the index file name + # handle the global index file case separately (no extra directory name in file) + # the local index file name must be extended by the name of the directory + if ($LocalGlobalLocal eq 'global') { $extradirfilename = ''; } + else { $extradirfilename = $dirnamesingle{$LocalActDir}; } + $indexfile = $var{'dirhtml'}.$dirnamerelpath{$LocalActDir}.$indexfilename.$var{'filenameextensionindex'}.$extradirfilename.$var{'exthtml'}; + + if ($LocalShortLong eq 'short' and $extradirfilename eq '' and $var{'frames'} ne 'yes') { + # With no frames and no subdir path, this must go in the + # top-level index file instead + $indexfile = $var{'dirhtml'}.$var{'filenametopframe'}.$var{'exthtml'}; + } + + if ($debug > 2) { print " indexfilename (a-z small): $indexfile\n"; } + + open(IFILE,">$indexfile") || die("Cannot open index file $indexfile: $!\n"); + + # Write the header of HTML file + print IFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n"; + + my $dirToPrint = $LocalActDir; + $dirToPrint =~ s,^./,,; + + if ($var{'texttitleindex'} eq '') { + print IFILE "<title>Index of Matlab Files in Directory $dirToPrint</title>\n"; + } else { + if ($LocalGlobalLocal eq 'global') { print IFILE "<title>$var{'texttitleindex'}</title>\n"; } + else { print IFILE "<title>$var{'texttitleindex'} in Directory $dirToPrint</title>\n"; } + } + + if ($var{'frames'} eq 'yes') { + print IFILE "<base target=\"$GlobalNameFrameMainRight\" />\n"; + } + print IFILE "</head>\n"; + + print IFILE "<body $var{'codebodyindex'}>\n"; + print IFILE "<div id=\"matlabdoc\">\n"; + if ($var{'textheaderindex'} eq '') { + print IFILE "<h1 $var{'codeheader'}>Index of Matlab Files in Directory $dirToPrint</h1>\n"; + } else { + if ($LocalGlobalLocal eq 'global') { print IFILE "<h1 $var{'codeheader'}>$var{'textheaderindex'}</h1>\n"; } + else { print IFILE "<h1 $var{'codeheader'}>$var{'textheaderindex'} in Directory $dirToPrint</h1>\n"; } + } + + # include links to indexes + &ConstructLinks2Index(IFILE, $dirnamerelpathtoindex{$LocalActDir}, $LocalActDir, $LocalGlobalLocal); + + # Collect the starting letters of m files in this directory or all m-files + for('a'..'z') { undef @{$_}; } + foreach $name (@localnames) { + if (! ($mfilename{$name} =~ /contents/i)) { + $firstletter = substr($mfilename{$name}, 0, 1); + # convert first letter always to lower case + # needed for reference to lower and upper case m-files + $firstletter = "\L$firstletter\E"; + push(@{$firstletter}, $name); + } + } + + if ($LocalShortLong eq 'short') { + # begin create short index + print IFILE "<table width=\"100%\">\n"; + + for('a'..'z') { + # print " $_: @{$_}\n"; + $numberofletter = $#{$_}+1; + $cols = 3; + if ($numberofletter > 0) { + print IFILE "\n<tr><td><br/><strong><a name=\"\U$_\E$_\"></a><span class=\"an\">\U$_\E</span></strong></td></tr>\n"; + for ($count = 0; $count < $numberofletter; $count++) { + if (($count % $cols) == 0) { + if ($count > 0) { + print IFILE "</tr><tr>\n"; + } + print IFILE "<tr><td></td>"; + } + $name = @{$_}[$count]; + if ($LocalGlobalLocal eq 'global') { $dirpath = $hfilerelpath{$name}; } else { $dirpath = ""; } + print IFILE "<td><a href=\"$dirpath$mfilename{$name}$var{'exthtml'}\">$mfilename{$name}</a></td>"; + } + + print IFILE "</tr>\n"; + + # $numberhalf = ($numberofletter + 1 - (($numberofletter+1) % 2))/2; + # if ($debug > 2) { print " $_: @{$_} \t $numberhalf \t $numberofletter\n"; } + # for($count = 0; $count < $numberhalf; $count++) { + # $name = @{$_}[$count]; + # if ($LocalGlobalLocal eq 'global') { $dirpath = $hfilerelpath{$name}; } else { $dirpath = ""; } + # print IFILE "<tr><td width=\"50%\"><a href=\"$dirpath$mfilename{$name}$var{'exthtml'}\">$mfilename{$name}</a></td>"; + # if (($count + $numberhalf) < $numberofletter) { + # $name = @{$_}[$count + $numberhalf]; + # if ($LocalGlobalLocal eq 'global') { $dirpath = $hfilerelpath{$name}; } else { $dirpath = ""; } + # print IFILE "<td width=\"50%\"><a href=\"$dirpath$mfilename{$name}$var{'exthtml'}\">$mfilename{$name}</a></td></tr>\n"; + # } else { + # print IFILE "<td width=\"50%\"></td></tr>\n"; + # } + # } + } + } + print IFILE "</table>\n<br />$var{'codehr'}\n"; + + } elsif ($LocalShortLong eq 'long') { + # begin create long index + print IFILE "<table width=\"100%\">\n"; + print IFILE "<tr><th>Name</th><th>Synopsis</th></tr>\n"; + + for('a'..'z') { + # print " $_: @{$_}\n"; + $numberofletter = $#{$_}+1; + if ($numberofletter > 0) { + $firstone = 1; + foreach $name (@{$_}) { + if ($debug > 1) { print " AZinforeach1: $name \t\t $hfilerelpath{$name} \t\t $dirnamerelpath{$LocalActDir}\n"; } + if ($LocalGlobalLocal eq 'global') { $dirpath = $hfilerelpath{$name}; } else { $dirpath = ""; } + if (! ($mfilename{$name} =~ /contents/i)) { + if ($firstone == 1) { print IFILE "\n<tr><td colspan=\"2\"><br /><strong><a name=\"\U$_\E$_\"></a><span class=\"an\">\U$_\E</span></strong></td></tr>\n"; $firstone = 0; } + print IFILE "<tr><td valign=\"top\"><a href=\"$dirpath$mfilename{$name}$var{'exthtml'}\">$mfilename{$name}</a></td><td>$apropos{$name}</td></tr>\n"; + } + } + } + } + print IFILE "</table>\n<br />$var{'codehr'}\n"; + } else { die "wrong parameter for LocalShortLong in ConstructAZIndexFile: $LocalShortLong."; } + + # Include info about author from authorfile + &WriteFile2Handle($var{'authorfile'}, IFILE); + + print IFILE "<!--navigate-->\n"; + print IFILE "<!--copyright-->\n"; + print IFILE "</div>\n</body>\n</html>\n"; + + close(IFILE); + + if ($opt_silent) { print "\r"; } + print " Indexfile small (A-Z) created: $indexfile\t"; + if (!$opt_silent) { print "\n"; } + + + # Build the A-Z jump index file name + # handle the global index file case separately (no extra directory name in file) + if ($LocalGlobalLocal eq 'global') { $extradirfilename = ''; } + else { $extradirfilename = $dirnamesingle{$LocalActDir}; } + + if ($var{'frames'} eq 'yes') { + + $indexfile = $var{'dirhtml'}.$dirnamerelpath{$LocalActDir}.$indexfilename.$var{'filenameextensionjump'}.$extradirfilename.$var{'exthtml'}; + if ($debug > 2) { print " indexfilename (a-z jump): $indexfile\n"; } + open(IFILE,">$indexfile") || die("Cannot open jump index file $indexfile: $!\n"); + + # Write the header of HTML file + print IFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n"; + + if ($var{'texttitleindex'} eq '') { + print IFILE "<title>A-Z jump index in directory $dirToPrint</title>\n"; + } else { + if ($LocalGlobalLocal eq 'global') { print IFILE "<title>$var{'texttitleindex'}</title>\n"; } + else { print IFILE "<title>$var{'texttitleindex'} in Directory $dirToPrint</title>\n"; } + } + + if ($var{'frames'} eq 'yes') { + print IFILE "<base target=\"$GlobalNameFrameAZIndexsmall\" />\n"; + } + print IFILE "</head>\n"; + print IFILE "<body $var{'codebodyindex'}>\n"; + print IFILE "<div id=\"matlabdoc\">\n"; + + # Write the A-Z jump line, generate link for letters with files starting with this letter + # and only letters for no files starting with this letter + # use previously generated arrays with names of files sorted by starting letter + for('a'..'z') { + $numberofletter = $#{$_}+1; + if ($numberofletter > 0) { + print IFILE "<strong><a href=\"$indexfilename$var{'filenameextensionindex'}$extradirfilename$var{'exthtml'}#\U$_\E$_\">\U$_\E</a> </strong>\n"; + } else { + print IFILE "\U$_\E \n"; + } + } + + print IFILE "</div></body>\n</html>\n"; + + close(IFILE); + + if ($opt_silent) { print "\r"; } + print " Indexfile small (A-Z jump) created: $indexfile\t"; + if (!$opt_silent) { print "\n"; } + } + + + # Build the frame layout file, this file includes the layout of the frames + # Build the frame layout file name (for small/compact A-Z index) + # handle the global index file case separately (no extra directory name in file) + if ($LocalGlobalLocal eq 'global') { $extradirfilename = ''; } + else { $extradirfilename = $dirnamesingle{$LocalActDir}; } + + if ($var{'frames'} eq 'yes') { + + $indexfile = $var{'dirhtml'}.$dirnamerelpath{$LocalActDir}.$indexfilename.$var{'filenameextensionframe'}.$extradirfilename.$var{'exthtml'}; + if ($debug > 2) { print " indexfilename (a-z frame): $indexfile\n"; } + + open(IFILE,">$indexfile") || die("Cannot open jump index frame file $indexfile: $!\n"); + + # Write the header of Frame file + print IFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n"; + + if ($var{'texttitleindex'} eq '') { + print IFILE "<title>Index of Matlab Files in Directory $dirToPrint</title>\n"; + } else { + if ($LocalGlobalLocal eq 'global') { print IFILE "<title>$var{'texttitleindex'}</title>\n"; } + else { print IFILE "<title>$var{'texttitleindex'} in Directory $dirToPrint</title>\n"; } + } + print IFILE "</head>\n"; + + # definition of 2 frames, top the A-Z index, below the jump letter line + print IFILE "<frameset rows=\"90%,10%\">\n"; + print IFILE " <frame src=\"$indexfilename$var{'filenameextensionindex'}$extradirfilename$var{'exthtml'}\" name=\"$GlobalNameFrameAZIndexsmall\" />\n"; + print IFILE " <frame src=\"$indexfilename$var{'filenameextensionjump'}$extradirfilename$var{'exthtml'}\" name=\"$GlobalNameFrameAZIndexjump\" />\n"; + print IFILE "</frameset>\n"; + + print IFILE "</html>\n"; + + close(IFILE); + + if ($opt_silent) { print "\r"; } + print " Frame layout file created: $indexfile\t"; + if (!$opt_silent) { print "\n"; } + } +} + + +#======================================================================== +# Construct the links to all indexes +#======================================================================== +sub ConstructLinks2Index +{ + local(*WRITEFILE, $LocalPath2Index, $PathContents, $LocalGlobalLocal) = @_; + + # include links to short/long - local/global index and C|contents.m + print WRITEFILE "\n<p>"; + print WRITEFILE "$var{'textjumpindexglobal'} "; + + if ($var{'frames'} eq 'yes') { + print WRITEFILE "<a href=\"$LocalPath2Index$var{'filenameindexshortglobal'}$var{'filenameextensionframe'}$var{'exthtml'}\">short</a> | "; + print WRITEFILE "<a href=\"$LocalPath2Index$var{'filenameindexlongglobal'}$var{'filenameextensionframe'}$var{'exthtml'}\">long</a>\n"; + } else { + print WRITEFILE "<a href=\"$LocalPath2Index$var{'filenametopframe'}$var{'exthtml'}\">short</a> | "; + print WRITEFILE "<a href=\"$LocalPath2Index$var{'filenameindexlongglobal'}$var{'filenameextensionindex'}$var{'exthtml'}\">long</a>\n"; + } + + if ($LocalGlobalLocal eq 'local') { + if ($var{'usecontentsm'} eq 'yes') { + print WRITEFILE " | <a href=\"$contentsname{$PathContents}$dirnamesingle{$PathContents}$var{'exthtml'}\">Local contents</a>\n"; + } + if ($var{'frames'} eq 'yes') { + print WRITEFILE " | $var{'textjumpindexlocal'} "; + print WRITEFILE "<a href=\"$var{'filenameindexshortlocal'}$var{'filenameextensionframe'}$dirnamesingle{$PathContents}$var{'exthtml'}\">short</a> | "; + print WRITEFILE "<a href=\"$var{'filenameindexlonglocal'}$var{'filenameextensionframe'}$dirnamesingle{$PathContents}$var{'exthtml'}\">long</a>\n"; + } else { + print WRITEFILE " | $var{'textjumpindexlocal'} "; + print WRITEFILE "<a href=\"$var{'filenameindexshortlocal'}$var{'filenameextensionindex'}$dirnamesingle{$PathContents}$var{'exthtml'}\">short</a> | "; + print WRITEFILE "<a href=\"$var{'filenameindexlonglocal'}$var{'filenameextensionindex'}$dirnamesingle{$PathContents}$var{'exthtml'}\">long</a>\n"; + } + } + print WRITEFILE "</p>\n\n"; + print WRITEFILE "$var{'codehr'}\n"; +} + + +#======================================================================== +# Construct the contents.m files or update +#======================================================================== +sub ConstructContentsmFile +{ + local($LocalActDir, @localnames) = @_; + local(*CFILE, $name,$newline); + local($contentsfile, $isincontentsonly); + local(@lines, @autoaddlines, @emptylines); + local($autoadd) = 'AutoAdd'; + local($autoaddsection) = 0; + local($emptylineflag) = 0; + local(%nameincontents); + + # Build the contents file name + $contentsfile = $LocalActDir.$contentsname{$LocalActDir}.$suffix; + + if (-e $contentsfile) { + open(CFILE,"<$contentsfile") || die("Cannot open contents file $contentsfile: $!\n"); + while (<CFILE>) { + # Search for the specified string pattern + @words = split; + if ((@words >= 3) && ($words[2] eq '-')) { + $isincontentsonly = 0; + foreach $name (@localnames) { + if ($name eq $words[1]) { # old + # if ($mfilename{$name} eq $words[1]) { + $isincontentsonly = 1; + $nameincontents{$name} = 1; + $newline = sprintf("%% %-13s - %s\n", $mfilename{$name}, $apropos{$name}); + push(@lines, $newline); + } + } + # issue a warning, if file is in contents, but not as file in the directory + if ($isincontentsonly == 0) { + print "\rConstructContents: Obsolete entry $words[1] in $contentsfile ! Entry not used.\n"; + } + } else { + # look for the AutoAdd section, should be the second word + if ((@words >= 2) && ($words[1] eq $autoadd)) { $autoaddsection = 1; } + # push the red line in an array + push(@lines, $_); + } + } + close(CFILE); + } else { + $newline = "% MATLAB Files in directory $LocalActDir\n%\n"; + push(@lines, $newline); + + } + + # collect the file names, that were not included in original C|contents.m + foreach $name (@localnames) { + if (! defined $nameincontents{$name}) { + if (! ($mfilename{$name} =~ /contents/i)) { + $newline = sprintf("%% %-13s - %s\n", $mfilename{$name}, $apropos{$name}); + push(@autoaddlines, $newline); + } + } + } + + # write/update C|contents.m only if variable is set + if ($var{'writecontentsm'} eq 'yes') { + unlink($contentsfile); + open(CFILE,">$contentsfile") || die("Cannot open contents file $contentsfile: $!\n"); + # write old C|contents.m or header of new file, as long as comment lines + foreach $line (@lines) { + if ($emptylineflag == 0) { + if ($line =~ /^\s*%/) { print CFILE $line; } + else { $emptylineflag = 1; push(@emptylines, $line); } + } else { push(@emptylines, $line); } + } + # add header of AutoAdd section + if (($autoaddsection == 0) && (@autoaddlines > 0)) { print CFILE "%\n% $autoadd\n"; } + # add autoadd section lines (previously undocumented files + foreach $line (@autoaddlines) { print CFILE $line; } + # add tail of original C|contents.m (everything behind first non-comment line) + foreach $line (@emptylines) { print CFILE $line; } + print CFILE "\n"; + close CFILE; + if ($opt_silent) { print "\r"; } + print " Contents file created/updated: $contentsfile\t"; + if (!$opt_silent) { print "\n"; } + } +} + + +#======================================================================== +# Replace found special characters with their HTMl Entities +#======================================================================== +sub SubstituteHTMLEntities { + local($_) = @_; + + # Replace & <-> & < <-> < > <-> > " <-> " + s/&/&/g; s/\</</g; s/\>/>/g; s/\"/"/g; + return $_; +} + +#======================================================================== +# Replace found m-filenamestring with full link. +#======================================================================== +sub SubstituteName2Link { + local($_, $funname) = @_; + local($refstr1, $refstr2, $reffound); + + # Look for something matching in the line + if ( /(\W+)($funname)(\W+)/i ) { + $reffound = $2; + $refstr1 = "<a class=\"mfun\" href=\"$hfileindexpath{$name}$hfilerelpath{$funname}$mfilename{$funname}$var{'exthtml'}\">"; + $refstr2 = "<\/a>"; + # Do links only for exact case match + if ( ($var{'links2filescase'} eq 'exact') || ($var{'links2filescase'} eq 'exactvery') ) { + if ( /(\W+)($funname)(\W+)/g ) { + s/(\W+)($funname)(\W+)/$1$refstr1$funname$refstr2$3/g; + } + else { + # Print info for not matching case in references, good for check up of files + if ( ($var{'links2filescase'} eq 'exactvery') ) { + print "Diff in case found: $funname (case of file name) <-> $reffound (case in source code)\n"; + print " (source line) $_ \n"; + } + } + } + # Do links for exact match and additionally for all upper case (often used in original matlab help text) + elsif ( ($var{'links2filescase'} eq 'exactupper') ) { + s/(\W+)($funname)(\W+)/$1$refstr1$2$refstr2$3/g; + $funname2 = "\U$funname\E"; + s/(\W+)($funname2)(\W+)/$1$refstr1$2$refstr2$3/g; + } + # Do links for all case mixes, this calls for trouble under LINUX/UNIX + else { #elsif ( ($var{'links2filescase'} eq 'all') ) + s/(\W+)($funname)(\W+)/$1$refstr1$2$refstr2$3/ig; + } + } + + return $_; +} + +#======================================================================== +# Construct the html files for each matlab file. +# Need to reread each matlab file to find the help text. +# Note that we can't do this in a single loop because sometimes +# the help text maybe before the function declaration. +#======================================================================== +sub ConstructHTMLFiles +{ + local(*MFILE); + local(*HFILE); + + local($filescreated) = 0; + local($functionline); + + foreach $name (@names) { + # Create cross reference information already here, used for keywords as well + # Construct list of referenced functions + @xref = @{'depcalls'.$name}; # the functions, that this m-file calls + @yref = @{'depcalled'.$name}; # the functions, that this m-file is called from + # print " depcalls: @{'depcalls'.$name}\n depcalled: @{'depcalled'.$name}\n"; + # foreach $cname (@names) { next if $cname eq $name; push(@yref,$cname) if grep(/$name/,@{'depcalls'.$cname}); } + + + # Open m-file and html-file + open(MFILE,"<$mfile{$name}"); + open(HFILE,">$hfile{$name}"); + + # Write the header of HTML file + print HFILE "$TextDocTypeHTML\n<html>\n<head>\n$var{'codeheadmeta'}\n$TextMetaCharset\n$var{'csslink'}\n"; + + # Write meta tags: use apropos (one line function description) for description + # and cross reference function names for keywords (any better ideas?) + print HFILE "<meta name=\"description\" content=\" $apropos{$name} \" />\n"; + print HFILE "<meta name=\"keywords\" content=\" @xref @yref \" />\n"; + + # Write Title and start body of html-file + print HFILE "<title>$var{'texttitlefiles'} $mfilename{$name}</title>\n</head>\n"; + print HFILE "<body $var{'codebodyfiles'}>\n"; + print HFILE "<div id=\"matlabdoc\">\n"; + print HFILE "<h1 $var{'codeheader'}>$var{'textheaderfiles'} $mfilename{$name}</h1>\n"; + +# http://test.soundsoftware.ac.uk/cannam/projects/smallbox/repository/annotate/DL/RLS-DLA/SolveFISTA.m +# print HFILE "<a href=\"" . $hfileindexpath{$name} . "../../projects/smallbox/repository/annotate/" . $mfiledir{$name} . $mfilename{$name} . ".m\">View in repository</a>\n"; + + print HFILE "$var{'codehr'}\n"; + + # include links to short/long - local/global index and C|contents.m + &ConstructLinks2Index(HFILE, $hfileindexpath{$name}, $mfiledir{$name}, 'local'); + + # If this is a function, then write out the first line as a synopsis + if ($mtype{$name} eq "function") { + print HFILE "<h2 $var{'codeheader'}>Function Synopsis</h2>\n"; + print HFILE "<pre>$synopsis{$name}</pre>\n$var{'codehr'}\n"; + } + + # Look for the matlab help text block + $functionline = "\n"; + do { + $_ = <MFILE>; + # remember functionline, if before help text block + if (/^\s*function/) { $functionline = $_; } + } until (/^\s*%/ || eof); + if (! (eof(MFILE))) { + print HFILE "<h2 $var{'codeheader'}>Help text</h2>\n"; + print HFILE "<pre>\n"; + while (/^\s*%/) { + # First remove leading % and white space, then Substitute special characlers + s/^\s*%//; + $_ = &SubstituteHTMLEntities($_); + + # check/create cross references + foreach $funname (@{'all'.$name}) { + if ($funname =~ /simulink/) { print "\n Simulink - Filename: $name; scanname: $funname\n"; } + next if $funname eq $name; + $_ = &SubstituteName2Link($_, $funname); + } + print HFILE $_; + if (! eof) { $_ = <MFILE>; } + } + print HFILE "</pre>\n$var{'codehr'}\n"; + } + + # Write the cross reference information + if (@xref || @yref) { + print HFILE "<h2 $var{'codeheader'}>Cross-Reference Information</H2>\n"; + print HFILE "<table border=\"0\" width=\"100%\">\n<tr align=\"left\">\n<th width=\"50%\">"; + if (@xref) { + print HFILE "This $mtype{$name} calls"; + } + print HFILE "</th>\n<th width=\"50%\">"; + if (@yref) { + print HFILE "This $mtype{$name} is called by"; + } + print HFILE "</th>\n</tr>\n<tr valign=\"top\"><td>"; + if (@xref) { + print HFILE "\n<ul>\n"; + foreach $cname (sort @xref) { + print HFILE "<li><a class=\"mfun\" href=\"$hfileindexpath{$name}$hfilerelpath{$cname}$mfilename{$cname}$var{'exthtml'}\">$mfilename{$cname}</a></li>\n"; + } + print HFILE "</ul>\n"; + } + print HFILE "</td><td>"; + if (@yref) { + print HFILE "\n<ul>\n"; + foreach $cname (sort @yref) { + print HFILE "<li><a class=\"mfun\" href=\"$hfileindexpath{$name}$hfilerelpath{$cname}$mfilename{$cname}$var{'exthtml'}\">$mfilename{$cname}</a></li>\n"; + } + print HFILE "</ul>\n"; + } + print HFILE "</td>\n</tr>\n</table>\n"; + print HFILE "$var{'codehr'}\n"; + } + + # Include source text if requested + if (($var{'includesource'} eq 'yes') && (! ($mfilename{$name} =~ /^contents$/i))) { + print HFILE "<h2 $var{'codeheader'}>Listing of $mtype{$name} $mfilename{$name}</h2>\n"; + seek(MFILE,0,0); + print HFILE "<pre>\n"; + $IsStillHelp = 2; + print HFILE $functionline; # functionline from scanning of help + while (<MFILE>) { + if ($IsStillHelp == 2) { + next if (/^\s*$/); + next if (/^\s*function/); + if (/^\s*%/) { $IsStillHelp = 1; next; } + } elsif ($IsStillHelp == 1) { + next if (/^\s*%/); + $IsStillHelp = 0; + } + + # Substritute special characters + $_ = &SubstituteHTMLEntities($_); + + # check for comment in line and format with css em + s/^(.*)%(.*?)([\s\r\n]+)$/$1<em class=\"mcom\">%$2<\/em>$3/; + + # check/create cross references + foreach $funname (@{'all'.$name}) { + next if $funname eq $name; + $_ = &SubstituteName2Link($_, $funname); + } + print HFILE $_; + } + print HFILE "</pre>\n$var{'codehr'}\n"; + } + + # Include info about author from authorfile + &WriteFile2Handle($var{'authorfile'}, HFILE) ; + + print HFILE "<!--navigate-->\n"; + print HFILE "<!--copyright-->\n"; + print HFILE "</div>\n</body>\n</html>\n"; + close(MFILE); + close(HFILE); + + # Print name of finished file + if ($opt_silent) { print "\r"; } + print " HTML-File created: $hfile{$name}\t"; + if (!$opt_silent) { print "\n"; } + $filescreated++; + } + + print "\n$PROGRAM: $indexcreated index and $filescreated files created.\n"; +} + +#======================================================================== +# Function: CheckFileName +# Purpose: . +#======================================================================== +sub CheckFileName { + local($filename, $description) = @_; + local(*CHECKFILE); + + open(CHECKFILE,"<$filename") || do { + if ($description eq '') {$description = 'file';} + # if (!$opt_silent) { print "Cannot open $description $filename: $!\n"; } + print "Cannot open $description $filename: $!\n"; + return 1; + }; + close(CHECKFILE); + return 0; + +} + +#======================================================================== +# Function: CheckDirName +# Purpose: . +#======================================================================== +sub CheckDirName { + local($dirname, $description) = @_; + local(*CHECKDIR); + + opendir(CHECKDIR,"$dirname") || die ("Cannot open $description directory $dirname: $!\n"); + closedir(CHECKDIR); +} + +#======================================================================== +# Function: WriteFile2Handle +# Purpose: . +#======================================================================== +sub WriteFile2Handle { + local($filename, *WRITEFILE) = @_; + local(*READFILE); + + if ($filename ne '') { + open(READFILE,"<$filename"); + @filecontents = <READFILE>; + close(READFILE); + print WRITEFILE "@filecontents\n"; + # if (!$opt_silent) {print " Contents of $filename added\n"}; + } +} + + +#======================================================================== +# Function: GetConfigFile +# Purpose: Read user's configuration file, if such exists. +#======================================================================== +sub GetConfigFile +{ + local($filename) = @_; + local(*CONFIG); + local($value); + + if (&CheckFileName($filename, 'configuration file')) { + # if (!$opt_silent) { print " Proceeding using built-in defaults for configuration.\n"; } + print " Proceeding using built-in defaults for configuration.\n"; + return 0; + }; + + open(CONFIG,"< $filename"); + while (<CONFIG>) { + s/#.*$//; + next if /^\s*$/o; + + # match keyword: process one or more arguments + # keyword set + if (/^\s*set\s+(\S+)\s*=\s*(.*)/) { + # setting a configuration variable + if (defined $var{$1}) { + $var{$1} = $2; + if ($debug > 3) { print "$1: $var{$1}\n"; } + } + else { + print "$PROGRAM: unknown variable `$1' in configuration file\n" + } + } else { + chop($_); + print "$PROGRAM: unknown keyword in configuration file in line: `$_'\n" + } + } + close CONFIG; + 1; +} + + +#------------------------------------------------------------------------ +# DisplayHelp - display help text using -h or -help command-line switch +#------------------------------------------------------------------------ +sub DisplayHelp +{ + $help=<<EofHelp; + $PROGRAM v$VERSION - generate html documentation from Matlab m-files + + Usage: $PROGRAM [-h] [-c config_file] [-m|dirmfiles matlab_dir] [-d|dirhtml html_dir] + [-i yes|no] [-r yes|no] [-p yes|no] [-quiet|q] [-a authorfile] + + $PROGRAM is a perl script that reads each matlab .m file in a directory + to produce a corresponding .html file of help documentation and cross + reference information. An index file is written with links to all of + the html files produced. The options are: + + -quiet or -q : be silent, no status information during generation + -help or -h : display this help message + -todo or -t : print the todo list for $PROGRAM + -version or -v : display version + + -configfile or -c : name of configuration file (default to $var{'configfile'}). + -dirmfiles or -m : top level directory containing matlab files to generate html for; + default to actual directory. + -dirhtml or -d : top level directory for generated html files; + default to actual directory. + + -includesource or -i : Include matlab source in the html documentation [yes|no] + default to yes. + -processtree or -r : create docu for m-file directory and all subdirectories [yes|no]; + default to yes. + -producetree or -p : create multi-level docu identical to directory structure + of m-files [yes|no]; default to yes. + -writecontentsm or -w: update or write contents.m files into the matlab source + directories [yes|no]; default to no. + + -authorfile or -a : name of file including author information, last element in html; + default to empty. + + The command line setting overwrite all other settings (built-in and configuration file). + The configuration file settings overwrite the built-in settings (and not the command + line settings). + + Typical usages are: + $PROGRAM + (use default parameters from perl script, if configuration + file is found -> generation of docu, else display of help) + + $PROGRAM -dirmfiles matlab -dirhtml html + (generate html documentation for all m-files in directory matlab, + place html files in directory html, use built-in defaults for + all other parameters, this way all m-files in the directory + matlab and below are converted and the generated html-files are + placed in the directory html and below producing the same + directory structure than below matlab) + + $PROGRAM -quiet + (use built-in parameters from perl script, if configuration + file is found use these settings as well, do generation, + no display except critical errors, status of conversion and result) + + $PROGRAM -m toolbox -dirhtml doc/html -r yes -p no + (convert all m-files in directory toolbox and below and place + the generated html files in directory doc/html, read all m-files + recursively, however, the generated html files are placed in one + directory) + + $PROGRAM -m toolbox -dirhtml doc/html -i no -r no + (convert all m-files in directory toolbox and place + the generated html files in directory doc/html, do not read m-files + recursively, do not include source code in documentation) + +EofHelp + + die "$help"; +} + +#------------------------------------------------------------------------ +# DisplayTodo - display ToDo list using -t or -todo command-line switch +#------------------------------------------------------------------------ +sub DisplayTodo +{ + $todo=<<EofToDo; + $PROGRAM v$VERSION - ToDo list + + o use more than one high level directory + + o what should/could be done here??? + +EofToDo + + die "$todo"; +} + + +#------------------------------------------------------------------------ +# ListVariables - list all defined variables and their values +#------------------------------------------------------------------------ +sub ListVariables +{ + local($value); + + if ($debug > 0) { + print "List of all variables and their values\n"; + foreach (sort keys %var) + { + if ($var{$_} eq '') { + $value = "empty"; + } else { + $value = $var{$_}; + } + print " $_\n $value\n"; + } + print "\n\n"; + } +} + + +__END__ +:endofperl
--- a/extra/soundsoftware/reposman-soundsoftware.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/extra/soundsoftware/reposman-soundsoftware.rb Mon Jun 06 14:55:38 2011 +0100 @@ -9,12 +9,12 @@ # reposman [OPTIONS...] -s [DIR] -r [HOST] # # Examples: -# reposman --svn-dir=/var/svn --redmine-host=redmine.example.net --scm subversion +# reposman --scm-dir=/var/svn --redmine-host=redmine.example.net --scm subversion # reposman -s /var/git -r redmine.example.net -u http://svn.example.net --scm git # # == Arguments (mandatory) # -# -s, --svn-dir=DIR use DIR as base directory for svn repositories +# -s, --scm-dir=DIR use DIR as base directory for repositories # -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples: # -r redmine.example.net # -r http://redmine.example.net @@ -70,7 +70,7 @@ SUPPORTED_SCM = %w( Subversion Darcs Mercurial Bazaar Git Filesystem ) opts = GetoptLong.new( - ['--svn-dir', '-s', GetoptLong::REQUIRED_ARGUMENT], + ['--scm-dir', '-s', GetoptLong::REQUIRED_ARGUMENT], ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT], ['--key', '-k', GetoptLong::REQUIRED_ARGUMENT], ['--owner', '-o', GetoptLong::REQUIRED_ARGUMENT], @@ -133,7 +133,7 @@ begin opts.each do |opt, arg| case opt - when '--svn-dir'; $repos_base = arg.dup + when '--scm-dir'; $repos_base = arg.dup when '--redmine-host'; $redmine_host = arg.dup when '--key'; $api_key = arg.dup when '--owner'; $svn_owner = arg.dup; $use_groupid = false; @@ -174,7 +174,7 @@ end unless File.directory?($repos_base) - log("directory '#{$repos_base}' doesn't exists", :exit => true) + log("directory '#{$repos_base}' doesn't exist", :exit => true) end begin @@ -184,7 +184,7 @@ end class Project < ActiveResource::Base - self.headers["User-agent"] = "Redmine repository manager/#{Version}" + self.headers["User-agent"] = "SoundSoftware repository manager/#{Version}" end log("querying Redmine for projects...", :level => 1);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extra/soundsoftware/update-external-repo.sh Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,132 @@ +#!/bin/sh + +mirrordir="/var/mirror" +hg="/usr/local/bin/hg" + +project="$1" +local_repo="$2" +remote_repo="$3" + +if [ -z "$project" ] || [ -z "$local_repo" ] || [ -z "$remote_repo" ]; then + echo "Usage: $0 <project> <local-repo-path> <remote-repo-url>" + exit 2 +fi + + # We need to handle different source repository types separately. + # + # The convert extension cannot convert directly from a remote git + # repo; we'd have to mirror to a local repo first. Incremental + # conversions do work though. The hg-git plugin will convert + # directly from remote repositories, but not via all schemes + # (e.g. https is not currently supported). It's probably easier to + # use git itself to clone locally and then convert or hg-git from + # there. + # + # We can of course convert directly from remote Subversion repos, + # but we need to keep track of that -- you can ask to convert into a + # repo that has already been used (for Mercurial) and it'll do so + # happily; we don't want that. + # + # Converting from a remote Hg repo should be fine! + # + # One other thing -- we can't actually tell the difference between + # the various SCM types based on URL alone. We have to try them + # (ideally in an order determined by a guess based on the URL) and + # see what happens. + +project_mirror="$mirrordir/$project" +mkdir -p "$project_mirror" +project_repo_mirror="$project_mirror/repo" + + # Some test URLs: + # + # http://aimc.googlecode.com/svn/trunk/ + # http://aimc.googlecode.com/svn/ + # http://vagar.org/git/flam + # https://github.com/wslihgt/IMMF0salience.git + # http://hg.breakfastquay.com/dssi-vst/ + # git://github.com/schacon/hg-git.git + # http://svn.drobilla.net/lad (externals!) + +# If we are importing from another distributed system, then our aim is +# to create either a Hg repo or a git repo at $project_mirror, which +# we can then pull from directly to the Hg repo at $local_repo (using +# hg-git, in the case of a git repo). + +# Importing from SVN, we should use hg convert directly to the target +# hg repo (or should we?) but keep a record of the last changeset ID +# we brought in, and test each time whether it matches the last +# changeset ID actually in the repo + +success="" + +# If we have a record of the last successfully updated remote repo +# URL, check it against our current remote URL: if it has changed, we +# will need to start again with a new clone rather than pulling +# updates into the existing local mirror + +successfile="$project_mirror/last_successful_url" +if [ -f "$successfile" ]; then + last=$(cat "$successfile") + if [ x"$last" = x"$remote_repo" ]; then + echo "$$: Remote URL is unchanged from last successful update" + else + echo "$$: Remote URL has changed since last successful update:" + echo "$$: Last URL was $last, current is $remote_repo" + suffix="$$.$(date +%s)" + echo "$$: Moving existing repos to $suffix suffix and starting afresh" + mv "$project_repo_mirror" "$project_repo_mirror"."$suffix" + mv "$local_repo" "$local_repo"."$suffix" + mv "$successfile" "$successfile"."$suffix" + touch "$project_mirror/url_changed" + fi +fi + +if [ -d "$project_repo_mirror" ]; then + + # Repo mirror exists: update it + echo "$$: Mirror for project $project exists at $project_repo_mirror, updating" 1>&2 + + if [ -d "$project_repo_mirror/.hg" ]; then + "$hg" --config extensions.convert= convert --datesort "$remote_repo" "$project_repo_mirror" && success=true + if [ -z "$success" ]; then + ( cd "$project_repo_mirror" && "$hg" pull "$remote_repo" ) && success=true + fi + elif [ -d "$project_repo_mirror/.git" ]; then + ( cd "$project_repo_mirror" && git pull "$remote_repo" master ) && success=true + else + echo "$$: ERROR: Repo mirror dir $project_repo_mirror exists but is not an Hg or git repo" 1>&2 + fi + +else + + # Repo mirror does not exist yet + echo "$$: Mirror for project $project does not yet exist at $project_repo_mirror, trying to convert or clone" 1>&2 + + case "$remote_repo" in + *git*) + git clone "$remote_repo" "$project_repo_mirror" || + "$hg" --config extensions.convert= convert --datesort "$remote_repo" "$project_repo_mirror" + ;; + *) + "$hg" --config extensions.convert= convert --datesort "$remote_repo" "$project_repo_mirror" || + git clone "$remote_repo" "$project_repo_mirror" || + "$hg" clone "$remote_repo" "$project_repo_mirror" + ;; + esac && success=true + +fi + +echo "Success=$success" + +if [ -n "$success" ]; then + echo "$$: Update successful, pulling into local repo at $local_repo" + if [ ! -d "$local_repo" ]; then + "$hg" init "$local_repo" + fi + if [ -d "$project_repo_mirror/.git" ]; then + ( cd "$local_repo" && "$hg" --config extensions.hggit= pull "$project_repo_mirror" ) && echo "$remote_repo" > "$successfile" + else + ( cd "$local_repo" && "$hg" pull "$project_repo_mirror" ) && echo "$remote_repo" > "$successfile" + fi +fi
--- a/lib/redmine.rb Mon Jun 06 14:41:04 2011 +0100 +++ b/lib/redmine.rb Mon Jun 06 14:55:38 2011 +0100 @@ -50,7 +50,7 @@ map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true map.permission :search_project, {:search => :index}, :public => true map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin - map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member + map.permission :edit_project, {:projects => [:settings, :edit, :update, :overview]}, :require => :member map.permission :select_project_modules, {:projects => :modules}, :require => :member map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy, :autocomplete_for_member]}, :require => :member map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member @@ -189,6 +189,7 @@ Redmine::MenuManager.map :project_menu do |menu| menu.push :overview, { :controller => 'projects', :action => 'show' } + menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural menu.push :activity, { :controller => 'activities', :action => 'index' } menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id, :if => Proc.new { |p| p.shared_versions.any? } @@ -197,7 +198,6 @@ :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) } menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar - menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id, :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/public/javascripts/repository.js Mon Jun 06 14:55:38 2011 +0100 @@ -0,0 +1,7 @@ +function toggle_ext_url(){ + if($('repository_is_external').checked) + $('repository_external_url').enable(); + else + $('repository_external_url').disable(); +} +
--- a/public/javascripts/ssamr_registration.js Mon Jun 06 14:41:04 2011 +0100 +++ b/public/javascripts/ssamr_registration.js Mon Jun 06 14:55:38 2011 +0100 @@ -4,13 +4,12 @@ /* institution related functions */ Event.observe(window, 'load', - function() { - + function() { if(!$('ssamr_user_details_institution_type_true').checked && $('ssamr_user_details_institution_type_true').checked){ - $('ssamr_user_details_other_institution').disable(); - $('ssamr_user_details_institution_id').enable(); - $('ssamr_user_details_institution_type_true').checked = true; - $('ssamr_user_details_institution_type_false').checked = false; + $('ssamr_user_details_other_institution').disable(); + $('ssamr_user_details_institution_id').enable(); + $('ssamr_user_details_institution_type_true').checked = true; + $('ssamr_user_details_institution_type_false').checked = false; } } );
--- a/public/stylesheets/application.css Mon Jun 06 14:41:04 2011 +0100 +++ b/public/stylesheets/application.css Mon Jun 06 14:55:38 2011 +0100 @@ -161,7 +161,7 @@ tr.changeset td.committed_on { text-align: center; width: 15%; } table.files tr.file td { text-align: center; } -table.files tr.file td.filename { text-align: left; padding-left: 24px; } +table.files tr.file td.filename { text-align: left; } table.files tr.file td.digest { font-size: 80%; } table.members td.roles, table.memberships td.roles { width: 45%; } @@ -225,6 +225,7 @@ h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; } div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; } +div.activity h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; } div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; } div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; } div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; } @@ -304,20 +305,24 @@ div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; } div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} -div#activity dl, #search-results { margin-left: 2em; } -div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } -div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } -div#activity dt.me .time { border-bottom: 1px solid #999; } -div#activity dt .time { color: #777; font-size: 80%; } +div#activity dl, div#news dl, #search-results { margin-left: 2em; } +div#activity .box dl { margin-left: 0; } +div#activity dd, div#news dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } +div#activity dt, div#news dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } +div#activity dt.me .time, div#news dt.me .time { border-bottom: 1px solid #999; } +div#activity dt .time, div#news dt .time, .projects .latest .time { color: #777; font-size: 80%; } div#activity dd .description, #search-results dd .description { font-style: italic; } -div#activity span.project:after, #search-results span.project:after { content: " -"; } +div#activity span.project:after, div#news span.project:after, #search-results span.project:after { content: " -"; } div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; } +.projects .latest .title { margin-right: 0.5em; } +.tipoftheday .tip { margin-left: 2em; margin-top: 0.5em; } + #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } -div#search-results-counts {float:right;} -div#search-results-counts ul { margin-top: 0.5em; } -div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; } +div#search-results-counts { display: block; padding-left: 0; margin-left: 0; } +div#search-results-counts ul { margin-top: 0.5em; padding-left: 0; margin-left: 0; } +div#search-results-counts li { display: inline; list-style-type: none; margin-right: 1em; } dt.issue { background-image: url(../images/ticket.png); } dt.issue-edit { background-image: url(../images/ticket_edit.png); } @@ -365,6 +370,8 @@ ul.projects li.child { list-style-type:none; margin-top: 1em;} ul.projects div.root a.project { font-weight: bold; font-size: 16px; margin: 0 0 10px 0; } +li.latest { margin-bottom: 0.5em; } + #tracker_project_ids ul { margin: 0; padding-left: 1em; } #tracker_project_ids li { list-style-type:none; }
--- a/public/themes/soundsoftware/stylesheets/application.css Mon Jun 06 14:41:04 2011 +0100 +++ b/public/themes/soundsoftware/stylesheets/application.css Mon Jun 06 14:55:38 2011 +0100 @@ -37,16 +37,41 @@ body,p,h2,h3,h4,li,table,.wiki h1 { font-family: DroidSans, 'Liberation Sans', tahoma, verdana, sans-serif; + line-height: 1.34; } h2,h3,h4,.wiki h1 { color: #3e442c; + font-weight: bold; +} + +.wiki h2,.wiki h3,.wiki h4 { + color: #000; } h2,.wiki h1 { - font-size: 1.8em; + font-size: 1.8em; } +.wiki h2 { + margin-top: 1em; +} + +.splitcontentleft p:first-child { + margin-top: 0; +} + +div.attachments { + margin-top: 2em; +} +#wiki_add_attachment { + margin-top: 1.5em; +} + +/* Hide these (the paragraph markers that show anchors) -- they confuse more than they help */ +a.wiki-anchor:hover { display: none; } +h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: none; } + .box { padding: 6px; margin-bottom: 10px; @@ -81,24 +106,29 @@ ul.projects .public, ul.projects .private { padding-left: 0.5em; color: #3e442c; font-size: 0.95em } +table.files tr.active td { padding-top: 0.5em; padding-bottom: 0.5em; } +table.files .file .active { font-weight: bold; } +table.files .file .description { font-weight: normal; color: #3e442c; } + #top-menu { position: absolute; top: 0; z-index: 1; left: 0px; width: 100%; font-size: 90%; /* height: 2em; */ margin: 0; padding: 0; padding-top: 0.5em; background-color: #3e442c; } #top-menu ul { margin-left: 10px; } #top-menu a { font-weight: bold; } #header { position: absolute; z-index: 0; top: 0; width: 100%; background: #fdfbf5; border-bottom: 2px solid #a9b680; /* height:80px; */ padding: 20px 0 0.5em 0; margin-bottom: 0; } #header a { color: #be5700; } #header h1 { color: #525a38; margin-top: 25px; font-size: 3em; font-weight: normal; margin-left: 10px; } +#header #project-title a, #header #project-title a:hover { color: #525a38; text-decoration: none; } .header-general h1 { background: url('soundsoftware-logo-title-only-transparent-beta.png') no-repeat 0 0; text-indent: -9999px; width: 500px; height: 34px; } - + #quick-search { margin-right: 6px; margin-top: 1em; color: #000; } #project-jump-box { float: right; margin-right: 6px; margin-top: 5px; color: #000; } #project-ancestors-title { margin-bottom: 0px; - margin-left: 10px; + margin-left: 12px; margin-top: 6px; font-family: GilliusADFNo2, 'Gill Sans', Tahoma, sans-serif; font-weight: normal; @@ -201,5 +231,9 @@ .embedded .contents .center { text-align: center; } /* undo javadoc hack above */ +/* For MATLAB documentation */ +.embedded #matlabdoc th { text-align: left; } + +
--- a/vendor/plugins/redmine_checkout/app/views/redmine_checkout_hooks/_view_repositories_show_contextual.rhtml Mon Jun 06 14:41:04 2011 +0100 +++ b/vendor/plugins/redmine_checkout/app/views/redmine_checkout_hooks/_view_repositories_show_contextual.rhtml Mon Jun 06 14:55:38 2011 +0100 @@ -17,8 +17,19 @@ <div id="clipboard_button"><%= image_tag 'paste.png', :plugin => 'redmine_checkout' %></div> </div> <% end -%> - <% if default_protocol %><p><%=l :label_access_type, :type => l(default_protocol.access_label(User.current)) %></p><% end %> - + + <p> + <% if User.current.logged? %> + <% if repository.is_external? %> + <%=l :label_access_type_all, :type => l(:label_access_read_only) %> + <% else %> + <% if default_protocol %><%=l :label_access_type, :type => l(default_protocol.access_label(User.current)) %><% end %> + <% end %> + <% else %> + + <% end %> + </p> + <% javascript_tag do %> var checkout_access = $H({<%= protocols.inject([]){|r,p| r << "'checkout_protocol_#{p.protocol.to_s.underscore}': '#{l(p.access_label(User.current))}'"}.join(', ') %>}); var checkout_commands = $H({<%= protocols.inject([]){|r,p| r << "'checkout_protocol_#{p.protocol.to_s.underscore}': '#{escape_javascript(p.full_command(checkout_path))}'"}.join(', ') %>}); @@ -26,6 +37,11 @@ <% end %> </div> <% end%> + <% if repository.is_external? %> + <div style="clear: left"> + </div> + <p class="topline" style="padding-top: 1em"><%= l(:text_repository_external, :location => repository.external_url) %></p> + <% end %> </div> <div style="clear: left"></div> @@ -33,4 +49,4 @@ <%= stylesheet_link_tag 'checkout', :plugin => 'redmine_checkout' %> <%= javascript_include_tag 'checkout', :plugin => 'redmine_checkout' %> <%= (javascript_include_tag 'ZeroClipboard', :plugin => 'redmine_checkout') if Setting.checkout_use_zero_clipboard? %> -<% end %> \ No newline at end of file +<% end %>
--- a/vendor/plugins/redmine_checkout/assets/stylesheets/checkout.css Mon Jun 06 14:41:04 2011 +0100 +++ b/vendor/plugins/redmine_checkout/assets/stylesheets/checkout.css Mon Jun 06 14:55:38 2011 +0100 @@ -20,6 +20,10 @@ border-bottom: 1px solid #ccc; } +.topline { + border-top: 1px solid #ccc; +} + #checkout_box { margin: 10px 0; } @@ -159,4 +163,4 @@ #clipboard_button img { padding-top: 2px; -} \ No newline at end of file +}
--- a/vendor/plugins/redmine_checkout/config/locales/en-GB.yml Mon Jun 06 14:41:04 2011 +0100 +++ b/vendor/plugins/redmine_checkout/config/locales/en-GB.yml Mon Jun 06 14:55:38 2011 +0100 @@ -16,9 +16,9 @@ label_protocol_plural: "Protocols" button_add_protocol: "Add Protocol" - label_access_type: 'This URL has <span id="checkout_access">{{type}}</span> access.' - label_access_read_only: 'Read-Only' - label_access_read_write: "Read+Write" + label_access_type: 'You have <span id="checkout_access">{{type}}</span> access to this URL.' + label_access_read_only: 'Read Only' + label_access_read_write: "Read and Write" label_access_permission: "Depending on user's permissions" label_append_path: "Append path" @@ -37,3 +37,5 @@ Leave the Checkout URL field empty to use the defined repository URL. help_moved_settings: "The settings page has been moved to {{link}}." label_settings_location: "Administration -> Settings -> Checkout" + + text_repository_external: "The primary repository for this project is hosted at <code>{{location}}</code> .<br>This repository is a read-only copy which is updated automatically."
--- a/vendor/plugins/redmine_checkout/config/locales/en.yml Mon Jun 06 14:41:04 2011 +0100 +++ b/vendor/plugins/redmine_checkout/config/locales/en.yml Mon Jun 06 14:55:38 2011 +0100 @@ -16,9 +16,10 @@ label_protocol_plural: "Protocols" button_add_protocol: "Add Protocol" - label_access_type: 'This URL has <span id="checkout_access">{{type}}</span> access.' - label_access_read_only: 'Read-Only' - label_access_read_write: "Read+Write" + label_access_type: 'You have <span id="checkout_access">{{type}}</span> access to this URL.' + label_access_type_all: 'All access to this URL is <span id="checkout_access">{{type}}</span>.' + label_access_read_only: 'Read Only' + label_access_read_write: "Read and Write" label_access_permission: "Depending on user's permissions" label_append_path: "Append path" @@ -37,3 +38,6 @@ Leave the Checkout URL field empty to use the defined repository URL. help_moved_settings: "The settings page has been moved to {{link}}." label_settings_location: "Administration -> Settings -> Checkout" + + text_repository_external: "The primary repository for this project is hosted at <code>{{location}}</code> .<br>This repository is a read-only copy which is updated automatically every hour." +