diff --git a/Gemfile b/Gemfile index a9f69acc..1815dfd5 100644 --- a/Gemfile +++ b/Gemfile @@ -99,6 +99,13 @@ gem "colored" # GitLab settings gem 'settingslogic' +# Wiki +# - Use latest master to resolve Gem dependency with Pygemnts +# github-linquist needs pygments 0.4.2 but Gollum 2.4.11 +# requires pygments 0.3.2. The latest master Gollum has been updated +# to use pygments 0.4.2. Change this after next Gollum release. +gem "gollum", "~> 2.4.0", git: "git://github.com/github/gollum.git" + # Misc gem "foreman" gem "git" diff --git a/Gemfile.lock b/Gemfile.lock index a19cbfdd..ebaab14f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,19 @@ +GIT + remote: git://github.com/github/gollum.git + revision: 544d499ab170c9d9b355b7a0160afc74139ee2a4 + specs: + gollum (2.4.11) + github-markdown (~> 0.5.3) + github-markup (>= 0.7.5, < 1.0.0) + grit (~> 2.5.0) + mustache (>= 0.99.4, < 1.0.0) + nokogiri (~> 1.5.6) + pygments.rb (~> 0.4.2) + sanitize (~> 2.0.3) + sinatra (~> 1.3.5) + stringex (~> 1.5.1) + useragent (~> 0.4.16) + GIT remote: https://github.com/ctran/annotate_models.git revision: be4e26825b521f0b2d86b181e2dff89901aa9b1e @@ -145,6 +161,7 @@ GEM escape_utils (~> 0.2.3) mime-types (~> 1.19) pygments.rb (>= 0.2.13) + github-markdown (0.5.3) github-markup (0.7.5) gitlab-grack (1.0.0) rack (~> 1.4.1) @@ -176,6 +193,10 @@ GEM grape-entity (0.2.0) activesupport multi_json (>= 1.3.2) + grit (2.5.0) + diff-lcs (~> 1.1) + mime-types (~> 1.15) + posix-spawn (~> 0.3.6) grit_ext (0.6.2) charlock_holmes (~> 0.6.9) growl (1.0.3) @@ -237,7 +258,8 @@ GEM sprockets (~> 2.0) multi_json (1.6.1) multi_xml (0.5.3) - multipart-post (1.2.0) + multipart-post (1.1.5) + mustache (0.99.4) mysql2 (0.3.11) net-ldap (0.2.2) nokogiri (1.5.6) @@ -373,6 +395,8 @@ GEM rspec-mocks (~> 2.12.0) rubyntlm (0.1.1) rubyzip (0.9.9) + sanitize (2.0.3) + nokogiri (>= 1.4.4, < 1.6) sass (3.2.5) sass-rails (3.2.5) railties (~> 3.2.0) @@ -429,6 +453,7 @@ GEM tilt (~> 1.1, != 1.3.0) stamp (0.5.0) state_machine (1.1.2) + stringex (1.5.1) temple (0.5.5) test_after_commit (0.0.1) therubyracer (0.10.2) @@ -451,6 +476,7 @@ GEM kgio (~> 2.6) rack raindrops (~> 0.7) + useragent (0.4.16) virtus (0.5.4) backports (~> 2.6.1) descendants_tracker (~> 0.0.1) @@ -499,6 +525,7 @@ DEPENDENCIES gitlab_meta (= 5.0) gitlab_omniauth-ldap (= 1.0.2) gitlab_yaml_db (= 1.0.0) + gollum (~> 2.4.0)! gon grape (~> 0.3.1) grape-entity (~> 0.2.0) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d1d51e15..f7cd2b55 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -34,6 +34,7 @@ @import "sections/login.scss"; @import "sections/editor.scss"; @import "sections/admin.scss"; +@import "sections/wiki.scss"; @import "highlight/white.scss"; @import "highlight/dark.scss"; diff --git a/app/assets/stylesheets/sections/wiki.scss b/app/assets/stylesheets/sections/wiki.scss new file mode 100644 index 00000000..175911d7 --- /dev/null +++ b/app/assets/stylesheets/sections/wiki.scss @@ -0,0 +1,6 @@ +h3.page_title .edit-wiki-header { + width: 780px; + margin-left: auto; + margin-right: auto; + padding-right: 7px; +} diff --git a/app/controllers/wikis_controller.rb b/app/controllers/wikis_controller.rb index 69280291..940b1e97 100644 --- a/app/controllers/wikis_controller.rb +++ b/app/controllers/wikis_controller.rb @@ -2,58 +2,94 @@ class WikisController < ProjectResourceController before_filter :authorize_read_wiki! before_filter :authorize_write_wiki!, only: [:edit, :create, :history] before_filter :authorize_admin_wiki!, only: :destroy + before_filter :load_gollum_wiki def pages - @wiki_pages = @project.wikis.group(:slug).ordered + @wiki_pages = @gollum_wiki.pages end def show - @most_recent_wiki = @project.wikis.where(slug: params[:id]).ordered.first - if params[:version_id] - @wiki = @project.wikis.find(params[:version_id]) - else - @wiki = @most_recent_wiki - end + @wiki = @gollum_wiki.find_page(params[:id], params[:version_id]) if @wiki render 'show' else - if can?(current_user, :write_wiki, @project) - @wiki = @project.wikis.new(slug: params[:id]) - render 'edit' - else - render 'empty' - end + return render('empty') unless can?(current_user, :write_wiki, @project) + @wiki = WikiPage.new(@gollum_wiki) + @wiki.title = params[:id] + + render 'edit' end end def edit - @wiki = @project.wikis.where(slug: params[:id]).ordered.first - @wiki = Wiki.regenerate_from @wiki + @wiki = @gollum_wiki.find_page(params[:id]) + end + + def update + @wiki = @gollum_wiki.find_page(params[:id]) + + return render('empty') unless can?(current_user, :write_wiki, @project) + + if @wiki.update(content, format, message) + redirect_to [@project, @wiki], notice: 'Wiki was successfully updated.' + else + render 'edit' + end end def create - @wiki = @project.wikis.new(params[:wiki]) - @wiki.user = current_user + @wiki = WikiPage.new(@gollum_wiki) - respond_to do |format| - if @wiki.save - format.html { redirect_to [@project, @wiki], notice: 'Wiki was successfully updated.' } - else - format.html { render action: "edit" } - end + if @wiki.create(wiki_params) + redirect_to project_wiki_path(@project, @wiki), notice: 'Wiki was successfully updated.' + else + render action: "edit" end end def history - @wiki_pages = @project.wikis.where(slug: params[:id]).ordered + unless @wiki = @gollum_wiki.find_page(params[:id]) + redirect_to project_wiki_path(@project, :home), notice: "Page not found" + end end def destroy - @wikis = @project.wikis.where(slug: params[:id]).delete_all - - respond_to do |format| - format.html { redirect_to project_wiki_path(@project, :index), notice: "Page was successfully deleted" } - end + @wiki = @gollum_wiki.find_page(params[:id]) + @wiki.delete if @wiki + redirect_to project_wiki_path(@project, :home), notice: "Page was successfully deleted" end + + def git_access + end + + private + + def load_gollum_wiki + @gollum_wiki = GollumWiki.new(@project, current_user) + + # Call #wiki to make sure the Wiki Repo is initialized + @gollum_wiki.wiki + rescue GollumWiki::CouldNotCreateWikiError => ex + flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." + redirect_to @project + return false + end + + def wiki_params + params[:wiki].slice(:title, :content, :format, :message) + end + + def content + params[:wiki][:content] + end + + def format + params[:wiki][:format] + end + + def message + params[:wiki][:message] + end + end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 1a3d34eb..375f8861 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -49,4 +49,12 @@ module GitlabMarkdownHelper @markdown.render(text).html_safe end + + def render_wiki_content(wiki_page) + if wiki_page.format == :markdown + markdown(wiki_page.content) + else + wiki_page.formatted_content.html_safe + end + end end diff --git a/app/models/gollum_wiki.rb b/app/models/gollum_wiki.rb new file mode 100644 index 00000000..95326505 --- /dev/null +++ b/app/models/gollum_wiki.rb @@ -0,0 +1,118 @@ +class GollumWiki + + MARKUPS = { + "Markdown" => :markdown, + "RDoc" => :rdoc + } + + class CouldNotCreateWikiError < StandardError; end + + # Returns a string describing what went wrong after + # an operation fails. + attr_reader :error_message + + def initialize(project, user = nil) + @project = project + @user = user + end + + def path_with_namespace + @project.path_with_namespace + ".wiki" + end + + def url_to_repo + gitlab_shell.url_to_repo(path_with_namespace) + end + + def ssh_url_to_repo + url_to_repo + end + + def http_url_to_repo + http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') + end + + # Returns the Gollum::Wiki object. + def wiki + @wiki ||= begin + Gollum::Wiki.new(path_to_repo) + rescue Grit::NoSuchPathError + create_repo! + end + end + + # Returns an Array of Gitlab WikiPage instances or an + # empty Array if this Wiki has no pages. + def pages + wiki.pages.map { |page| WikiPage.new(self, page, true) } + end + + # Returns the last 30 Commit objects accross the entire + # repository. + def recent_history + Commit.fresh_commits(wiki.repo, 30) + end + + # Finds a page within the repository based on a tile + # or slug. + # + # title - The human readable or parameterized title of + # the page. + # + # Returns an initialized WikiPage instance or nil + def find_page(title, version = nil) + if page = wiki.page(title, version) + WikiPage.new(self, page, true) + else + nil + end + end + + def create_page(title, content, format = :markdown, message = nil) + commit = commit_details(:created, message, title) + + wiki.write_page(title, format, content, commit) + rescue Gollum::DuplicatePageError => e + @error_message = "Duplicate page: #{e.message}" + return false + end + + def update_page(page, content, format = :markdown, message = nil) + commit = commit_details(:updated, message, page.title) + + wiki.update_page(page, page.name, format, content, commit) + end + + def delete_page(page, message = nil) + wiki.delete_page(page, commit_details(:deleted, message, page.title)) + end + + private + + def create_repo! + if gitlab_shell.add_repository(path_with_namespace) + Gollum::Wiki.new(path_to_repo) + else + raise CouldNotCreateWikiError + end + end + + def commit_details(action, message = nil, title = nil) + commit_message = message || default_message(action, title) + + {email: @user.email, name: @user.name, message: commit_message} + end + + def default_message(action, title) + "#{@user.username} #{action} page: #{title}" + end + + def gitlab_shell + @gitlab_shell ||= Gitlab::Shell.new + end + + def path_to_repo + @path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git") + end + +end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb new file mode 100644 index 00000000..adc77b22 --- /dev/null +++ b/app/models/wiki_page.rb @@ -0,0 +1,181 @@ +class WikiPage + include ActiveModel::Validations + include ActiveModel::Conversion + include StaticModel + extend ActiveModel::Naming + + def self.primary_key + 'slug' + end + + def self.model_name + ActiveModel::Name.new(self, nil, 'wiki') + end + + def to_key + [:slug] + end + + validates :title, presence: true + validates :content, presence: true + + # The Gitlab GollumWiki instance. + attr_reader :wiki + + # The raw Gollum::Page instance. + attr_reader :page + + # The attributes Hash used for storing and validating + # new Page values before writing to the Gollum repository. + attr_accessor :attributes + + def initialize(wiki, page = nil, persisted = false) + @wiki = wiki + @page = page + @persisted = persisted + @attributes = {}.with_indifferent_access + + set_attributes if persisted? + end + + # The escaped URL path of this page. + def slug + @attributes[:slug] + end + + alias :to_param :slug + + # The formatted title of this page. + def title + @attributes[:title] || "" + end + + # Sets the title of this page. + def title=(new_title) + @attributes[:title] = new_title + end + + # The raw content of this page. + def content + @attributes[:content] + end + + # The processed/formatted content of this page. + def formatted_content + @attributes[:formatted_content] + end + + # The markup format for the page. + def format + @attributes[:format] || :markdown + end + + # The commit message for this page version. + def message + version.try(:message) + end + + # The Gitlab Commit instance for this page. + def version + return nil unless persisted? + + @version ||= Commit.new(@page.version) + end + + # Returns an array of Gitlab Commit instances. + def versions + return [] unless persisted? + + @page.versions.map { |v| Commit.new(v) } + end + + # Returns the Date that this latest version was + # created on. + def created_at + @page.version.date + end + + # Returns boolean True or False if this instance + # is an old version of the page. + def historical? + @page.historical? + end + + # Returns boolean True or False if this instance + # has been fully saved to disk or not. + def persisted? + @persisted == true + end + + # Creates a new Wiki Page. + # + # attr - Hash of attributes to set on the new page. + # :title - The title for the new page. + # :content - The raw markup content. + # :format - Optional symbol representing the + # content format. Can be any type + # listed in the GollumWiki::MARKUPS + # Hash. + # :message - Optional commit message to set on + # the new page. + # + # Returns the String SHA1 of the newly created page + # or False if the save was unsuccessful. + def create(attr = {}) + @attributes.merge!(attr) + + save :create_page, title, content, format, message + end + + # Updates an existing Wiki Page, creating a new version. + # + # new_content - The raw markup content to replace the existing. + # format - Optional symbol representing the content format. + # See GollumWiki::MARKUPS Hash for available formats. + # message - Optional commit message to set on the new version. + # + # Returns the String SHA1 of the newly created page + # or False if the save was unsuccessful. + def update(new_content = "", format = :markdown, message = nil) + @attributes[:content] = new_content + @attributes[:format] = format + + save :update_page, @page, content, format, message + end + + # Destroys the WIki Page. + # + # Returns boolean True or False. + def delete + if wiki.delete_page(@page) + true + else + false + end + end + + private + + def set_attributes + attributes[:slug] = @page.escaped_url_path + attributes[:title] = @page.title + attributes[:content] = @page.raw_data + attributes[:formatted_content] = @page.formatted_data + attributes[:format] = @page.format + end + + def save(method, *args) + if valid? && wiki.send(method, *args) + @page = wiki.wiki.paged(title) + + set_attributes + + @persisted = true + else + errors.add(:base, wiki.error_message) if wiki.error_message + @persisted = false + end + @persisted + end + +end diff --git a/app/observers/project_observer.rb b/app/observers/project_observer.rb index 4b1f8295..89dc97ac 100644 --- a/app/observers/project_observer.rb +++ b/app/observers/project_observer.rb @@ -18,6 +18,11 @@ class ProjectObserver < ActiveRecord::Observer project.path_with_namespace ) + GitlabShellWorker.perform_async( + :remove_repository, + project.path_with_namespace + ".wiki" + ) + project.satellite.destroy log_info("Project \"#{project.name}\" was removed") diff --git a/app/views/layouts/project_resource.html.haml b/app/views/layouts/project_resource.html.haml index e4558b69..dfbb6a03 100644 --- a/app/views/layouts/project_resource.html.haml +++ b/app/views/layouts/project_resource.html.haml @@ -36,7 +36,7 @@ %span.count.merge_counter= @project.merge_requests.opened.count = nav_link(html_options: {class: "#{project_wiki_tab_class}"}) do - = link_to 'Wiki', project_wiki_path(@project, :index) + = link_to 'Wiki', project_wiki_path(@project, :home) - if can? current_user, :admin_project, @project = nav_link(html_options: {class: "#{project_tab_class}"}) do diff --git a/app/views/wikis/_form.html.haml b/app/views/wikis/_form.html.haml index 7758b129..6fa41db4 100644 --- a/app/views/wikis/_form.html.haml +++ b/app/views/wikis/_form.html.haml @@ -8,9 +8,12 @@ .ui-box.ui-box-show .ui-box-head - = f.label :title - .input= f.text_field :title, class: 'span8' - = f.hidden_field :slug + %h3.page_title + .edit-wiki-header + = @wiki.title.titleize + = f.hidden_field :title, value: @wiki.title + = f.select :format, options_for_select(GollumWiki::MARKUPS, {selected: @wiki.format}), {}, class: "pull-right input-medium" + = f.label :format, class: "pull-right", style: "padding-right: 20px;" .ui-box-body .input %span.cgray @@ -22,6 +25,9 @@ .ui-box-bottom = f.label :content .input= f.text_area :content, class: 'span8 js-gfm-input' + .ui-box-bottom + = f.label :commit_message + .input= f.text_field :message, class: 'span8' .actions = f.submit 'Save', class: "btn-save btn" = link_to "Cancel", project_wiki_path(@project, :index), class: "btn btn-cancel" diff --git a/app/views/wikis/_main_links.html.haml b/app/views/wikis/_main_links.html.haml new file mode 100644 index 00000000..262ed746 --- /dev/null +++ b/app/views/wikis/_main_links.html.haml @@ -0,0 +1,16 @@ +%span.pull-right + = link_to project_wiki_path(@project, :home), class: "btn btn-small grouped" do + Home + = link_to pages_project_wikis_path(@project), class: "btn btn-small grouped" do + Pages + - if (@wiki && @wiki.persisted?) + = link_to history_project_wiki_path(@project, @wiki), class: "btn btn-small grouped" do + History + - if can?(current_user, :write_wiki, @project) + - if @wiki && @wiki.persisted? + = link_to edit_project_wiki_path(@project, @wiki), class: "btn btn-small grouped" do + %i.icon-edit + Edit + = link_to git_access_project_wikis_path(@project), class: "btn btn-small grouped" do + %i.icon-download-alt + Git Access diff --git a/app/views/wikis/_nav.html.haml b/app/views/wikis/_nav.html.haml index 36ab1053..f4b0b190 100644 --- a/app/views/wikis/_nav.html.haml +++ b/app/views/wikis/_nav.html.haml @@ -1,7 +1,7 @@ %ul.nav.nav-tabs - if @project.wiki_enabled = nav_link(controller: 'wikis') do - = link_to 'Wiki', project_wiki_path(@project, :index) + = link_to 'Wiki', project_wiki_path(@project, :home) - if @project.wall_enabled = nav_link(path: 'projects#wall') do diff --git a/app/views/wikis/edit.html.haml b/app/views/wikis/edit.html.haml index 501ba092..dd200a35 100644 --- a/app/views/wikis/edit.html.haml +++ b/app/views/wikis/edit.html.haml @@ -1,7 +1,9 @@ -%h3.page_title Editing page +%h3.page_title + Editing page + = render partial: 'main_links' = render 'form' .pull-right - - if can? current_user, :admin_wiki, @project + - if @wiki.persisted? && can?(current_user, :admin_wiki, @project) = link_to project_wiki_path(@project, @wiki), confirm: "Are you sure you want to delete this page?", method: :delete, class: "btn btn-small btn-remove" do Delete this page diff --git a/app/views/wikis/git_access.html.haml b/app/views/wikis/git_access.html.haml new file mode 100644 index 00000000..353d86f2 --- /dev/null +++ b/app/views/wikis/git_access.html.haml @@ -0,0 +1,36 @@ +%h3.page_title + Git Access + %strong= @gollum_wiki.path_with_namespace + = render partial: 'main_links' + +%br +.content + .project_clone_panel + .row + .span7 + .form-horizontal + .input-prepend.project_clone_holder + %button{class: "btn active", :"data-clone" => @gollum_wiki.ssh_url_to_repo} SSH + %button{class: "btn", :"data-clone" => @gollum_wiki.http_url_to_repo}= Gitlab.config.gitlab.protocol.upcase + = text_field_tag :project_clone, @gollum_wiki.url_to_repo, class: "one_click_select input-xxlarge", readonly: true + .git-empty + %fieldset + %legend Install Gollum: + %pre.dark + :preserve + gem install gollum + + %legend Clone Your Wiki: + %pre.dark + :preserve + git clone #{@gollum_wiki.path_with_namespace}.git + cd #{@gollum_wiki.path_with_namespace} + + %legend Start Gollum And Edit Locally: + %pre.dark + :preserve + gollum + == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin + >> Thin web server (v1.5.0 codename Knife) + >> Maximum connections set to 1024 + >> Listening on 0.0.0.0:4567, CTRL+C to stop diff --git a/app/views/wikis/history.html.haml b/app/views/wikis/history.html.haml index 18df8e1d..60920710 100644 --- a/app/views/wikis/history.html.haml +++ b/app/views/wikis/history.html.haml @@ -1,23 +1,29 @@ %h3.page_title %span.cgray History for - = @wiki_pages.first.title + = @wiki.title.titleize + = render partial: 'main_links' %br %table %thead %tr %th Page version + %th Author + %th Commit Message %th Last updated - %th Updated by + %th Format %tbody - - @wiki_pages.each_with_index do |wiki_page, i| + - @wiki.versions.each do |version| + - commit = CommitDecorator.new(version) %tr %td - %strong - = link_to project_wiki_path(@project, wiki_page, version_id: wiki_page.id) do - Version - = @wiki_pages.count - i + = link_to project_wiki_path(@project, @wiki, version_id: commit.id) do + = commit.short_id + %td= commit.author_link avatar: true, size: 24 %td - = wiki_page.created_at.to_s(:short) - (#{time_ago_in_words(wiki_page.created_at)} - ago) - %td= link_to_member(@project, wiki_page.user) + = commit.title + %td + = time_ago_in_words(version.date) + ago + %td + %strong + = @wiki.page.wiki.page(@wiki.page.name, commit.id).try(:format) diff --git a/app/views/wikis/pages.html.haml b/app/views/wikis/pages.html.haml index 7bf57adc..eb65599d 100644 --- a/app/views/wikis/pages.html.haml +++ b/app/views/wikis/pages.html.haml @@ -1,21 +1,25 @@ = render 'wikis/nav' -%h3.page_title All Pages +%h3.page_title + All Pages + = render partial: 'main_links' %br %table %thead %tr %th Title - %th Slug + %th Format %th Last updated %th Updated by %tbody - @wiki_pages.each do |wiki_page| %tr %td - %strong= link_to wiki_page.title, project_wiki_path(@project, wiki_page) - %td= wiki_page.slug + %strong= link_to wiki_page.title.titleize, project_wiki_path(@project, wiki_page) + %td + %strong= wiki_page.format %td = wiki_page.created_at.to_s(:short) do (#{time_ago_in_words(wiki_page.created_at)} ago) - %td= link_to_member(@project, wiki_page.user) + - commit = CommitDecorator.decorate(wiki_page.version) + %td= commit.author_link avatar: true, size: 24 diff --git a/app/views/wikis/show.html.haml b/app/views/wikis/show.html.haml index ea8de155..b660a15e 100644 --- a/app/views/wikis/show.html.haml +++ b/app/views/wikis/show.html.haml @@ -1,24 +1,17 @@ = render 'wikis/nav' -- if @wiki != @most_recent_wiki - .alert +%h3.page_title + = @wiki.title.titleize + = render partial: 'main_links' +%br +- if @wiki.historical? + .warning_message This is an old version of this page. You can view the #{link_to "most recent version", project_wiki_path(@project, @wiki)} or browse the #{link_to "history", history_project_wiki_path(@project, @wiki)}. .file_holder - .file_title - = @wiki.title - %span.options - = link_to pages_project_wikis_path(@project), class: "btn btn-tiny grouped" do - Pages - - if can? current_user, :write_wiki, @project - = link_to history_project_wiki_path(@project, @wiki), class: "btn btn-tiny grouped" do - History - = link_to edit_project_wiki_path(@project, @wiki), class: "btn btn-tiny grouped" do - %i.icon-edit - Edit - .file_content.wiki = preserve do - = markdown @wiki.content + = render_wiki_content(@wiki) -%p.time Last edited by #{link_to_member @project, @wiki.user}, #{time_ago_in_words @wiki.created_at} ago +- commit = CommitDecorator.new(@wiki.version) +%p.time Last edited by #{commit.author_link(avatar: true, size: 16)} #{time_ago_in_words @wiki.created_at} ago diff --git a/config/routes.rb b/config/routes.rb index 27977ae3..2e6d31c2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -185,6 +185,8 @@ Gitlab::Application.routes.draw do resources :wikis, only: [:show, :edit, :destroy, :create] do collection do get :pages + put ':id' => 'wikis#update' + get :git_access end member do diff --git a/features/project/wiki.feature b/features/project/wiki.feature index f052e2f2..45761f09 100644 --- a/features/project/wiki.feature +++ b/features/project/wiki.feature @@ -5,5 +5,32 @@ Feature: Project Wiki Given I visit project wiki page Scenario: Add new page - Given I create Wiki page - Then I should see newly created wiki page + Given I create the Wiki Home page + Then I should see the newly created wiki page + + Scenario: Edit existing page + Given I have an existing Wiki page + And I browse to that Wiki page + And I click on the Edit button + And I change the content + Then I should see the updated content + + Scenario: View page history + Given I have an existing wiki page + And That page has two revisions + And I browse to that Wiki page + And I click the History button + Then I should see both revisions + + Scenario: Destroy Wiki page + Given I have an existing wiki page + And I browse to that Wiki page + And I click on the Edit button + And I click on the "Delete this page" button + Then The page should be deleted + + Scenario: View all pages + Given I have an existing wiki page + And I browse to that Wiki page + And I click on the "Pages" button + Then I should see the existing page in the pages list diff --git a/features/steps/project/project_wiki.rb b/features/steps/project/project_wiki.rb index 902e9ce1..1a811bad 100644 --- a/features/steps/project/project_wiki.rb +++ b/features/steps/project/project_wiki.rb @@ -4,17 +4,73 @@ class ProjectWiki < Spinach::FeatureSteps include SharedNote include SharedPaths - Given 'I create Wiki page' do - fill_in "Title", :with => 'Test title' + Given 'I create the Wiki Home page' do fill_in "Content", :with => '[link test](test)' click_on "Save" end - Then 'I should see newly created wiki page' do - page.should have_content "Test title" + Then 'I should see the newly created wiki page' do + page.should have_content "Home" page.should have_content "link test" click_link "link test" page.should have_content "Editing page" end + + Given 'I have an existing Wiki page' do + wiki.create_page("existing", "content", :markdown, "first commit") + @page = wiki.find_page("existing") + end + + And 'I browse to that Wiki page' do + visit project_wiki_path(project, @page) + end + + And 'I click on the Edit button' do + click_on "Edit" + end + + And 'I change the content' do + fill_in "Content", :with => 'Updated Wiki Content' + click_on "Save" + end + + Then 'I should see the updated content' do + page.should have_content "Updated Wiki Content" + end + + And 'That page has two revisions' do + @page.update("new content", :markdown, "second commit") + end + + And 'I click the History button' do + click_on "History" + end + + Then 'I should see both revisions' do + page.should have_content current_user.name + page.should have_content "first commit" + page.should have_content "second commit" + end + + And 'I click on the "Delete this page" button' do + click_on "Delete this page" + end + + Then 'The page should be deleted' do + page.should have_content "Page was successfully deleted" + end + + And 'I click on the "Pages" button' do + click_on "Pages" + end + + Then 'I should see the existing page in the pages list' do + page.should have_content current_user.name + page.should have_content @page.title.titleize + end + + def wiki + @gollum_wiki = GollumWiki.new(project, current_user) + end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 2713e20a..444e1d0c 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -165,7 +165,7 @@ module SharedPaths end Given "I visit my project's wiki page" do - visit project_wiki_path(@project, :index) + visit project_wiki_path(@project, :home) end When 'I visit project hooks page' do @@ -260,7 +260,7 @@ module SharedPaths end Given 'I visit project wiki page' do - visit project_wiki_path(@project, :index) + visit project_wiki_path(@project, :home) end def root_ref diff --git a/features/support/env.rb b/features/support/env.rb index 6f1e4df3..f6f88955 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -37,6 +37,9 @@ DatabaseCleaner.strategy = :truncation Spinach.hooks.before_scenario do # Use tmp dir for FS manipulations Gitlab.config.gitlab_shell.stub(repos_path: Rails.root.join('tmp', 'test-git-base-path')) + Gitlab::Shell.any_instance.stub(:add_repository) do |path| + create_temp_repo("#{Rails.root}/tmp/test-git-base-path/#{path}.git") + end FileUtils.rm_rf Gitlab.config.gitlab_shell.repos_path FileUtils.mkdir_p Gitlab.config.gitlab_shell.repos_path DatabaseCleaner.start @@ -51,3 +54,9 @@ Spinach.hooks.before_run do include FactoryGirl::Syntax::Methods end + +def create_temp_repo(path) + FileUtils.mkdir_p path + command = "git init --quiet --bare #{path};" + system(command) +end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 22ac49c6..81451638 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -12,10 +12,18 @@ module Gitlab # ref - branch name # get "/allowed" do + # Check for *.wiki repositories. + # Strip out the .wiki from the pathname before finding the + # project. This applies the correct project permissions to + # the wiki repository as well. + project_path = params[:project] + project_path.gsub!(/\.wiki/,'') if project_path =~ /\.wiki/ + key = Key.find(params[:key_id]) - project = Project.find_with_namespace(params[:project]) + project = Project.find_with_namespace(project_path) git_cmd = params[:action] + if key.is_deploy_key project == key.project && git_cmd == 'git-upload-pack' else diff --git a/lib/tasks/gitlab/migrate_wiki.rake b/lib/tasks/gitlab/migrate_wiki.rake new file mode 100644 index 00000000..9b2f34c6 --- /dev/null +++ b/lib/tasks/gitlab/migrate_wiki.rake @@ -0,0 +1,20 @@ +namespace :gitlab do + namespace :wiki do + + # This task will migrate all of the existing Wiki + # content stored in your database into the new + # Gollum Wiki system. A new repository named + # namespace/project.wiki.git will be created for + # each project that currently has Wiki pages in + # the database. + # + # Notes: + # * The existing Wiki content will remain in your + # database in-tact. + desc "GITLAB | Migrate Wiki content from database to Gollum repositories." + task :migrate => :environment do + wiki_migrator = WikiToGollumMigrator.new + wiki_migrator.migrate! + end + end +end diff --git a/lib/wiki_to_gollum_migrator.rb b/lib/wiki_to_gollum_migrator.rb new file mode 100644 index 00000000..6083533b --- /dev/null +++ b/lib/wiki_to_gollum_migrator.rb @@ -0,0 +1,103 @@ +class WikiToGollumMigrator + + attr_reader :projects + + def initialize + @projects = [] + + Project.find_in_batches(batch_size: 50) do |batch| + batch.each { |p| @projects << p if p.wikis.any? } + end + end + + def migrate! + projects.each do |project| + log "\nMigrating Wiki for '#{project.path_with_namespace}'" + wiki = create_gollum_repo(project) + create_pages project, wiki + log "Project '#{project.path_with_namespace}' migrated. " + "[OK]".green + end + end + + private + + def create_gollum_repo(project) + GollumWiki.new(project, nil).wiki + end + + def create_pages(project, wiki) + pages = project.wikis.group(:slug).all + + pages.each do |page| + create_page_and_revisions(project, page) + end + end + + def create_page_and_revisions(project, page) + # Grab all revisions of the page + revisions = project.wikis.where(slug: page.slug).ordered.all + + # Remove the first revision created from the array + # and use it to create the Gollum page. Each successive revision + # will then be applied to the new Gollum page as an update. + first_rev = revisions.pop + + wiki = GollumWiki.new(project, page.user) + wiki_page = WikiPage.new(wiki) + + attributes = extract_attributes_from_page(first_rev) + + if wiki_page.create(attributes) + log " Created page '#{wiki_page.title}' " + "[OK]".green + + # Reverse the revisions to create them in the correct + # chronological order. + create_revisions(project, wiki_page, revisions.reverse) + else + log " Failed to create page '#{wiki_page.title}' " + "[FAILED]".red + end + end + + def create_revisions(project, page, revisions) + revisions.each do |revision| + log " Creating revisions..." + # Reinitialize a new GollumWiki instance for each page + # and revision created so the correct User is shown in + # the commit message. + wiki = GollumWiki.new(project, revision.user) + wiki_page = wiki.find_page(page.slug) + + attributes = extract_attributes_from_page(revision) + + content = attributes[:content] + + if wiki_page.update(content) + log " Created revision " + "[OK]".green + else + log " Failed to create revision " + "[FAILED]".red + end + end + end + + def extract_attributes_from_page(page) + attributes = page.attributes + .with_indifferent_access + .slice(:title, :content) + + # Change 'index' pages to 'home' pages to match Gollum standards + if attributes[:title].downcase == "index" + attributes[:title] = "home" unless home_already_exists?(project) + end + + attributes + end + + def home_already_exists?(project) + project.wikis.where(title: 'home').any? || project.wikis.where(title: 'Home').any? + end + + def log(message) + puts message + end + +end diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb index 1da18275..a6485328 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -207,25 +207,4 @@ describe "Gitlab Flavored Markdown" do page.should have_link("##{issue.id}") end end - - - describe "for wikis" do - before do - visit project_wiki_path(project, :index) - fill_in "Title", with: "Circumvent ##{issue.id}" - fill_in "Content", with: "# Other pages\n\n* [Foo](foo)\n* [Bar](bar)\n\nAlso look at ##{issue.id} :-)" - click_on "Save" - end - - it "should NOT render title in wikis#show" do - within(".content .file_title") do # page title - page.should have_content("Circumvent ##{issue.id}") - page.should_not have_link("##{issue.id}") - end - end - - it "should render content in wikis#show" do - page.should have_link("##{issue.id}") - end - end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 1f5fabfb..ac49e4d6 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -363,4 +363,28 @@ describe GitlabMarkdownHelper do markdown(":smile:").should include("src=\"#{url_to_image("emoji/smile")}") end end + + describe "#render_wiki_content" do + before do + @wiki = stub('WikiPage') + @wiki.stub(:content).and_return('wiki content') + end + + it "should use Gitlab Flavored Markdown for markdown files" do + @wiki.stub(:format).and_return(:markdown) + + helper.should_receive(:markdown).with('wiki content') + + helper.render_wiki_content(@wiki) + end + + it "should use the Gollum renderer for all other file types" do + @wiki.stub(:format).and_return(:rdoc) + formatted_content_stub = stub('formatted_content') + formatted_content_stub.should_receive(:html_safe) + @wiki.stub(:formatted_content).and_return(formatted_content_stub) + + helper.render_wiki_content(@wiki) + end + end end diff --git a/spec/lib/wiki_to_gollum_migrator_spec.rb b/spec/lib/wiki_to_gollum_migrator_spec.rb new file mode 100644 index 00000000..a784d836 --- /dev/null +++ b/spec/lib/wiki_to_gollum_migrator_spec.rb @@ -0,0 +1,114 @@ +require "spec_helper" + +describe WikiToGollumMigrator do + + def create_wiki_for(project) + 3.times { @pages[project.id] << create_page(project) } + end + + def create_revisions_for(project) + @pages[project.id].each do |page| + create_revision(page) + end + end + + def create_page(project) + page = project.wikis.new(title: "Page #{rand(1000)}", content: "Content") + page.user = project.owner + page.slug = page.title.parameterize + page.save! + page + end + + def create_revision(page) + revision = page.dup + revision.content = "Updated Content" + revision.save! + end + + def create_temp_repo(path) + FileUtils.mkdir_p path + command = "git init --quiet --bare #{path};" + system(command) + end + + before do + @repo_path = "#{Rails.root}/tmp/test-git-base-path" + @projects = [] + @pages = Hash.new {|h,k| h[k] = Array.new } + + @projects << create(:project) + @projects << create(:project) + + @projects.each do |project| + create_wiki_for project + create_revisions_for project + end + + @project_without_wiki = create(:project) + end + + context "Before the migration" do + it "has two projects with valid wikis" do + @projects.each do |project| + pages = project.wikis.group(:slug).all + pages.count.should == 3 + end + end + + it "has two revision for each page" do + @projects.each do |project| + @pages[project.id].each do |page| + revisions = project.wikis.where(slug: page.slug) + revisions.count.should == 2 + end + end + end + end + + describe "#initialize" do + it "finds all projects that have existing wiki pages" do + Project.count.should == 3 + subject.projects.count.should == 2 + end + end + + context "#migrate!" do + before do + Gitlab::Shell.any_instance.stub(:add_repository) do |path| + create_temp_repo("#{@repo_path}/#{path}.git") + end + + subject.stub(:log).as_null_object + + subject.migrate! + end + + it "creates a new Gollum Wiki for each project" do + @projects.each do |project| + wiki_path = project.path_with_namespace + ".wiki.git" + full_path = @repo_path + "/" + wiki_path + File.exist?(full_path).should be_true + File.directory?(full_path).should be_true + end + end + + it "creates a gollum page for each unique Wiki page" do + @projects.each do |project| + wiki = GollumWiki.new(project, nil) + wiki.pages.count.should == 3 + end + end + + it "creates a new revision for each old revision of the page" do + @projects.each do |project| + wiki = GollumWiki.new(project, nil) + wiki.pages.each do |page| + page.versions.count.should == 2 + end + end + end + end + + +end diff --git a/spec/models/gollum_wiki_spec.rb b/spec/models/gollum_wiki_spec.rb new file mode 100644 index 00000000..87601683 --- /dev/null +++ b/spec/models/gollum_wiki_spec.rb @@ -0,0 +1,196 @@ +require "spec_helper" + +describe GollumWiki do + + def create_temp_repo(path) + FileUtils.mkdir_p path + command = "git init --quiet #{path};" + system(command) + end + + def remove_temp_repo(path) + FileUtils.rm_rf path + end + + def commit_details + commit = {name: user.name, email: user.email, message: "test commit"} + end + + def create_page(name, content) + subject.wiki.write_page(name, :markdown, content, commit_details) + end + + def destroy_page(page) + subject.wiki.delete_page(page, commit_details) + end + + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:user) { project.owner } + let(:gitlab_shell) { Gitlab::Shell.new } + + subject { GollumWiki.new(project, user) } + + before do + create_temp_repo(subject.send(:path_to_repo)) + end + + describe "#path_with_namespace" do + it "returns the project path with namespace with the .wiki extension" do + subject.path_with_namespace.should == project.path_with_namespace + ".wiki" + end + end + + describe "#url_to_repo" do + it "returns the correct ssh url to the repo" do + subject.url_to_repo.should == gitlab_shell.url_to_repo(subject.path_with_namespace) + end + end + + describe "#ssh_url_to_repo" do + it "equals #url_to_repo" do + subject.ssh_url_to_repo.should == subject.url_to_repo + end + end + + describe "#http_url_to_repo" do + it "provides the full http url to the repo" do + gitlab_url = Gitlab.config.gitlab.url + repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git" + subject.http_url_to_repo.should == repo_http_url + end + end + + describe "#wiki" do + it "contains a Gollum::Wiki instance" do + subject.wiki.should be_a Gollum::Wiki + end + + before do + Gitlab::Shell.any_instance.stub(:add_repository) do + create_temp_repo("#{Rails.root}/tmp/test-git-base-path/non-existant.wiki.git") + end + project.stub(:path_with_namespace).and_return("non-existant") + end + + it "creates a new wiki repo if one does not yet exist" do + wiki = GollumWiki.new(project, user) + wiki.create_page("index", "test content").should_not == false + + FileUtils.rm_rf wiki.send(:path_to_repo) + end + + it "raises CouldNotCreateWikiError if it can't create the wiki repository" do + Gitlab::Shell.any_instance.stub(:add_repository).and_return(false) + expect { GollumWiki.new(project, user).wiki }.to raise_exception(GollumWiki::CouldNotCreateWikiError) + end + end + + describe "#pages" do + before do + create_page("index", "This is an awesome new Gollum Wiki") + @pages = subject.pages + end + + after do + destroy_page(@pages.first.page) + end + + it "returns an array of WikiPage instances" do + @pages.first.should be_a WikiPage + end + + it "returns the correct number of pages" do + @pages.count.should == 1 + end + end + + describe "#find_page" do + before do + create_page("index page", "This is an awesome Gollum Wiki") + end + + after do + destroy_page(subject.pages.first.page) + end + + it "returns the latest version of the page if it exists" do + page = subject.find_page("index page") + page.title.should == "index page" + end + + it "returns nil if the page does not exist" do + subject.find_page("non-existant").should == nil + end + + it "can find a page by slug" do + page = subject.find_page("index-page") + page.title.should == "index page" + end + + it "returns a WikiPage instance" do + page = subject.find_page("index page") + page.should be_a WikiPage + end + end + + describe "#create_page" do + after do + destroy_page(subject.pages.first.page) + end + + it "creates a new wiki page" do + subject.create_page("test page", "this is content").should_not == false + subject.pages.count.should == 1 + end + + it "returns false when a duplicate page exists" do + subject.create_page("test page", "content") + subject.create_page("test page", "content").should == false + end + + it "stores an error message when a duplicate page exists" do + 2.times { subject.create_page("test page", "content") } + subject.error_message.should =~ /Duplicate page:/ + end + + it "sets the correct commit message" do + subject.create_page("test page", "some content", :markdown, "commit message") + subject.pages.first.page.version.message.should == "commit message" + end + end + + describe "#update_page" do + before do + create_page("update-page", "some content") + @gollum_page = subject.wiki.paged("update-page") + subject.update_page(@gollum_page, "some other content", :markdown, "updated page") + @page = subject.pages.first.page + end + + after do + destroy_page(@page) + end + + it "updates the content of the page" do + @page.raw_data.should == "some other content" + end + + it "sets the correct commit message" do + @page.version.message.should == "updated page" + end + end + + describe "#delete_page" do + before do + create_page("index", "some content") + @page = subject.wiki.paged("index") + end + + it "deletes the page" do + subject.delete_page(@page) + subject.pages.count.should == 0 + end + end + +end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb new file mode 100644 index 00000000..67f2a6da --- /dev/null +++ b/spec/models/wiki_page_spec.rb @@ -0,0 +1,164 @@ +require "spec_helper" + +describe WikiPage do + + def create_temp_repo(path) + FileUtils.mkdir_p path + command = "git init --quiet #{path};" + system(command) + end + + def remove_temp_repo(path) + FileUtils.rm_rf path + end + + def commit_details + commit = {name: user.name, email: user.email, message: "test commit"} + end + + def create_page(name, content) + wiki.wiki.write_page(name, :markdown, content, commit_details) + end + + def destroy_page(title) + page = wiki.wiki.paged(title) + wiki.wiki.delete_page(page, commit_details) + end + + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:user) { project.owner } + let(:wiki) { GollumWiki.new(project, user) } + + subject { WikiPage.new(wiki) } + + before do + create_temp_repo(wiki.send(:path_to_repo)) + end + + describe "#initialize" do + context "when initialized with an existing gollum page" do + before do + create_page("test page", "test content") + @page = wiki.wiki.paged("test page") + @wiki_page = WikiPage.new(wiki, @page, true) + end + + it "sets the slug attribute" do + @wiki_page.slug.should == "test-page" + end + + it "sets the title attribute" do + @wiki_page.title.should == "test page" + end + + it "sets the formatted content attribute" do + @wiki_page.content.should == "test content" + end + + it "sets the format attribute" do + @wiki_page.format.should == :markdown + end + + it "sets the message attribute" do + @wiki_page.message.should == "test commit" + end + + it "sets the version attribute" do + @wiki_page.version.should be_a Commit + end + end + end + + describe "validations" do + before do + subject.attributes = {title: 'title', content: 'content'} + end + + it "validates presence of title" do + subject.attributes.delete(:title) + subject.valid?.should be_false + end + + it "validates presence of content" do + subject.attributes.delete(:content) + subject.valid?.should be_false + end + end + + before do + @wiki_attr = {title: "Index", content: "Home Page", format: "markdown"} + end + + describe "#create" do + after do + destroy_page("Index") + end + + context "with valid attributes" do + it "saves the wiki page" do + subject.create(@wiki_attr) + wiki.find_page("Index").should_not be_nil + end + + it "returns true" do + subject.create(@wiki_attr).should == true + end + end + end + + describe "#update" do + before do + create_page("Update", "content") + @page = wiki.find_page("Update") + end + + after do + destroy_page("Update") + end + + context "with valid attributes" do + it "updates the content of the page" do + @page.update("new content") + @page = wiki.find_page("Update") + end + + it "returns true" do + @page.update("more content").should be_true + end + end + end + + describe "#destroy" do + before do + create_page("Delete Page", "content") + @page = wiki.find_page("Delete Page") + end + + it "should delete the page" do + @page.delete + wiki.pages.should be_empty + end + + it "should return true" do + @page.delete.should == true + end + end + + describe "#versions" do + before do + create_page("Update", "content") + @page = wiki.find_page("Update") + end + + after do + destroy_page("Update") + end + + it "returns an array of all commits for the page" do + 3.times { |i| @page.update("content #{i}") } + @page.versions.count.should == 4 + end + end + +end