From 239d942606bd35d90c4546eb7b0fd530baf00a05 Mon Sep 17 00:00:00 2001 From: Bastian Krol Date: Wed, 9 Jul 2014 14:49:53 +0200 Subject: [PATCH 0001/1609] print validation errors when import fails --- lib/tasks/gitlab/import.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index cbfa736c84c..d38c7a59431 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -72,6 +72,7 @@ namespace :gitlab do puts " * Created #{project.name} (#{repo_path})".green else puts " * Failed trying to create #{project.name} (#{repo_path})".red + puts " Validation Errors: #{project.errors.messages}".red end end end -- GitLab From fec3a34bed67da87c37898e6ce5f71d55db2811d Mon Sep 17 00:00:00 2001 From: Dmitri Goosens Date: Wed, 17 Sep 2014 22:17:41 +0000 Subject: [PATCH 0002/1609] how to render line-breaks --- doc/markdown/markdown.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 5627fd0659f..1290fa934b2 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -45,14 +45,15 @@ You can also use other rich text files in GitLab. You might have to install a de GFM honors the markdown specification in how [paragraphs and line breaks are handled](http://daringfireball.net/projects/markdown/syntax#p). -A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.: +A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. +Line-breaks, or softreturns, are rendered if you end a line with two or more spaces - Roses are red + Roses are red [followed by two or more spaces] Violets are blue Sugar is sweet -Roses are red +Roses are red Violets are blue Sugar is sweet -- GitLab From 083f1e2f13e546e973cf7304adb00c81b1423ba6 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Mon, 29 Sep 2014 13:26:09 +0200 Subject: [PATCH 0003/1609] Fix dev user seed: multiple ID was used twice. --- db/fixtures/development/05_users.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb index c263dd232af..b697f58d4ef 100644 --- a/db/fixtures/development/05_users.rb +++ b/db/fixtures/development/05_users.rb @@ -16,14 +16,13 @@ Gitlab::Seeder.quiet do (1..5).each do |i| begin - User.seed(:id, [ - id: i + 10, - username: "user#{i}", - name: "User #{i}", - email: "user#{i}@example.com", - confirmed_at: DateTime.now, - password: '12345678' - ]) + User.seed do |s| + s.username = "user#{i}" + s.name = "User #{i}" + s.email = "user#{i}@example.com" + s.confirmed_at = DateTime.now + s.password = '12345678' + end print '.' rescue ActiveRecord::RecordNotSaved print 'F' -- GitLab From e2e4dc5942221f37713c3414e1811cc85dfaeab9 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sat, 27 Sep 2014 22:05:02 +0200 Subject: [PATCH 0004/1609] Use blob local instead of instance. --- app/views/projects/blob/_blob.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index be785dacedb..6ad40e8fa1f 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -16,7 +16,7 @@ = link_to title, '#' %ul.blob-commit-info.bs-callout.bs-callout-info.hidden-xs - - blob_commit = @repository.last_commit_for_path(@commit.id, @blob.path) + - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project %div#tree-content-holder.tree-content-holder -- GitLab From 81eacd1b2a591d3ce1f14d4119527ea9b290ba8f Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sun, 28 Sep 2014 11:02:29 +0200 Subject: [PATCH 0005/1609] Disable / hide MR edit blob button if cannot edit. --- app/helpers/tree_helper.rb | 31 ++++++++++++++++++---- app/views/projects/blob/_actions.html.haml | 8 +----- app/views/projects/diffs/_file.html.haml | 6 ++--- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index d815257a4e3..7d616589519 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -53,13 +53,34 @@ module TreeHelper File.join(*args) end - def allowed_tree_edit? - return false unless @repository.branch_names.include?(@ref) + def allowed_tree_edit?(project = nil, ref = nil) + project ||= @project + ref ||= @ref + return false unless project.repository.branch_names.include?(ref) - if @project.protected_branch? @ref - can?(current_user, :push_code_to_protected_branches, @project) + if project.protected_branch? ref + can?(current_user, :push_code_to_protected_branches, project) else - can?(current_user, :push_code, @project) + can?(current_user, :push_code, project) + end + end + + def edit_blob_link(project, ref, path, options = {}) + if project.repository.blob_at(ref, path).text? + text = 'Edit' + after = options[:after] || '' + from_mr = options[:from_merge_request_id] + link_opts = {} + link_opts[:from_merge_request_id] = from_mr if from_mr + cls = 'btn btn-small' + if allowed_tree_edit?(project, ref) + link_to text, project_edit_tree_path(project, tree_join(ref, path), + link_opts), class: cls + else + content_tag :span, text, class: cls + ' disabled' + end + after.html_safe + else + '' end end diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 64c19a57803..d8e190417af 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,11 +1,5 @@ .btn-group.tree-btn-group - -# only show edit link for text files - - if @blob.text? - - if allowed_tree_edit? - = link_to 'Edit', project_edit_tree_path(@project, @id), - class: 'btn btn-small' - - else - %span.btn.btn-small.disabled Edit + = edit_blob_link(@project, @ref, @path) = link_to 'Raw', project_raw_path(@project, @id), class: 'btn btn-small', target: '_blank' -# only show normal/blame view links for text files diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 751df6a02e9..fc1faf73854 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -27,9 +27,9 @@   - if @merge_request && @merge_request.source_project - = link_to project_edit_tree_path(@merge_request.source_project, tree_join(@merge_request.source_branch, diff_file.new_path), from_merge_request_id: @merge_request.id), { class: 'btn btn-small' } do - Edit -   + = edit_blob_link(@merge_request.source_project, + @merge_request.source_branch, diff_file.new_path, + after: ' ', from_merge_request_id: @merge_request.id) = view_file_btn(@commit.id, diff_file, project) -- GitLab From c44764f523cea756f1f2efdc4db954f4f19df440 Mon Sep 17 00:00:00 2001 From: Bernhard Kaindl Date: Fri, 3 Oct 2014 10:12:44 +0200 Subject: [PATCH 0006/1609] Prepare ForkService to support forking projects to given namespaces Remove overload of BaseService.initialize, so initialize gains params, which is used to pass the namespace (like e.g. in TransferService). The namespace is checked for permission to create projects in it. --- CHANGELOG | 1 + app/services/projects/fork_service.rb | 19 +++++--- spec/services/projects/fork_service_spec.rb | 52 +++++++++++++++++++-- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0250b4a23c0..410863d3f99 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 7.4.0 - Do not delete tmp/repositories itself during clean-up, only its contents - Support for backup uploads to remote storage - Prevent notes polling when there are not notes + - Internal ForkService: Prepare support for fork to a given namespace - API: Add support for forking a project via the API (Bernhard Kaindl) - API: filter project issues by milestone (Julien Bianchi) - Fail harder in the backup script diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index a59311bf942..c4f2d08efe9 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -2,11 +2,9 @@ module Projects class ForkService < BaseService include Gitlab::ShellAdapter - def initialize(project, user) - @from_project, @current_user = project, user - end - def execute + @from_project = @project + project_params = { visibility_level: @from_project.visibility_level, description: @from_project.description, @@ -15,8 +13,15 @@ module Projects project = Project.new(project_params) project.name = @from_project.name project.path = @from_project.path - project.namespace = current_user.namespace - project.creator = current_user + project.namespace = @current_user.namespace + if namespace = @params[:namespace] + project.namespace = namespace + end + project.creator = @current_user + unless @current_user.can?(:create_projects, project.namespace) + project.errors.add(:namespace, 'insufficient access rights') + return project + end # If the project cannot save, we do not want to trigger the project destroy # as this can have the side effect of deleting a repo attached to an existing @@ -27,7 +32,7 @@ module Projects #First save the DB entries as they can be rolled back if the repo fork fails project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id) if project.save - project.team << [current_user, :master] + project.team << [@current_user, :master] end #Now fork the repo unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path) diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 0edc3a8e807..5c80345c2b3 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -42,10 +42,54 @@ describe Projects::ForkService do end end - def fork_project(from_project, user, fork_success = true) - context = Projects::ForkService.new(from_project, user) - shell = double("gitlab_shell") - shell.stub(fork_repository: fork_success) + describe :fork_to_namespace do + before do + @group_owner = create(:user) + @developer = create(:user) + @project = create(:project, creator_id: @group_owner.id, + star_count: 777, + description: 'Wow, such a cool project!') + @group = create(:group) + @group.add_user(@group_owner, GroupMember::OWNER) + @group.add_user(@developer, GroupMember::DEVELOPER) + @opts = { namespace: @group } + end + + context 'fork project for group' do + it 'group owner successfully forks project into the group' do + to_project = fork_project(@project, @group_owner, true, @opts) + to_project.owner.should == @group + to_project.namespace.should == @group + to_project.name.should == @project.name + to_project.path.should == @project.path + to_project.description.should == @project.description + to_project.star_count.should be_zero + end + end + + context 'fork project for group when user not owner' do + it 'group developer should fail to fork project into the group' do + to_project = fork_project(@project, @developer, true, @opts) + to_project.errors[:namespace].should == ['insufficient access rights'] + end + end + + context 'project already exists in group' do + it 'should fail due to validation, not transaction failure' do + existing_project = create(:project, name: @project.name, + namespace: @group) + to_project = fork_project(@project, @group_owner, true, @opts) + existing_project.persisted?.should be_true + to_project.errors[:base].should == ['Invalid fork destination'] + to_project.errors[:name].should == ['has already been taken'] + to_project.errors[:path].should == ['has already been taken'] + end + end + end + + def fork_project(from_project, user, fork_success = true, params = {}) + context = Projects::ForkService.new(from_project, user, params) + shell = double('gitlab_shell').stub(fork_repository: fork_success) context.stub(gitlab_shell: shell) context.execute end -- GitLab From d92749989490289793e1e64fc6fff5673c41c75a Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Sat, 4 Oct 2014 10:54:00 +0200 Subject: [PATCH 0007/1609] Remove unused Project#code function. --- app/models/project.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index d228da192e4..1c7fd27a38b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -326,11 +326,6 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.select(&:activated?).first end - # For compatibility with old code - def code - path - end - def items_for(entity) case entity when 'issue' then -- GitLab From f4efb19038d01225374c91cca9274cce3d728b3d Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Mon, 6 Oct 2014 23:37:27 +0200 Subject: [PATCH 0008/1609] Add tests for tree edit routes Critical because of possible confusion between /:id/preview and /:id for a path that ends in preview. --- config/routes.rb | 1 + spec/routing/project_routing_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index 2534153758b..c0a970517b6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -198,6 +198,7 @@ Gitlab::Application.routes.draw do resources :raw, only: [:show], constraints: {id: /.+/} resources :tree, only: [:show], constraints: {id: /.+/, format: /(html|js)/ } resources :edit_tree, only: [:show, :update], constraints: { id: /.+/ }, path: 'edit' do + # Cannot be GET to differentiate from GET paths that end in preview. post :preview, on: :member end resources :new_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'new' diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 4b2eb42c709..8a7d76cc970 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -432,6 +432,26 @@ describe Projects::TreeController, "routing" do end end +describe Projects::EditTreeController, 'routing' do + it 'to #show' do + get('/gitlab/gitlabhq/edit/master/app/models/project.rb').should( + route_to('projects/edit_tree#show', + project_id: 'gitlab/gitlabhq', + id: 'master/app/models/project.rb')) + get('/gitlab/gitlabhq/edit/master/app/models/project.rb/preview').should( + route_to('projects/edit_tree#show', + project_id: 'gitlab/gitlabhq', + id: 'master/app/models/project.rb/preview')) + end + + it 'to #preview' do + post('/gitlab/gitlabhq/edit/master/app/models/project.rb/preview').should( + route_to('projects/edit_tree#preview', + project_id: 'gitlab/gitlabhq', + id: 'master/app/models/project.rb')) + end +end + # project_compare_index GET /:project_id/compare(.:format) compare#index {id: /[^\/]+/, project_id: /[^\/]+/} # POST /:project_id/compare(.:format) compare#create {id: /[^\/]+/, project_id: /[^\/]+/} # project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/} -- GitLab From 6d4076fdc674cd5f9002e908e082d6c1c9dd1b90 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Tue, 7 Oct 2014 20:48:26 +0200 Subject: [PATCH 0009/1609] Disallow POST to compare: does not create objects --- config/routes.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 2534153758b..9273499f196 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -212,7 +212,8 @@ Gitlab::Application.routes.draw do end end - match "/compare/:from...:to" => "compare#show", as: "compare", via: [:get, :post], constraints: {from: /.+/, to: /.+/} + get '/compare/:from...:to' => 'compare#show', :as => 'compare', + :constraints => {from: /.+/, to: /.+/} resources :snippets, constraints: {id: /\d+/} do member do -- GitLab From 6798a6a8e224471b69018ad5cc4a526654ea5772 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Fri, 10 Oct 2014 14:34:43 -0500 Subject: [PATCH 0010/1609] Allow HTML tags in user Markdown input Allow whitelisted tags to appear in rendered HTML output by disabling Redcarpet's `:filter_html` option. --- app/helpers/gitlab_markdown_helper.rb | 3 +-- lib/gitlab/markdown.rb | 5 ++++ spec/helpers/gitlab_markdown_helper_spec.rb | 30 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 0365681a128..3a0e8bcda4e 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -33,7 +33,6 @@ module GitlabMarkdownHelper @options = options gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self, { # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch- - filter_html: true, with_toc_data: true, safe_links_only: true }.merge(options)) @@ -48,7 +47,7 @@ module GitlabMarkdownHelper space_after_headers: true, superscript: true) end - @markdown.render(text).html_safe + @markdown.render(sanitize_html(text)).html_safe end def first_line_in_markdown(text) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 17512a51658..464b88d07ea 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -80,6 +80,11 @@ module Gitlab markdown_context) text = result[:output].to_html(save_with: 0) + sanitize_html(text) + end + + # Remove HTML tags and attributes that are not whitelisted + def sanitize_html(text) allowed_attributes = ActionView::Base.sanitized_allowed_attributes allowed_tags = ActionView::Base.sanitized_allowed_tags diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 15033f07432..c75773ad321 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -614,6 +614,36 @@ describe GitlabMarkdownHelper do expected = "" markdown(actual).should match(expected) end + + it 'should allow whitelisted HTML tags from the user' do + actual = '
Term
Definition
' + expect(markdown(actual)).to match(actual) + end + + it 'should sanitize tags that are not whitelisted' do + actual = ' no blinks' + expected = 'no inputs allowed no blinks' + expect(markdown(actual)).to match(expected) + expect(markdown(actual)).not_to match('<.textarea>') + expect(markdown(actual)).not_to match('<.blink>') + end + + it 'should allow whitelisted tag attributes from the user' do + actual = 'link text' + expect(markdown(actual)).to match(actual) + end + + it 'should sanitize tag attributes that are not whitelisted' do + actual = 'link text' + expected = 'link text' + expect(markdown(actual)).to match(expected) + end + + it 'should sanitize javascript in attributes' do + actual = %q(link text) + expected = 'link text' + expect(markdown(actual)).to match(expected) + end end describe 'markdown for empty repository' do -- GitLab From 1a9c2ddc55cf563ea42d67811a19b2693d7a44e9 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Fri, 10 Oct 2014 18:12:50 -0500 Subject: [PATCH 0011/1609] Document whitelisted HTML tags and attributes --- doc/markdown/markdown.md | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 6d96da76ad7..0f63eca1f61 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -438,6 +438,65 @@ You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
Does *not* work **very** well. Use HTML tags.
+The following tags can be used: + +* `` +* `` +* `` +* `
` +* `` +* `` +* `
` +* `
` +* `` +* `` +* `
` +* `` +* `` +* `
` +* `
` +* `
` +* `` +* `

` +* `

` +* `

` +* `

` +* `

` +* `
` +* `
` +* `` +* `` +* `` +* `` +* `
  • ` +* `
      ` +* `

      ` +* `

      `
      +* ``
      +* ``
      +* ``
      +* ``
      +* ``
      +* ``
      +* ``
      +* `
  • }) + expect(markdown(actual, {no_header_anchors:true})).to match(%r{Apply !#{merge_request.iid}}) end it "should add ids and links to headers" do # Test every rule except nested tags. text = '..Ab_c-d. e..' id = 'ab_c-d-e' - markdown("# #{text}").should match(%r{

    #{text}

    }) - markdown("# #{text}", {no_header_anchors:true}).should == "

    #{text}

    " + expect(markdown("# #{text}")).to match(%r{

    #{text}

    }) + expect(markdown("# #{text}", {no_header_anchors:true})).to eq("

    #{text}

    ") id = 'link-text' - markdown("# [link text](url) ![img alt](url)").should match( + expect(markdown("# [link text](url) ![img alt](url)")).to match( %r{

    link text ]*>

    } ) end @@ -530,32 +530,32 @@ describe GitlabMarkdownHelper do actual = "\n* dark: ##{issue.iid}\n* light by @#{member.user.username}" - markdown(actual).should match(%r{
  • dark: ##{issue.iid}
  • }) - markdown(actual).should match(%r{
  • light by @#{member.user.username}
  • }) + expect(markdown(actual)).to match(%r{
  • dark: ##{issue.iid}
  • }) + expect(markdown(actual)).to match(%r{
  • light by @#{member.user.username}
  • }) end it "should not link the apostrophe to issue 39" do project.team << [user, :master] - project.issues.stub(:where).with(iid: '39').and_return([issue]) + allow(project.issues).to receive(:where).with(iid: '39').and_return([issue]) actual = "Yes, it is @#{member.user.username}'s task." expected = /Yes, it is @#{member.user.username}<\/a>'s task/ - markdown(actual).should match(expected) + expect(markdown(actual)).to match(expected) end it "should not link the apostrophe to issue 39 in code blocks" do project.team << [user, :master] - project.issues.stub(:where).with(iid: '39').and_return([issue]) + allow(project.issues).to receive(:where).with(iid: '39').and_return([issue]) actual = "Yes, `it is @#{member.user.username}'s task.`" expected = /Yes, it is @gfm\'s task.<\/code>/ - markdown(actual).should match(expected) + expect(markdown(actual)).to match(expected) end it "should handle references in " do actual = "Apply _!#{merge_request.iid}_ ASAP" - markdown(actual).should match(%r{Apply !#{merge_request.iid}}) + expect(markdown(actual)).to match(%r{Apply !#{merge_request.iid}}) end it "should handle tables" do @@ -564,91 +564,92 @@ describe GitlabMarkdownHelper do | cell 1 | cell 2 | | cell 3 | cell 4 |} - markdown(actual).should match(/\Asome code from $40\nhere too\n\n" - helper.markdown("\n some code from $#{snippet.id}\n here too\n").should == target_html - helper.markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n").should == target_html + expect(helper.markdown("\n some code from $#{snippet.id}\n here too\n")).to eq(target_html) + expect(helper.markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n")).to eq(target_html) end it "should leave inline code untouched" do - markdown("\nDon't use `$#{snippet.id}` here.\n").should == + expect(markdown("\nDon't use `$#{snippet.id}` here.\n")).to eq( "

    Don't use $#{snippet.id} here.

    \n" + ) end it "should leave ref-like autolinks untouched" do - markdown("look at http://example.tld/#!#{merge_request.iid}").should == "

    look at http://example.tld/#!#{merge_request.iid}

    \n" + expect(markdown("look at http://example.tld/#!#{merge_request.iid}")).to eq("

    look at http://example.tld/#!#{merge_request.iid}

    \n") end it "should leave ref-like href of 'manual' links untouched" do - markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})").should == "

    why not inspect !#{merge_request.iid}

    \n" + expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("

    why not inspect !#{merge_request.iid}

    \n") end it "should leave ref-like src of images untouched" do - markdown("screen shot: ![some image](http://example.tld/#!#{merge_request.iid})").should == "

    screen shot: \"some

    \n" + expect(markdown("screen shot: ![some image](http://example.tld/#!#{merge_request.iid})")).to eq("

    screen shot: \"some

    \n") end it "should generate absolute urls for refs" do - markdown("##{issue.iid}").should include(project_issue_url(project, issue)) + expect(markdown("##{issue.iid}")).to include(project_issue_url(project, issue)) end it "should generate absolute urls for emoji" do - markdown(':smile:').should( + expect(markdown(':smile:')).to( include(%(src="#{Gitlab.config.gitlab.url}/assets/emoji/smile.png)) ) end it "should generate absolute urls for emoji if relative url is present" do - Gitlab.config.gitlab.stub(:url).and_return('http://localhost/gitlab/root') - markdown(":smile:").should include("src=\"http://localhost/gitlab/root/assets/emoji/smile.png") + allow(Gitlab.config.gitlab).to receive(:url).and_return('http://localhost/gitlab/root') + expect(markdown(":smile:")).to include("src=\"http://localhost/gitlab/root/assets/emoji/smile.png") end it "should generate absolute urls for emoji if asset_host is present" do - Gitlab::Application.config.stub(:asset_host).and_return("https://cdn.example.com") + allow(Gitlab::Application.config).to receive(:asset_host).and_return("https://cdn.example.com") ActionView::Base.any_instance.stub_chain(:config, :asset_host).and_return("https://cdn.example.com") - markdown(":smile:").should include("src=\"https://cdn.example.com/assets/emoji/smile.png") + expect(markdown(":smile:")).to include("src=\"https://cdn.example.com/assets/emoji/smile.png") end it "should handle relative urls for a file in master" do actual = "[GitLab API doc](doc/api/README.md)\n" expected = "

    GitLab API doc

    \n" - markdown(actual).should match(expected) + expect(markdown(actual)).to match(expected) end it "should handle relative urls for a directory in master" do actual = "[GitLab API doc](doc/api)\n" expected = "

    GitLab API doc

    \n" - markdown(actual).should match(expected) + expect(markdown(actual)).to match(expected) end it "should handle absolute urls" do actual = "[GitLab](https://www.gitlab.com)\n" expected = "

    GitLab

    \n" - markdown(actual).should match(expected) + expect(markdown(actual)).to match(expected) end it "should handle relative urls in reference links for a file in master" do actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" expected = "

    GitLab API doc

    \n" - markdown(actual).should match(expected) + expect(markdown(actual)).to match(expected) end it "should handle relative urls in reference links for a directory in master" do actual = "[GitLab API doc directory][GitLab readmes]\n [GitLab readmes]: doc/api/\n" expected = "

    GitLab API doc directory

    \n" - markdown(actual).should match(expected) + expect(markdown(actual)).to match(expected) end it "should not handle malformed relative urls in reference links for a file in master" do actual = "[GitLab readme]: doc/api/README.md\n" expected = "" - markdown(actual).should match(expected) + expect(markdown(actual)).to match(expected) end end @@ -661,29 +662,29 @@ describe GitlabMarkdownHelper do it "should not touch relative urls" do actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n" expected = "

    GitLab API doc

    \n" - markdown(actual).should match(expected) + expect(markdown(actual)).to match(expected) end end describe "#render_wiki_content" do before do @wiki = double('WikiPage') - @wiki.stub(:content).and_return('wiki content') + allow(@wiki).to receive(:content).and_return('wiki content') end it "should use GitLab Flavored Markdown for markdown files" do - @wiki.stub(:format).and_return(:markdown) + allow(@wiki).to receive(:format).and_return(:markdown) - helper.should_receive(:markdown).with('wiki content') + expect(helper).to 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) + allow(@wiki).to receive(:format).and_return(:rdoc) formatted_content_stub = double('formatted_content') - formatted_content_stub.should_receive(:html_safe) - @wiki.stub(:formatted_content).and_return(formatted_content_stub) + expect(formatted_content_stub).to receive(:html_safe) + allow(@wiki).to receive(:formatted_content).and_return(formatted_content_stub) helper.render_wiki_content(@wiki) end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index ebcc26852cc..7a8fd25e02d 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -8,18 +8,18 @@ describe IssuesHelper do describe "title_for_issue" do it "should return issue title if used internal tracker" do @project = project - title_for_issue(issue.iid).should eq issue.title + expect(title_for_issue(issue.iid)).to eq issue.title end it "should always return empty string if used external tracker" do @project = ext_project - title_for_issue(rand(100)).should eq "" + expect(title_for_issue(rand(100))).to eq "" end it "should always return empty string if project nil" do @project = nil - title_for_issue(rand(100)).should eq "" + expect(title_for_issue(rand(100))).to eq "" end end @@ -33,29 +33,29 @@ describe IssuesHelper do it "should return internal path if used internal tracker" do @project = project - url_for_project_issues.should match(int_expected) + expect(url_for_project_issues).to match(int_expected) end it "should return path to external tracker" do @project = ext_project - url_for_project_issues.should match(ext_expected) + expect(url_for_project_issues).to match(ext_expected) end it "should return empty string if project nil" do @project = nil - url_for_project_issues.should eq "" + expect(url_for_project_issues).to eq "" end describe "when external tracker was enabled and then config removed" do before do @project = ext_project - Gitlab.config.stub(:issues_tracker).and_return(nil) + allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) end it "should return path to external tracker" do - url_for_project_issues.should match(ext_expected) + expect(url_for_project_issues).to match(ext_expected) end end end @@ -71,34 +71,34 @@ describe IssuesHelper do it "should return internal path if used internal tracker" do @project = project - url_for_issue(issue.iid).should match(int_expected) + expect(url_for_issue(issue.iid)).to match(int_expected) end it "should return path to external tracker" do @project = ext_project - url_for_issue(issue.iid).should match(ext_expected) + expect(url_for_issue(issue.iid)).to match(ext_expected) end it "should return empty string if project nil" do @project = nil - url_for_issue(issue.iid).should eq "" + expect(url_for_issue(issue.iid)).to eq "" end describe "when external tracker was enabled and then config removed" do before do @project = ext_project - Gitlab.config.stub(:issues_tracker).and_return(nil) + allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) end it "should return external path" do - url_for_issue(issue.iid).should match(ext_expected) + expect(url_for_issue(issue.iid)).to match(ext_expected) end end end - describe :url_for_new_issue do + describe '#url_for_new_issue' do let(:issues_url) { ext_project.external_issue_tracker.new_issue_url } let(:ext_expected) do issues_url.gsub(':project_id', ext_project.id.to_s) @@ -108,29 +108,29 @@ describe IssuesHelper do it "should return internal path if used internal tracker" do @project = project - url_for_new_issue.should match(int_expected) + expect(url_for_new_issue).to match(int_expected) end it "should return path to external tracker" do @project = ext_project - url_for_new_issue.should match(ext_expected) + expect(url_for_new_issue).to match(ext_expected) end it "should return empty string if project nil" do @project = nil - url_for_new_issue.should eq "" + expect(url_for_new_issue).to eq "" end describe "when external tracker was enabled and then config removed" do before do @project = ext_project - Gitlab.config.stub(:issues_tracker).and_return(nil) + allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) end it "should return internal path" do - url_for_new_issue.should match(ext_expected) + expect(url_for_new_issue).to match(ext_expected) end end end diff --git a/spec/helpers/merge_requests_helper.rb b/spec/helpers/merge_requests_helper.rb index 5a317c4886b..5262d644048 100644 --- a/spec/helpers/merge_requests_helper.rb +++ b/spec/helpers/merge_requests_helper.rb @@ -7,6 +7,6 @@ describe MergeRequestsHelper do [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)] end - it { should eq('#1, #2, and #3') } + it { is_expected.to eq('#1, #2, and #3') } end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index dcc3318e4f9..482cb33e94f 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -11,7 +11,7 @@ describe NotificationsHelper do before { notification.stub(disabled?: true) } it "has a red icon" do - notification_icon(notification).should match('class="fa fa-volume-off ns-mute"') + expect(notification_icon(notification)).to match('class="fa fa-volume-off ns-mute"') end end @@ -19,7 +19,7 @@ describe NotificationsHelper do before { notification.stub(participating?: true) } it "has a blue icon" do - notification_icon(notification).should match('class="fa fa-volume-down ns-part"') + expect(notification_icon(notification)).to match('class="fa fa-volume-down ns-part"') end end @@ -27,12 +27,12 @@ describe NotificationsHelper do before { notification.stub(watch?: true) } it "has a green icon" do - notification_icon(notification).should match('class="fa fa-volume-up ns-watch"') + expect(notification_icon(notification)).to match('class="fa fa-volume-up ns-watch"') end end it "has a blue icon" do - notification_icon(notification).should match('class="fa fa-circle-o ns-default"') + expect(notification_icon(notification)).to match('class="fa fa-circle-o ns-default"') end end end diff --git a/spec/helpers/oauth_helper_spec.rb b/spec/helpers/oauth_helper_spec.rb index 453699136e9..088c342fa13 100644 --- a/spec/helpers/oauth_helper_spec.rb +++ b/spec/helpers/oauth_helper_spec.rb @@ -4,17 +4,17 @@ describe OauthHelper do describe "additional_providers" do it 'returns all enabled providers' do allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :github] } - helper.additional_providers.should include(*[:twitter, :github]) + expect(helper.additional_providers).to include(*[:twitter, :github]) end it 'does not return ldap provider' do allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :ldapmain] } - helper.additional_providers.should include(:twitter) + expect(helper.additional_providers).to include(:twitter) end it 'returns empty array' do allow(helper).to receive(:enabled_oauth_providers) { [] } - helper.additional_providers.should == [] + expect(helper.additional_providers).to eq([]) end end end \ No newline at end of file diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 281d4862199..0f78725e3d9 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe ProjectsHelper do describe "#project_status_css_class" do it "returns appropriate class" do - project_status_css_class("started").should == "active" - project_status_css_class("failed").should == "danger" - project_status_css_class("finished").should == "success" + expect(project_status_css_class("started")).to eq("active") + expect(project_status_css_class("failed")).to eq("danger") + expect(project_status_css_class("finished")).to eq("success") end end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 733f2754727..b327f4f911a 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -13,7 +13,7 @@ describe SearchHelper do end it "it returns nil" do - search_autocomplete_opts("q").should be_nil + expect(search_autocomplete_opts("q")).to be_nil end end @@ -25,29 +25,29 @@ describe SearchHelper do end it "includes Help sections" do - search_autocomplete_opts("hel").size.should == 9 + expect(search_autocomplete_opts("hel").size).to eq(9) end it "includes default sections" do - search_autocomplete_opts("adm").size.should == 1 + expect(search_autocomplete_opts("adm").size).to eq(1) end it "includes the user's groups" do create(:group).add_owner(user) - search_autocomplete_opts("gro").size.should == 1 + expect(search_autocomplete_opts("gro").size).to eq(1) end it "includes the user's projects" do project = create(:project, namespace: create(:namespace, owner: user)) - search_autocomplete_opts(project.name).size.should == 1 + expect(search_autocomplete_opts(project.name).size).to eq(1) end context "with a current project" do before { @project = create(:project) } it "includes project-specific sections" do - search_autocomplete_opts("Files").size.should == 1 - search_autocomplete_opts("Commits").size.should == 1 + expect(search_autocomplete_opts("Files").size).to eq(1) + expect(search_autocomplete_opts("Commits").size).to eq(1) end end end diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index 41c9f038c26..3d80dc9d0a4 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -19,28 +19,28 @@ describe SubmoduleHelper do Gitlab.config.gitlab_shell.stub(ssh_port: 22) # set this just to be sure Gitlab.config.gitlab_shell.stub(ssh_path_prefix: Settings.send(:build_gitlab_shell_ssh_path_prefix)) stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) end it 'should detect ssh on non-standard port' do Gitlab.config.gitlab_shell.stub(ssh_port: 2222) Gitlab.config.gitlab_shell.stub(ssh_path_prefix: Settings.send(:build_gitlab_shell_ssh_path_prefix)) stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) end it 'should detect http on standard port' do Gitlab.config.gitlab.stub(port: 80) Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) end it 'should detect http on non-standard port' do Gitlab.config.gitlab.stub(port: 3000) Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) end it 'should work with relative_url_root' do @@ -48,67 +48,67 @@ describe SubmoduleHelper do Gitlab.config.gitlab.stub(relative_url_root: '/gitlab/root') Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) stub_url([ 'http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git' ].join('')) - submodule_links(submodule_item).should == [ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ] + expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) end end context 'submodule on github.com' do it 'should detect ssh' do stub_url('git@github.com:gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should detect http' do stub_url('http://github.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should detect https' do stub_url('https://github.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should return original with non-standard url' do stub_url('http://github.com/gitlab-org/gitlab-ce') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) stub_url('http://github.com/another/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) end end context 'submodule on gitlab.com' do it 'should detect ssh' do stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should detect http' do stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should detect https' do stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ] + expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ]) end it 'should return original with non-standard url' do stub_url('http://gitlab.com/gitlab-org/gitlab-ce') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) end end context 'submodule on unsupported' do it 'should return original' do stub_url('http://mygitserver.com/gitlab-org/gitlab-ce') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) stub_url('http://mygitserver.com/gitlab-org/gitlab-ce.git') - submodule_links(submodule_item).should == [ repo.submodule_url_for, nil ] + expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ]) end end end diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb index fa8a3f554f7..fc0ceecfbe7 100644 --- a/spec/helpers/tab_helper_spec.rb +++ b/spec/helpers/tab_helper_spec.rb @@ -5,40 +5,40 @@ describe TabHelper do describe 'nav_link' do before do - controller.stub(:controller_name).and_return('foo') + allow(controller).to receive(:controller_name).and_return('foo') allow(self).to receive(:action_name).and_return('foo') end it "captures block output" do - nav_link { "Testing Blocks" }.should match(/Testing Blocks/) + expect(nav_link { "Testing Blocks" }).to match(/Testing Blocks/) end it "performs checks on the current controller" do - nav_link(controller: :foo).should match(/
  • /) - nav_link(controller: :bar).should_not match(/active/) - nav_link(controller: [:foo, :bar]).should match(/active/) + expect(nav_link(controller: :foo)).to match(/
  • /) + expect(nav_link(controller: :bar)).not_to match(/active/) + expect(nav_link(controller: [:foo, :bar])).to match(/active/) end it "performs checks on the current action" do - nav_link(action: :foo).should match(/
  • /) - nav_link(action: :bar).should_not match(/active/) - nav_link(action: [:foo, :bar]).should match(/active/) + expect(nav_link(action: :foo)).to match(/
  • /) + expect(nav_link(action: :bar)).not_to match(/active/) + expect(nav_link(action: [:foo, :bar])).to match(/active/) end it "performs checks on both controller and action when both are present" do - nav_link(controller: :bar, action: :foo).should_not match(/active/) - nav_link(controller: :foo, action: :bar).should_not match(/active/) - nav_link(controller: :foo, action: :foo).should match(/active/) + expect(nav_link(controller: :bar, action: :foo)).not_to match(/active/) + expect(nav_link(controller: :foo, action: :bar)).not_to match(/active/) + expect(nav_link(controller: :foo, action: :foo)).to match(/active/) end it "accepts a path shorthand" do - nav_link(path: 'foo#bar').should_not match(/active/) - nav_link(path: 'foo#foo').should match(/active/) + expect(nav_link(path: 'foo#bar')).not_to match(/active/) + expect(nav_link(path: 'foo#foo')).to match(/active/) end it "passes extra html options to the list element" do - nav_link(action: :foo, html_options: {class: 'home'}).should match(/
  • /) - nav_link(html_options: {class: 'active'}).should match(/
  • /) + expect(nav_link(action: :foo, html_options: {class: 'home'})).to match(/
  • /) + expect(nav_link(html_options: {class: 'active'})).to match(/
  • /) end end end diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index 8aa50c4c778..8271e00f41b 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -13,7 +13,7 @@ describe TreeHelper do let(:tree_item) { double(name: "files", path: "files") } it "should return the directory name" do - flatten_tree(tree_item).should match('files') + expect(flatten_tree(tree_item)).to match('files') end end @@ -21,7 +21,7 @@ describe TreeHelper do let(:tree_item) { double(name: "foo", path: "foo") } it "should return the flattened path" do - flatten_tree(tree_item).should match('foo/bar') + expect(flatten_tree(tree_item)).to match('foo/bar') end end end diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb index 8bf6ee2ed50..06d5450688b 100644 --- a/spec/lib/disable_email_interceptor_spec.rb +++ b/spec/lib/disable_email_interceptor_spec.rb @@ -6,7 +6,7 @@ describe DisableEmailInterceptor do end it 'should not send emails' do - Gitlab.config.gitlab.stub(:email_enabled).and_return(false) + allow(Gitlab.config.gitlab).to receive(:email_enabled).and_return(false) expect { deliver_mail }.not_to change(ActionMailer::Base.deliveries, :count) diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index 7b3818ea5c8..ac602eac154 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -14,44 +14,46 @@ describe ExtractsPath do describe '#extract_ref' do it "returns an empty pair when no @project is set" do @project = nil - extract_ref('master/CHANGELOG').should == ['', ''] + expect(extract_ref('master/CHANGELOG')).to eq(['', '']) end context "without a path" do it "extracts a valid branch" do - extract_ref('master').should == ['master', ''] + expect(extract_ref('master')).to eq(['master', '']) end it "extracts a valid tag" do - extract_ref('v2.0.0').should == ['v2.0.0', ''] + expect(extract_ref('v2.0.0')).to eq(['v2.0.0', '']) end it "extracts a valid commit ref without a path" do - extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062').should == + expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062')).to eq( ['f4b14494ef6abf3d144c28e4af0c20143383e062', ''] + ) end it "falls back to a primitive split for an invalid ref" do - extract_ref('stable').should == ['stable', ''] + expect(extract_ref('stable')).to eq(['stable', '']) end end context "with a path" do it "extracts a valid branch" do - extract_ref('foo/bar/baz/CHANGELOG').should == ['foo/bar/baz', 'CHANGELOG'] + expect(extract_ref('foo/bar/baz/CHANGELOG')).to eq(['foo/bar/baz', 'CHANGELOG']) end it "extracts a valid tag" do - extract_ref('v2.0.0/CHANGELOG').should == ['v2.0.0', 'CHANGELOG'] + expect(extract_ref('v2.0.0/CHANGELOG')).to eq(['v2.0.0', 'CHANGELOG']) end it "extracts a valid commit SHA" do - extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG').should == + expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq( ['f4b14494ef6abf3d144c28e4af0c20143383e062', 'CHANGELOG'] + ) end it "falls back to a primitive split for an invalid ref" do - extract_ref('stable/CHANGELOG').should == ['stable', 'CHANGELOG'] + expect(extract_ref('stable/CHANGELOG')).to eq(['stable', 'CHANGELOG']) end end end diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb index b2469c18395..4633b6f3934 100644 --- a/spec/lib/git_ref_validator_spec.rb +++ b/spec/lib/git_ref_validator_spec.rb @@ -1,20 +1,20 @@ require 'spec_helper' describe Gitlab::GitRefValidator do - it { Gitlab::GitRefValidator.validate('feature/new').should be_true } - it { Gitlab::GitRefValidator.validate('implement_@all').should be_true } - it { Gitlab::GitRefValidator.validate('my_new_feature').should be_true } - it { Gitlab::GitRefValidator.validate('#1').should be_true } - it { Gitlab::GitRefValidator.validate('feature/~new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/^new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/:new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/?new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/*new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/[new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/new/').should be_false } - it { Gitlab::GitRefValidator.validate('feature/new.').should be_false } - it { Gitlab::GitRefValidator.validate('feature\@{').should be_false } - it { Gitlab::GitRefValidator.validate('feature\new').should be_false } - it { Gitlab::GitRefValidator.validate('feature//new').should be_false } - it { Gitlab::GitRefValidator.validate('feature new').should be_false } + it { expect(Gitlab::GitRefValidator.validate('feature/new')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy } + it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/?new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/*new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/[new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/new/')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature/new.')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature\@{')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey } + it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey } end diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb index f00ec0fa401..27279465c1a 100644 --- a/spec/lib/gitlab/backend/shell_spec.rb +++ b/spec/lib/gitlab/backend/shell_spec.rb @@ -8,11 +8,11 @@ describe Gitlab::Shell do Project.stub(find: project) end - it { should respond_to :add_key } - it { should respond_to :remove_key } - it { should respond_to :add_repository } - it { should respond_to :remove_repository } - it { should respond_to :fork_repository } + it { is_expected.to respond_to :add_key } + it { is_expected.to respond_to :remove_key } + it { is_expected.to respond_to :add_repository } + it { is_expected.to respond_to :remove_repository } + it { is_expected.to respond_to :fork_repository } - it { gitlab_shell.url_to_repo('diaspora').should == Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git" } + it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") } end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 0a1f3fa351d..c96ee78e5fd 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -9,122 +9,122 @@ describe Gitlab::ClosingIssueExtractor do context 'with a single reference' do it do message = "Awesome commit (Closes ##{iid1})" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Awesome commit (closes ##{iid1})" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Closed ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "closed ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Closing ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "closing ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Close ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "close ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Awesome commit (Fixes ##{iid1})" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Awesome commit (fixes ##{iid1})" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Fixed ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "fixed ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Fixing ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "fixing ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Fix ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "fix ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Awesome commit (Resolves ##{iid1})" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Awesome commit (resolves ##{iid1})" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Resolved ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "resolved ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Resolving ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "resolving ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "Resolve ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end it do message = "resolve ##{iid1}" - subject.closed_by_message_in_project(message, project).should == [issue] + expect(subject.closed_by_message_in_project(message, project)).to eq([issue]) end end @@ -137,37 +137,37 @@ describe Gitlab::ClosingIssueExtractor do it 'fetches issues in single line message' do message = "Closes ##{iid1} and fix ##{iid2}" - subject.closed_by_message_in_project(message, project). - should == [issue, other_issue] + expect(subject.closed_by_message_in_project(message, project)). + to eq([issue, other_issue]) end it 'fetches comma-separated issues references in single line message' do message = "Closes ##{iid1}, closes ##{iid2}" - subject.closed_by_message_in_project(message, project). - should == [issue, other_issue] + expect(subject.closed_by_message_in_project(message, project)). + to eq([issue, other_issue]) end it 'fetches comma-separated issues numbers in single line message' do message = "Closes ##{iid1}, ##{iid2} and ##{iid3}" - subject.closed_by_message_in_project(message, project). - should == [issue, other_issue, third_issue] + expect(subject.closed_by_message_in_project(message, project)). + to eq([issue, other_issue, third_issue]) end it 'fetches issues in multi-line message' do message = "Awesome commit (closes ##{iid1})\nAlso fixes ##{iid2}" - subject.closed_by_message_in_project(message, project). - should == [issue, other_issue] + expect(subject.closed_by_message_in_project(message, project)). + to eq([issue, other_issue]) end it 'fetches issues in hybrid message' do message = "Awesome commit (closes ##{iid1})\n"\ "Also fixing issues ##{iid2}, ##{iid3} and #4" - subject.closed_by_message_in_project(message, project). - should == [issue, other_issue, third_issue] + expect(subject.closed_by_message_in_project(message, project)). + to eq([issue, other_issue, third_issue]) end end end diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index cf0b5c282c1..40eb45e37ca 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -11,11 +11,11 @@ describe Gitlab::Diff::File do describe :diff_lines do let(:diff_lines) { diff_file.diff_lines } - it { diff_lines.size.should == 30 } - it { diff_lines.first.should be_kind_of(Gitlab::Diff::Line) } + it { expect(diff_lines.size).to eq(30) } + it { expect(diff_lines.first).to be_kind_of(Gitlab::Diff::Line) } end describe :mode_changed? do - it { diff_file.mode_changed?.should be_false } + it { expect(diff_file.mode_changed?).to be_falsey } end end diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index 35b78260acd..918f6d0ead4 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -50,43 +50,43 @@ eos @lines = parser.parse(diff.lines) end - it { @lines.size.should == 30 } + it { expect(@lines.size).to eq(30) } describe 'lines' do describe 'first line' do let(:line) { @lines.first } - it { line.type.should == 'match' } - it { line.old_pos.should == 6 } - it { line.new_pos.should == 6 } - it { line.text.should == '@@ -6,12 +6,18 @@ module Popen' } + it { expect(line.type).to eq('match') } + it { expect(line.old_pos).to eq(6) } + it { expect(line.new_pos).to eq(6) } + it { expect(line.text).to eq('@@ -6,12 +6,18 @@ module Popen') } end describe 'removal line' do let(:line) { @lines[10] } - it { line.type.should == 'old' } - it { line.old_pos.should == 14 } - it { line.new_pos.should == 13 } - it { line.text.should == '- options = { chdir: path }' } + it { expect(line.type).to eq('old') } + it { expect(line.old_pos).to eq(14) } + it { expect(line.new_pos).to eq(13) } + it { expect(line.text).to eq('- options = { chdir: path }') } end describe 'addition line' do let(:line) { @lines[16] } - it { line.type.should == 'new' } - it { line.old_pos.should == 15 } - it { line.new_pos.should == 18 } - it { line.text.should == '+ options = {' } + it { expect(line.type).to eq('new') } + it { expect(line.old_pos).to eq(15) } + it { expect(line.new_pos).to eq(18) } + it { expect(line.text).to eq('+ options = {') } end describe 'unchanged line' do let(:line) { @lines.last } - it { line.type.should == nil } - it { line.old_pos.should == 24 } - it { line.new_pos.should == 31 } - it { line.text.should == ' @cmd_output << stderr.read' } + it { expect(line.type).to eq(nil) } + it { expect(line.old_pos).to eq(24) } + it { expect(line.new_pos).to eq(31) } + it { expect(line.text).to eq(' @cmd_output << stderr.read') } end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index fbcaa405f8d..666398eedd4 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -9,17 +9,17 @@ describe Gitlab::GitAccess do describe 'push to none protected branch' do it "returns true if user is a master" do project.team << [user, :master] - Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch").should be_true + expect(Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch")).to be_truthy end it "returns true if user is a developer" do project.team << [user, :developer] - Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch").should be_true + expect(Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch")).to be_truthy end it "returns false if user is a reporter" do project.team << [user, :reporter] - Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch").should be_false + expect(Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch")).to be_falsey end end @@ -30,17 +30,17 @@ describe Gitlab::GitAccess do it "returns true if user is a master" do project.team << [user, :master] - Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name).should be_true + expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_truthy end it "returns false if user is a developer" do project.team << [user, :developer] - Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name).should be_false + expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_falsey end it "returns false if user is a reporter" do project.team << [user, :reporter] - Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name).should be_false + expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_falsey end end @@ -51,17 +51,17 @@ describe Gitlab::GitAccess do it "returns true if user is a master" do project.team << [user, :master] - Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name).should be_true + expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_truthy end it "returns true if user is a developer" do project.team << [user, :developer] - Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name).should be_true + expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_truthy end it "returns false if user is a reporter" do project.team << [user, :reporter] - Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name).should be_false + expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_falsey end end @@ -74,7 +74,7 @@ describe Gitlab::GitAccess do context 'pull code' do subject { access.download_access_check(user, project) } - it { subject.allowed?.should be_true } + it { expect(subject.allowed?).to be_truthy } end end @@ -84,7 +84,7 @@ describe Gitlab::GitAccess do context 'pull code' do subject { access.download_access_check(user, project) } - it { subject.allowed?.should be_false } + it { expect(subject.allowed?).to be_falsey } end end @@ -97,7 +97,7 @@ describe Gitlab::GitAccess do context 'pull code' do subject { access.download_access_check(user, project) } - it { subject.allowed?.should be_false } + it { expect(subject.allowed?).to be_falsey } end end @@ -105,7 +105,7 @@ describe Gitlab::GitAccess do context 'pull code' do subject { access.download_access_check(user, project) } - it { subject.allowed?.should be_false } + it { expect(subject.allowed?).to be_falsey } end end @@ -117,13 +117,13 @@ describe Gitlab::GitAccess do before { key.projects << project } subject { access.download_access_check(key, project) } - it { subject.allowed?.should be_true } + it { expect(subject.allowed?).to be_truthy } end context 'denied' do subject { access.download_access_check(key, project) } - it { subject.allowed?.should be_false } + it { expect(subject.allowed?).to be_falsey } end end end @@ -207,7 +207,7 @@ describe Gitlab::GitAccess do context action do subject { access.push_access_check(user, project, changes[action]) } - it { subject.allowed?.should allowed ? be_true : be_false } + it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } end end end @@ -223,7 +223,7 @@ describe Gitlab::GitAccess do context action do subject { access.push_access_check(user, project, changes[action]) } - it { subject.allowed?.should allowed ? be_true : be_false } + it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } end end end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 4ff45c0c616..c31c6764091 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::GitAccessWiki do subject { access.push_access_check(user, project, changes) } - it { subject.allowed?.should be_true } + it { expect(subject.allowed?).to be_truthy } end def changes diff --git a/spec/lib/gitlab/github/project_creator.rb b/spec/lib/gitlab/github/project_creator.rb index 0bade5619a5..3686ddbf170 100644 --- a/spec/lib/gitlab/github/project_creator.rb +++ b/spec/lib/gitlab/github/project_creator.rb @@ -13,13 +13,13 @@ describe Gitlab::Github::ProjectCreator do let(:namespace){ create(:namespace) } it 'creates project' do - Project.any_instance.stub(:add_import_job) + allow_any_instance_of(Project).to receive(:add_import_job) project_creator = Gitlab::Github::ProjectCreator.new(repo, namespace, user) project_creator.execute project = Project.last - project.import_url.should == "https://asdffg@gitlab.com/asd/vim.git" - project.visibility_level.should == Gitlab::VisibilityLevel::PRIVATE + expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git") + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) end end diff --git a/spec/lib/gitlab/gitlab_import/project_creator.rb b/spec/lib/gitlab/gitlab_import/project_creator.rb index 51f3534ed60..e5d917830b0 100644 --- a/spec/lib/gitlab/gitlab_import/project_creator.rb +++ b/spec/lib/gitlab/gitlab_import/project_creator.rb @@ -13,13 +13,13 @@ describe Gitlab::GitlabImport::ProjectCreator do let(:namespace){ create(:namespace) } it 'creates project' do - Project.any_instance.stub(:add_import_job) + allow_any_instance_of(Project).to receive(:add_import_job) project_creator = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, user) project_creator.execute project = Project.last - project.import_url.should == "https://oauth2:asdffg@gitlab.com/asd/vim.git" - project.visibility_level.should == Gitlab::VisibilityLevel::PRIVATE + expect(project.import_url).to eq("https://oauth2:asdffg@gitlab.com/asd/vim.git") + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) end end diff --git a/spec/lib/gitlab/gitlab_markdown_helper_spec.rb b/spec/lib/gitlab/gitlab_markdown_helper_spec.rb index 540618a5603..ab613193f41 100644 --- a/spec/lib/gitlab/gitlab_markdown_helper_spec.rb +++ b/spec/lib/gitlab/gitlab_markdown_helper_spec.rb @@ -5,24 +5,24 @@ describe Gitlab::MarkdownHelper do %w(textile rdoc org creole wiki mediawiki rst adoc asciidoc asc).each do |type| it "returns true for #{type} files" do - Gitlab::MarkdownHelper.markup?("README.#{type}").should be_true + expect(Gitlab::MarkdownHelper.markup?("README.#{type}")).to be_truthy end end it 'returns false when given a non-markup filename' do - Gitlab::MarkdownHelper.markup?('README.rb').should_not be_true + expect(Gitlab::MarkdownHelper.markup?('README.rb')).not_to be_truthy end end describe '#gitlab_markdown?' do %w(mdown md markdown).each do |type| it "returns true for #{type} files" do - Gitlab::MarkdownHelper.gitlab_markdown?("README.#{type}").should be_true + expect(Gitlab::MarkdownHelper.gitlab_markdown?("README.#{type}")).to be_truthy end end it 'returns false when given a non-markdown filename' do - Gitlab::MarkdownHelper.gitlab_markdown?('README.rb').should_not be_true + expect(Gitlab::MarkdownHelper.gitlab_markdown?('README.rb')).not_to be_truthy end end end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 4573b8696c4..a2b05249147 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -10,7 +10,7 @@ describe Gitlab::LDAP::Access do context 'when the user cannot be found' do before { Gitlab::LDAP::Person.stub(find_by_dn: nil) } - it { should be_false } + it { is_expected.to be_falsey } end context 'when the user is found' do @@ -19,13 +19,13 @@ describe Gitlab::LDAP::Access do context 'and the user is diabled via active directory' do before { Gitlab::LDAP::Person.stub(disabled_via_active_directory?: true) } - it { should be_false } + it { is_expected.to be_falsey } end context 'and has no disabled flag in active diretory' do before { Gitlab::LDAP::Person.stub(disabled_via_active_directory?: false) } - it { should be_true } + it { is_expected.to be_truthy } end context 'without ActiveDirectory enabled' do @@ -34,7 +34,7 @@ describe Gitlab::LDAP::Access do Gitlab::LDAP::Config.any_instance.stub(active_directory: false) end - it { should be_true } + it { is_expected.to be_truthy } end end end diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index 19347e47378..b609e4b38f2 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -12,20 +12,20 @@ describe Gitlab::LDAP::Adapter do context "and the result is non-empty" do before { ldap.stub(search: [:foo]) } - it { should be_true } + it { is_expected.to be_truthy } end context "and the result is empty" do before { ldap.stub(search: []) } - it { should be_false } + it { is_expected.to be_falsey } end end context "when the search encounters an error" do before { ldap.stub(search: nil, get_operation_result: double(code: 1, message: 'some error')) } - it { should be_false } + it { is_expected.to be_falsey } end end end diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb index 11fdf108756..8afc2b21f46 100644 --- a/spec/lib/gitlab/ldap/authentication_spec.rb +++ b/spec/lib/gitlab/ldap/authentication_spec.rb @@ -19,7 +19,7 @@ describe Gitlab::LDAP::Authentication do klass.any_instance.stub(adapter: double(:adapter, bind_as: double(:ldap_user, dn: dn) )) - expect(klass.login(login, password)).to be_true + expect(klass.login(login, password)).to be_truthy end it "is false if the user does not exist" do @@ -27,27 +27,27 @@ describe Gitlab::LDAP::Authentication do klass.any_instance.stub(adapter: double(:adapter, bind_as: double(:ldap_user, dn: dn) )) - expect(klass.login(login, password)).to be_false + expect(klass.login(login, password)).to be_falsey end it "is false if authentication fails" do user # try only to fake the LDAP call klass.any_instance.stub(adapter: double(:adapter, bind_as: nil)) - expect(klass.login(login, password)).to be_false + expect(klass.login(login, password)).to be_falsey end it "fails if ldap is disabled" do Gitlab::LDAP::Config.stub(enabled?: false) - expect(klass.login(login, password)).to be_false + expect(klass.login(login, password)).to be_falsey end it "fails if no login is supplied" do - expect(klass.login('', password)).to be_false + expect(klass.login('', password)).to be_falsey end it "fails if no password is supplied" do - expect(klass.login(login, '')).to be_false + expect(klass.login(login, '')).to be_falsey end end end \ No newline at end of file diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 3ebb8aae243..2df2beca7a6 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::LDAP::Config do let(:config) { Gitlab::LDAP::Config.new provider } let(:provider) { 'ldapmain' } - describe :initalize do + describe '#initalize' do it 'requires a provider' do expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 63ffc21ba3b..4f93545feb6 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -16,17 +16,17 @@ describe Gitlab::LDAP::User do describe :changed? do it "marks existing ldap user as changed" do existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') - expect(gl_user.changed?).to be_true + expect(gl_user.changed?).to be_truthy end it "marks existing non-ldap user if the email matches as changed" do existing_user = create(:user, email: 'john@example.com') - expect(gl_user.changed?).to be_true + expect(gl_user.changed?).to be_truthy end it "dont marks existing ldap user as changed" do existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain') - expect(gl_user.changed?).to be_false + expect(gl_user.changed?).to be_falsey end end diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/oauth/user_spec.rb index 88307515789..adfae5e5b4b 100644 --- a/spec/lib/gitlab/oauth/user_spec.rb +++ b/spec/lib/gitlab/oauth/user_spec.rb @@ -19,12 +19,12 @@ describe Gitlab::OAuth::User do it "finds an existing user based on uid and provider (facebook)" do auth = double(info: double(name: 'John'), uid: 'my-uid', provider: 'my-provider') - expect( oauth_user.persisted? ).to be_true + expect( oauth_user.persisted? ).to be_truthy end it "returns false if use is not found in database" do auth_hash.stub(uid: 'non-existing') - expect( oauth_user.persisted? ).to be_false + expect( oauth_user.persisted? ).to be_falsey end end @@ -62,8 +62,8 @@ describe Gitlab::OAuth::User do it do oauth_user.save - gl_user.should be_valid - gl_user.should_not be_blocked + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked end end @@ -72,8 +72,8 @@ describe Gitlab::OAuth::User do it do oauth_user.save - gl_user.should be_valid - gl_user.should be_blocked + expect(gl_user).to be_valid + expect(gl_user).to be_blocked end end end @@ -89,8 +89,8 @@ describe Gitlab::OAuth::User do it do oauth_user.save - gl_user.should be_valid - gl_user.should_not be_blocked + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked end end @@ -99,8 +99,8 @@ describe Gitlab::OAuth::User do it do oauth_user.save - gl_user.should be_valid - gl_user.should_not be_blocked + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked end end end diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index 76d506eb3c0..cd9d0456b25 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -13,8 +13,8 @@ describe 'Gitlab::Popen', no_db: true do @output, @status = @klass.new.popen(%W(ls), path) end - it { @status.should be_zero } - it { @output.should include('cache') } + it { expect(@status).to be_zero } + it { expect(@output).to include('cache') } end context 'non-zero status' do @@ -22,8 +22,8 @@ describe 'Gitlab::Popen', no_db: true do @output, @status = @klass.new.popen(%W(cat NOTHING), path) end - it { @status.should == 1 } - it { @output.should include('No such file or directory') } + it { expect(@status).to eq(1) } + it { expect(@output).to include('No such file or directory') } end context 'unsafe string command' do @@ -37,8 +37,8 @@ describe 'Gitlab::Popen', no_db: true do @output, @status = @klass.new.popen(%W(ls)) end - it { @status.should be_zero } - it { @output.should include('spec') } + it { expect(@status).to be_zero } + it { expect(@output).to include('spec') } end end diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb index 691fd133637..da25d45f1ff 100644 --- a/spec/lib/gitlab/push_data_builder_spec.rb +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -8,12 +8,12 @@ describe 'Gitlab::PushDataBuilder' do describe :build_sample do let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) } - it { data.should be_a(Hash) } - it { data[:before].should == '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } - it { data[:after].should == '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } - it { data[:ref].should == 'refs/heads/master' } - it { data[:commits].size.should == 3 } - it { data[:total_commits_count].should == 3 } + it { expect(data).to be_a(Hash) } + it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } + it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } + it { expect(data[:ref]).to eq('refs/heads/master') } + it { expect(data[:commits].size).to eq(3) } + it { expect(data[:total_commits_count]).to eq(3) } end describe :build do @@ -25,12 +25,12 @@ describe 'Gitlab::PushDataBuilder' do 'refs/tags/v1.1.0') end - it { data.should be_a(Hash) } - it { data[:before].should == Gitlab::Git::BLANK_SHA } - it { data[:checkout_sha].should == '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } - it { data[:after].should == '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b' } - it { data[:ref].should == 'refs/tags/v1.1.0' } - it { data[:commits].should be_empty } - it { data[:total_commits_count].should be_zero } + it { expect(data).to be_a(Hash) } + it { expect(data[:before]).to eq(Gitlab::Git::BLANK_SHA) } + it { expect(data[:checkout_sha]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } + it { expect(data[:after]).to eq('8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b') } + it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } + it { expect(data[:commits]).to be_empty } + it { expect(data[:total_commits_count]).to be_zero } end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 5f45df4e8c3..0847c31258c 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -3,51 +3,51 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor do it 'extracts username references' do subject.analyze('this contains a @user reference', nil) - subject.users.should == [{ project: nil, id: 'user' }] + expect(subject.users).to eq([{ project: nil, id: 'user' }]) end it 'extracts issue references' do subject.analyze('this one talks about issue #1234', nil) - subject.issues.should == [{ project: nil, id: '1234' }] + expect(subject.issues).to eq([{ project: nil, id: '1234' }]) end it 'extracts JIRA issue references' do subject.analyze('this one talks about issue JIRA-1234', nil) - subject.issues.should == [{ project: nil, id: 'JIRA-1234' }] + expect(subject.issues).to eq([{ project: nil, id: 'JIRA-1234' }]) end it 'extracts merge request references' do subject.analyze("and here's !43, a merge request", nil) - subject.merge_requests.should == [{ project: nil, id: '43' }] + expect(subject.merge_requests).to eq([{ project: nil, id: '43' }]) end it 'extracts snippet ids' do subject.analyze('snippets like $12 get extracted as well', nil) - subject.snippets.should == [{ project: nil, id: '12' }] + expect(subject.snippets).to eq([{ project: nil, id: '12' }]) end it 'extracts commit shas' do subject.analyze('commit shas 98cf0ae3 are pulled out as Strings', nil) - subject.commits.should == [{ project: nil, id: '98cf0ae3' }] + expect(subject.commits).to eq([{ project: nil, id: '98cf0ae3' }]) end it 'extracts multiple references and preserves their order' do subject.analyze('@me and @you both care about this', nil) - subject.users.should == [ + expect(subject.users).to eq([ { project: nil, id: 'me' }, { project: nil, id: 'you' } - ] + ]) end it 'leaves the original note unmodified' do text = 'issue #123 is just the worst, @user' subject.analyze(text, nil) - text.should == 'issue #123 is just the worst, @user' + expect(text).to eq('issue #123 is just the worst, @user') end it 'handles all possible kinds of references' do accessors = Gitlab::Markdown::TYPES.map { |t| "#{t}s".to_sym } - subject.should respond_to(*accessors) + expect(subject).to respond_to(*accessors) end context 'with a project' do @@ -62,7 +62,7 @@ describe Gitlab::ReferenceExtractor do project.team << [@u_bar, :guest] subject.analyze('@foo, @baduser, @bar, and @offteam', project) - subject.users_for(project).should == [@u_foo, @u_bar] + expect(subject.users_for(project)).to eq([@u_foo, @u_bar]) end it 'accesses valid issue objects' do @@ -70,7 +70,7 @@ describe Gitlab::ReferenceExtractor do @i1 = create(:issue, project: project) subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.", project) - subject.issues_for(project).should == [@i0, @i1] + expect(subject.issues_for(project)).to eq([@i0, @i1]) end it 'accesses valid merge requests' do @@ -78,7 +78,7 @@ describe Gitlab::ReferenceExtractor do @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'bbb') subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.", project) - subject.merge_requests_for(project).should == [@m1, @m0] + expect(subject.merge_requests_for(project)).to eq([@m1, @m0]) end it 'accesses valid snippets' do @@ -87,7 +87,7 @@ describe Gitlab::ReferenceExtractor do @s2 = create(:project_snippet) subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}", project) - subject.snippets_for(project).should == [@s0, @s1] + expect(subject.snippets_for(project)).to eq([@s0, @s1]) end it 'accesses valid commits' do @@ -96,9 +96,9 @@ describe Gitlab::ReferenceExtractor do subject.analyze("this references commits #{commit.sha[0..6]} and 012345", project) extracted = subject.commits_for(project) - extracted.should have(1).item - extracted[0].sha.should == commit.sha - extracted[0].message.should == commit.message + expect(extracted.size).to eq(1) + expect(extracted[0].sha).to eq(commit.sha) + expect(extracted[0].message).to eq(commit.message) end end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index a3aae7771bd..1db9f15b790 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -2,20 +2,20 @@ require 'spec_helper' describe Gitlab::Regex do describe 'path regex' do - it { 'gitlab-ce'.should match(Gitlab::Regex.path_regex) } - it { 'gitlab_git'.should match(Gitlab::Regex.path_regex) } - it { '_underscore.js'.should match(Gitlab::Regex.path_regex) } - it { '100px.com'.should match(Gitlab::Regex.path_regex) } - it { '?gitlab'.should_not match(Gitlab::Regex.path_regex) } - it { 'git lab'.should_not match(Gitlab::Regex.path_regex) } - it { 'gitlab.git'.should_not match(Gitlab::Regex.path_regex) } + it { expect('gitlab-ce').to match(Gitlab::Regex.path_regex) } + it { expect('gitlab_git').to match(Gitlab::Regex.path_regex) } + it { expect('_underscore.js').to match(Gitlab::Regex.path_regex) } + it { expect('100px.com').to match(Gitlab::Regex.path_regex) } + it { expect('?gitlab').not_to match(Gitlab::Regex.path_regex) } + it { expect('git lab').not_to match(Gitlab::Regex.path_regex) } + it { expect('gitlab.git').not_to match(Gitlab::Regex.path_regex) } end describe 'project name regex' do - it { 'gitlab-ce'.should match(Gitlab::Regex.project_name_regex) } - it { 'GitLab CE'.should match(Gitlab::Regex.project_name_regex) } - it { '100 lines'.should match(Gitlab::Regex.project_name_regex) } - it { 'gitlab.git'.should match(Gitlab::Regex.project_name_regex) } - it { '?gitlab'.should_not match(Gitlab::Regex.project_name_regex) } + it { expect('gitlab-ce').to match(Gitlab::Regex.project_name_regex) } + it { expect('GitLab CE').to match(Gitlab::Regex.project_name_regex) } + it { expect('100 lines').to match(Gitlab::Regex.project_name_regex) } + it { expect('gitlab.git').to match(Gitlab::Regex.project_name_regex) } + it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) } end end diff --git a/spec/lib/gitlab/satellite/action_spec.rb b/spec/lib/gitlab/satellite/action_spec.rb index 3eb1258d67e..28e3d64ee2b 100644 --- a/spec/lib/gitlab/satellite/action_spec.rb +++ b/spec/lib/gitlab/satellite/action_spec.rb @@ -6,7 +6,7 @@ describe 'Gitlab::Satellite::Action' do describe '#prepare_satellite!' do it 'should be able to fetch timeout from conf' do - Gitlab::Satellite::Action::DEFAULT_OPTIONS[:git_timeout].should == 30.seconds + expect(Gitlab::Satellite::Action::DEFAULT_OPTIONS[:git_timeout]).to eq(30.seconds) end it 'create a repository with a parking branch and one remote: origin' do @@ -15,22 +15,22 @@ describe 'Gitlab::Satellite::Action' do #now lets dirty it up starting_remote_count = repo.git.list_remotes.size - starting_remote_count.should >= 1 + expect(starting_remote_count).to be >= 1 #kind of hookey way to add a second remote origin_uri = repo.git.remote({v: true}).split(" ")[1] begin repo.git.remote({raise: true}, 'add', 'another-remote', origin_uri) repo.git.branch({raise: true}, 'a-new-branch') - repo.heads.size.should > (starting_remote_count) - repo.git.remote().split(" ").size.should > (starting_remote_count) + expect(repo.heads.size).to be > (starting_remote_count) + expect(repo.git.remote().split(" ").size).to be > (starting_remote_count) rescue end repo.git.config({}, "user.name", "#{user.name} -- foo") repo.git.config({}, "user.email", "#{user.email} -- foo") - repo.config['user.name'].should =="#{user.name} -- foo" - repo.config['user.email'].should =="#{user.email} -- foo" + expect(repo.config['user.name']).to eq("#{user.name} -- foo") + expect(repo.config['user.email']).to eq("#{user.email} -- foo") #These must happen in the context of the satellite directory... @@ -42,13 +42,13 @@ describe 'Gitlab::Satellite::Action' do #verify it's clean heads = repo.heads.map(&:name) - heads.size.should == 1 - heads.include?(Gitlab::Satellite::Satellite::PARKING_BRANCH).should == true + expect(heads.size).to eq(1) + expect(heads.include?(Gitlab::Satellite::Satellite::PARKING_BRANCH)).to eq(true) remotes = repo.git.remote().split(' ') - remotes.size.should == 1 - remotes.include?('origin').should == true - repo.config['user.name'].should ==user.name - repo.config['user.email'].should ==user.email + expect(remotes.size).to eq(1) + expect(remotes.include?('origin')).to eq(true) + expect(repo.config['user.name']).to eq(user.name) + expect(repo.config['user.email']).to eq(user.email) end end @@ -61,16 +61,16 @@ describe 'Gitlab::Satellite::Action' do #set assumptions FileUtils.rm_f(project.satellite.lock_file) - File.exists?(project.satellite.lock_file).should be_false + expect(File.exists?(project.satellite.lock_file)).to be_falsey satellite_action = Gitlab::Satellite::Action.new(user, project) satellite_action.send(:in_locked_and_timed_satellite) do |sat_repo| - repo.should == sat_repo - (File.exists? project.satellite.lock_file).should be_true + expect(repo).to eq(sat_repo) + expect(File.exists? project.satellite.lock_file).to be_truthy called = true end - called.should be_true + expect(called).to be_truthy end @@ -80,19 +80,19 @@ describe 'Gitlab::Satellite::Action' do # Set base assumptions if File.exists? project.satellite.lock_file - FileLockStatusChecker.new(project.satellite.lock_file).flocked?.should be_false + expect(FileLockStatusChecker.new(project.satellite.lock_file).flocked?).to be_falsey end satellite_action = Gitlab::Satellite::Action.new(user, project) satellite_action.send(:in_locked_and_timed_satellite) do |sat_repo| called = true - repo.should == sat_repo - (File.exists? project.satellite.lock_file).should be_true - FileLockStatusChecker.new(project.satellite.lock_file).flocked?.should be_true + expect(repo).to eq(sat_repo) + expect(File.exists? project.satellite.lock_file).to be_truthy + expect(FileLockStatusChecker.new(project.satellite.lock_file).flocked?).to be_truthy end - called.should be_true - FileLockStatusChecker.new(project.satellite.lock_file).flocked?.should be_false + expect(called).to be_truthy + expect(FileLockStatusChecker.new(project.satellite.lock_file).flocked?).to be_falsey end diff --git a/spec/lib/gitlab/satellite/merge_action_spec.rb b/spec/lib/gitlab/satellite/merge_action_spec.rb index 479a73a1081..915e3ff0e51 100644 --- a/spec/lib/gitlab/satellite/merge_action_spec.rb +++ b/spec/lib/gitlab/satellite/merge_action_spec.rb @@ -13,9 +13,9 @@ describe 'Gitlab::Satellite::MergeAction' do describe '#commits_between' do def verify_commits(commits, first_commit_sha, last_commit_sha) - commits.each { |commit| commit.class.should == Gitlab::Git::Commit } - commits.first.id.should == first_commit_sha - commits.last.id.should == last_commit_sha + commits.each { |commit| expect(commit.class).to eq(Gitlab::Git::Commit) } + expect(commits.first.id).to eq(first_commit_sha) + expect(commits.last.id).to eq(last_commit_sha) end context 'on fork' do @@ -35,7 +35,7 @@ describe 'Gitlab::Satellite::MergeAction' do describe '#format_patch' do def verify_content(patch) sample_compare.commits.each do |commit| - patch.include?(commit).should be_true + expect(patch.include?(commit)).to be_truthy end end @@ -57,11 +57,11 @@ describe 'Gitlab::Satellite::MergeAction' do describe '#diffs_between_satellite tested against diff_in_satellite' do def is_a_matching_diff(diff, diffs) diff_count = diff.scan('diff --git').size - diff_count.should >= 1 - diffs.size.should == diff_count + expect(diff_count).to be >= 1 + expect(diffs.size).to eq(diff_count) diffs.each do |a_diff| - a_diff.class.should == Gitlab::Git::Diff - (diff.include? a_diff.diff).should be_true + expect(a_diff.class).to eq(Gitlab::Git::Diff) + expect(diff.include? a_diff.diff).to be_truthy end end @@ -82,23 +82,23 @@ describe 'Gitlab::Satellite::MergeAction' do describe '#can_be_merged?' do context 'on fork' do - it { Gitlab::Satellite::MergeAction.new( + it { expect(Gitlab::Satellite::MergeAction.new( merge_request_fork.author, - merge_request_fork).can_be_merged?.should be_true } + merge_request_fork).can_be_merged?).to be_truthy } - it { Gitlab::Satellite::MergeAction.new( + it { expect(Gitlab::Satellite::MergeAction.new( merge_request_fork_with_conflict.author, - merge_request_fork_with_conflict).can_be_merged?.should be_false } + merge_request_fork_with_conflict).can_be_merged?).to be_falsey } end context 'between branches' do - it { Gitlab::Satellite::MergeAction.new( + it { expect(Gitlab::Satellite::MergeAction.new( merge_request.author, - merge_request).can_be_merged?.should be_true } + merge_request).can_be_merged?).to be_truthy } - it { Gitlab::Satellite::MergeAction.new( + it { expect(Gitlab::Satellite::MergeAction.new( merge_request_with_conflict.author, - merge_request_with_conflict).can_be_merged?.should be_false } + merge_request_with_conflict).can_be_merged?).to be_falsey } end end end diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb index 2b254d6b3a6..ce3ea6c260a 100644 --- a/spec/lib/gitlab/upgrader_spec.rb +++ b/spec/lib/gitlab/upgrader_spec.rb @@ -5,20 +5,20 @@ describe Gitlab::Upgrader do let(:current_version) { Gitlab::VERSION } describe 'current_version_raw' do - it { upgrader.current_version_raw.should == current_version } + it { expect(upgrader.current_version_raw).to eq(current_version) } end describe 'latest_version?' do it 'should be true if newest version' do upgrader.stub(latest_version_raw: current_version) - upgrader.latest_version?.should be_true + expect(upgrader.latest_version?).to be_truthy end end describe 'latest_version_raw' do it 'should be latest version for GitLab 5' do upgrader.stub(current_version_raw: "5.3.0") - upgrader.latest_version_raw.should == "v5.4.2" + expect(upgrader.latest_version_raw).to eq("v5.4.2") end end end diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb index 94dccf7a4e5..5afeb1c1ec3 100644 --- a/spec/lib/gitlab/version_info_spec.rb +++ b/spec/lib/gitlab/version_info_spec.rb @@ -12,58 +12,58 @@ describe 'Gitlab::VersionInfo', no_db: true do end context '>' do - it { @v2_0_0.should > @v1_1_0 } - it { @v1_1_0.should > @v1_0_1 } - it { @v1_0_1.should > @v1_0_0 } - it { @v1_0_0.should > @v0_1_0 } - it { @v0_1_0.should > @v0_0_1 } + it { expect(@v2_0_0).to be > @v1_1_0 } + it { expect(@v1_1_0).to be > @v1_0_1 } + it { expect(@v1_0_1).to be > @v1_0_0 } + it { expect(@v1_0_0).to be > @v0_1_0 } + it { expect(@v0_1_0).to be > @v0_0_1 } end context '>=' do - it { @v2_0_0.should >= Gitlab::VersionInfo.new(2, 0, 0) } - it { @v2_0_0.should >= @v1_1_0 } + it { expect(@v2_0_0).to be >= Gitlab::VersionInfo.new(2, 0, 0) } + it { expect(@v2_0_0).to be >= @v1_1_0 } end context '<' do - it { @v0_0_1.should < @v0_1_0 } - it { @v0_1_0.should < @v1_0_0 } - it { @v1_0_0.should < @v1_0_1 } - it { @v1_0_1.should < @v1_1_0 } - it { @v1_1_0.should < @v2_0_0 } + it { expect(@v0_0_1).to be < @v0_1_0 } + it { expect(@v0_1_0).to be < @v1_0_0 } + it { expect(@v1_0_0).to be < @v1_0_1 } + it { expect(@v1_0_1).to be < @v1_1_0 } + it { expect(@v1_1_0).to be < @v2_0_0 } end context '<=' do - it { @v0_0_1.should <= Gitlab::VersionInfo.new(0, 0, 1) } - it { @v0_0_1.should <= @v0_1_0 } + it { expect(@v0_0_1).to be <= Gitlab::VersionInfo.new(0, 0, 1) } + it { expect(@v0_0_1).to be <= @v0_1_0 } end context '==' do - it { @v0_0_1.should == Gitlab::VersionInfo.new(0, 0, 1) } - it { @v0_1_0.should == Gitlab::VersionInfo.new(0, 1, 0) } - it { @v1_0_0.should == Gitlab::VersionInfo.new(1, 0, 0) } + it { expect(@v0_0_1).to eq(Gitlab::VersionInfo.new(0, 0, 1)) } + it { expect(@v0_1_0).to eq(Gitlab::VersionInfo.new(0, 1, 0)) } + it { expect(@v1_0_0).to eq(Gitlab::VersionInfo.new(1, 0, 0)) } end context '!=' do - it { @v0_0_1.should_not == @v0_1_0 } + it { expect(@v0_0_1).not_to eq(@v0_1_0) } end context 'unknown' do - it { @unknown.should_not be @v0_0_1 } - it { @unknown.should_not be Gitlab::VersionInfo.new } + it { expect(@unknown).not_to be @v0_0_1 } + it { expect(@unknown).not_to be Gitlab::VersionInfo.new } it { expect{@unknown > @v0_0_1}.to raise_error(ArgumentError) } it { expect{@unknown < @v0_0_1}.to raise_error(ArgumentError) } end context 'parse' do - it { Gitlab::VersionInfo.parse("1.0.0").should == @v1_0_0 } - it { Gitlab::VersionInfo.parse("1.0.0.1").should == @v1_0_0 } - it { Gitlab::VersionInfo.parse("git 1.0.0b1").should == @v1_0_0 } - it { Gitlab::VersionInfo.parse("git 1.0b1").should_not be_valid } + it { expect(Gitlab::VersionInfo.parse("1.0.0")).to eq(@v1_0_0) } + it { expect(Gitlab::VersionInfo.parse("1.0.0.1")).to eq(@v1_0_0) } + it { expect(Gitlab::VersionInfo.parse("git 1.0.0b1")).to eq(@v1_0_0) } + it { expect(Gitlab::VersionInfo.parse("git 1.0b1")).not_to be_valid } end context 'to_s' do - it { @v1_0_0.to_s.should == "1.0.0" } - it { @unknown.to_s.should == "Unknown" } + it { expect(@v1_0_0.to_s).to eq("1.0.0") } + it { expect(@unknown.to_s).to eq("Unknown") } end end diff --git a/spec/lib/votes_spec.rb b/spec/lib/votes_spec.rb index a88a10d927f..df243a26008 100644 --- a/spec/lib/votes_spec.rb +++ b/spec/lib/votes_spec.rb @@ -5,107 +5,107 @@ describe Issue, 'Votes' do describe "#upvotes" do it "with no notes has a 0/0 score" do - issue.upvotes.should == 0 + expect(issue.upvotes).to eq(0) end it "should recognize non-+1 notes" do add_note "No +1 here" - issue.should have(1).note - issue.notes.first.upvote?.should be_false - issue.upvotes.should == 0 + expect(issue.notes.size).to eq(1) + expect(issue.notes.first.upvote?).to be_falsey + expect(issue.upvotes).to eq(0) end it "should recognize a single +1 note" do add_note "+1 This is awesome" - issue.upvotes.should == 1 + expect(issue.upvotes).to eq(1) end it 'should recognize multiple +1 notes' do add_note '+1 This is awesome', create(:user) add_note '+1 I want this', create(:user) - issue.upvotes.should == 2 + expect(issue.upvotes).to eq(2) end it 'should not count 2 +1 votes from the same user' do add_note '+1 This is awesome' add_note '+1 I want this' - issue.upvotes.should == 1 + expect(issue.upvotes).to eq(1) end end describe "#downvotes" do it "with no notes has a 0/0 score" do - issue.downvotes.should == 0 + expect(issue.downvotes).to eq(0) end it "should recognize non--1 notes" do add_note "Almost got a -1" - issue.should have(1).note - issue.notes.first.downvote?.should be_false - issue.downvotes.should == 0 + expect(issue.notes.size).to eq(1) + expect(issue.notes.first.downvote?).to be_falsey + expect(issue.downvotes).to eq(0) end it "should recognize a single -1 note" do add_note "-1 This is bad" - issue.downvotes.should == 1 + expect(issue.downvotes).to eq(1) end it "should recognize multiple -1 notes" do add_note('-1 This is bad', create(:user)) add_note('-1 Away with this', create(:user)) - issue.downvotes.should == 2 + expect(issue.downvotes).to eq(2) end end describe "#votes_count" do it "with no notes has a 0/0 score" do - issue.votes_count.should == 0 + expect(issue.votes_count).to eq(0) end it "should recognize non notes" do add_note "No +1 here" - issue.should have(1).note - issue.votes_count.should == 0 + expect(issue.notes.size).to eq(1) + expect(issue.votes_count).to eq(0) end it "should recognize a single +1 note" do add_note "+1 This is awesome" - issue.votes_count.should == 1 + expect(issue.votes_count).to eq(1) end it "should recognize a single -1 note" do add_note "-1 This is bad" - issue.votes_count.should == 1 + expect(issue.votes_count).to eq(1) end it "should recognize multiple notes" do add_note('+1 This is awesome', create(:user)) add_note('-1 This is bad', create(:user)) add_note('+1 I want this', create(:user)) - issue.votes_count.should == 3 + expect(issue.votes_count).to eq(3) end it 'should not count 2 -1 votes from the same user' do add_note '-1 This is suspicious' add_note '-1 This is bad' - issue.votes_count.should == 1 + expect(issue.votes_count).to eq(1) end end describe "#upvotes_in_percent" do it "with no notes has a 0% score" do - issue.upvotes_in_percent.should == 0 + expect(issue.upvotes_in_percent).to eq(0) end it "should count a single 1 note as 100%" do add_note "+1 This is awesome" - issue.upvotes_in_percent.should == 100 + expect(issue.upvotes_in_percent).to eq(100) end it 'should count multiple +1 notes as 100%' do add_note('+1 This is awesome', create(:user)) add_note('+1 I want this', create(:user)) - issue.upvotes_in_percent.should == 100 + expect(issue.upvotes_in_percent).to eq(100) end it 'should count fractions for multiple +1 and -1 notes correctly' do @@ -113,24 +113,24 @@ describe Issue, 'Votes' do add_note('+1 I want this', create(:user)) add_note('-1 This is bad', create(:user)) add_note('+1 me too', create(:user)) - issue.upvotes_in_percent.should == 75 + expect(issue.upvotes_in_percent).to eq(75) end end describe "#downvotes_in_percent" do it "with no notes has a 0% score" do - issue.downvotes_in_percent.should == 0 + expect(issue.downvotes_in_percent).to eq(0) end it "should count a single -1 note as 100%" do add_note "-1 This is bad" - issue.downvotes_in_percent.should == 100 + expect(issue.downvotes_in_percent).to eq(100) end it 'should count multiple -1 notes as 100%' do add_note('-1 This is bad', create(:user)) add_note('-1 Away with this', create(:user)) - issue.downvotes_in_percent.should == 100 + expect(issue.downvotes_in_percent).to eq(100) end it 'should count fractions for multiple +1 and -1 notes correctly' do @@ -138,7 +138,7 @@ describe Issue, 'Votes' do add_note('+1 I want this', create(:user)) add_note('-1 This is bad', create(:user)) add_note('+1 me too', create(:user)) - issue.downvotes_in_percent.should == 25 + expect(issue.downvotes_in_percent).to eq(25) end end @@ -151,8 +151,8 @@ describe Issue, 'Votes' do add_note('+1 this looks good now') add_note('+1 This is awesome', create(:user)) add_note('+1 me too', create(:user)) - issue.downvotes.should == 0 - issue.upvotes.should == 5 + expect(issue.downvotes).to eq(0) + expect(issue.upvotes).to eq(5) end it 'should count each users vote only once' do @@ -161,8 +161,8 @@ describe Issue, 'Votes' do add_note '+1 I still like this' add_note '+1 I really like this' add_note '+1 Give me this now!!!!' - issue.downvotes.should == 0 - issue.upvotes.should == 1 + expect(issue.downvotes).to eq(0) + expect(issue.upvotes).to eq(1) end it 'should count a users vote only once without caring about comments' do @@ -171,8 +171,8 @@ describe Issue, 'Votes' do add_note 'Another comment' add_note '+1 vote' add_note 'final comment' - issue.downvotes.should == 0 - issue.upvotes.should == 1 + expect(issue.downvotes).to eq(0) + expect(issue.upvotes).to eq(1) end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index c045f85052c..64367ed9d80 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -16,34 +16,34 @@ describe Notify do shared_examples 'a multiple recipients email' do it 'is sent to the given recipient' do - should deliver_to recipient.notification_email + is_expected.to deliver_to recipient.notification_email end end shared_examples 'an email sent from GitLab' do it 'is sent from GitLab' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq('GitLab') - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq('GitLab') + expect(sender.address).to eq(gitlab_sender) end end shared_examples 'an email starting a new thread' do |message_id_prefix| it 'has a discussion identifier' do - should have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - should have_header 'X-GitLab-Project', /#{project.name}/ + is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ end end shared_examples 'an answer to an existing thread' do |thread_id_prefix| it 'has a subject that begins with Re: ' do - should have_subject /^Re: / + is_expected.to have_subject /^Re: / end it 'has headers that reference an existing thread' do - should have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - should have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - should have_header 'X-GitLab-Project', /#{project.name}/ + is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ end end @@ -58,30 +58,30 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'is sent to the new user' do - should deliver_to new_user.email + is_expected.to deliver_to new_user.email end it 'has the correct subject' do - should have_subject /^Account was created for you$/i + is_expected.to have_subject /^Account was created for you$/i end it 'contains the new user\'s login name' do - should have_body_text /#{new_user.email}/ + is_expected.to have_body_text /#{new_user.email}/ end it 'contains the password text' do - should have_body_text /Click here to set your password/ + is_expected.to have_body_text /Click here to set your password/ end it 'includes a link for user to set password' do params = "reset_password_token=#{token}" - should have_body_text( + is_expected.to have_body_text( %r{http://localhost(:\d+)?/users/password/edit\?#{params}} ) end it 'includes a link to the site' do - should have_body_text /#{example_site_path}/ + is_expected.to have_body_text /#{example_site_path}/ end end @@ -95,23 +95,23 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'is sent to the new user' do - should deliver_to new_user.email + is_expected.to deliver_to new_user.email end it 'has the correct subject' do - should have_subject /^Account was created for you$/i + is_expected.to have_subject /^Account was created for you$/i end it 'contains the new user\'s login name' do - should have_body_text /#{new_user.email}/ + is_expected.to have_body_text /#{new_user.email}/ end it 'should not contain the new user\'s password' do - should_not have_body_text /password/ + is_expected.not_to have_body_text /password/ end it 'includes a link to the site' do - should have_body_text /#{example_site_path}/ + is_expected.to have_body_text /#{example_site_path}/ end end @@ -123,19 +123,19 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'is sent to the new user' do - should deliver_to key.user.email + is_expected.to deliver_to key.user.email end it 'has the correct subject' do - should have_subject /^SSH key was added to your account$/i + is_expected.to have_subject /^SSH key was added to your account$/i end it 'contains the new ssh key title' do - should have_body_text /#{key.title}/ + is_expected.to have_body_text /#{key.title}/ end it 'includes a link to ssh keys page' do - should have_body_text /#{profile_keys_path}/ + is_expected.to have_body_text /#{profile_keys_path}/ end end @@ -145,19 +145,19 @@ describe Notify do subject { Notify.new_email_email(email.id) } it 'is sent to the new user' do - should deliver_to email.user.email + is_expected.to deliver_to email.user.email end it 'has the correct subject' do - should have_subject /^Email was added to your account$/i + is_expected.to have_subject /^Email was added to your account$/i end it 'contains the new email address' do - should have_body_text /#{email.email}/ + is_expected.to have_body_text /#{email.email}/ end it 'includes a link to emails page' do - should have_body_text /#{profile_emails_path}/ + is_expected.to have_body_text /#{profile_emails_path}/ end end @@ -170,12 +170,12 @@ describe Notify do shared_examples 'an assignee email' do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'is sent to the assignee' do - should deliver_to assignee.email + is_expected.to deliver_to assignee.email end end @@ -190,11 +190,11 @@ describe Notify do it_behaves_like 'an email starting a new thread', 'issue' it 'has the correct subject' do - should have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ + is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ end it 'contains a link to the new issue' do - should have_body_text /#{project_issue_path project, issue}/ + is_expected.to have_body_text /#{project_issue_path project, issue}/ end end @@ -202,7 +202,7 @@ describe Notify do subject { Notify.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) } it 'contains the description' do - should have_body_text /#{issue_with_description.description}/ + is_expected.to have_body_text /#{issue_with_description.description}/ end end @@ -214,24 +214,24 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ end it 'contains the name of the previous assignee' do - should have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text /#{previous_assignee.name}/ end it 'contains the name of the new assignee' do - should have_body_text /#{assignee.name}/ + is_expected.to have_body_text /#{assignee.name}/ end it 'contains a link to the issue' do - should have_body_text /#{project_issue_path project, issue}/ + is_expected.to have_body_text /#{project_issue_path project, issue}/ end end @@ -243,24 +243,24 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{issue.title} \(##{issue.iid}\)/i + is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/i end it 'contains the new status' do - should have_body_text /#{status}/i + is_expected.to have_body_text /#{status}/i end it 'contains the user name' do - should have_body_text /#{current_user.name}/i + is_expected.to have_body_text /#{current_user.name}/i end it 'contains a link to the issue' do - should have_body_text /#{project_issue_path project, issue}/ + is_expected.to have_body_text /#{project_issue_path project, issue}/ end end @@ -278,23 +278,23 @@ describe Notify do it_behaves_like 'an email starting a new thread', 'merge_request' it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains a link to the new merge request' do - should have_body_text /#{project_merge_request_path(project, merge_request)}/ + is_expected.to have_body_text /#{project_merge_request_path(project, merge_request)}/ end it 'contains the source branch for the merge request' do - should have_body_text /#{merge_request.source_branch}/ + is_expected.to have_body_text /#{merge_request.source_branch}/ end it 'contains the target branch for the merge request' do - should have_body_text /#{merge_request.target_branch}/ + is_expected.to have_body_text /#{merge_request.target_branch}/ end it 'has the correct message-id set' do - should have_header 'Message-ID', "" + is_expected.to have_header 'Message-ID', "" end end @@ -302,7 +302,7 @@ describe Notify do subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } it 'contains the description' do - should have_body_text /#{merge_request_with_description.description}/ + is_expected.to have_body_text /#{merge_request_with_description.description}/ end end @@ -314,24 +314,24 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains the name of the previous assignee' do - should have_body_text /#{previous_assignee.name}/ + is_expected.to have_body_text /#{previous_assignee.name}/ end it 'contains the name of the new assignee' do - should have_body_text /#{assignee.name}/ + is_expected.to have_body_text /#{assignee.name}/ end it 'contains a link to the merge request' do - should have_body_text /#{project_merge_request_path project, merge_request}/ + is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/ end end @@ -343,24 +343,24 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(current_user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/i + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/i end it 'contains the new status' do - should have_body_text /#{status}/i + is_expected.to have_body_text /#{status}/i end it 'contains the user name' do - should have_body_text /#{current_user.name}/i + is_expected.to have_body_text /#{current_user.name}/i end it 'contains a link to the merge request' do - should have_body_text /#{project_merge_request_path project, merge_request}/ + is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/ end end @@ -372,20 +372,20 @@ describe Notify do it 'is sent as the merge author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(merge_author.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(merge_author.name) + expect(sender.address).to eq(gitlab_sender) end it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains the new status' do - should have_body_text /merged/i + is_expected.to have_body_text /merged/i end it 'contains a link to the merge request' do - should have_body_text /#{project_merge_request_path project, merge_request}/ + is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/ end end end @@ -399,15 +399,15 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'has the correct subject' do - should have_subject /Project was moved/ + is_expected.to have_subject /Project was moved/ end it 'contains name of project' do - should have_body_text /#{project.name_with_namespace}/ + is_expected.to have_body_text /#{project.name_with_namespace}/ end it 'contains new user role' do - should have_body_text /#{project.ssh_url_to_repo}/ + is_expected.to have_body_text /#{project.ssh_url_to_repo}/ end end @@ -422,13 +422,13 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'has the correct subject' do - should have_subject /Access to project was granted/ + is_expected.to have_subject /Access to project was granted/ end it 'contains name of project' do - should have_body_text /#{project.name}/ + is_expected.to have_body_text /#{project.name}/ end it 'contains new user role' do - should have_body_text /#{project_member.human_access}/ + is_expected.to have_body_text /#{project_member.human_access}/ end end @@ -437,29 +437,29 @@ describe Notify do let(:note) { create(:note, project: project, author: note_author) } before :each do - Note.stub(:find).with(note.id).and_return(note) + allow(Note).to receive(:find).with(note.id).and_return(note) end shared_examples 'a note email' do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(note_author.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(note_author.name) + expect(sender.address).to eq(gitlab_sender) end it 'is sent to the given recipient' do - should deliver_to recipient.notification_email + is_expected.to deliver_to recipient.notification_email end it 'contains the message from the note' do - should have_body_text /#{note.note}/ + is_expected.to have_body_text /#{note.note}/ end end describe 'on a commit' do let(:commit) { project.repository.commit } - before(:each) { note.stub(:noteable).and_return(commit) } + before(:each) { allow(note).to receive(:noteable).and_return(commit) } subject { Notify.note_commit_email(recipient.id, note.id) } @@ -467,18 +467,18 @@ describe Notify do it_behaves_like 'an answer to an existing thread', 'commits' it 'has the correct subject' do - should have_subject /#{commit.title} \(#{commit.short_id}\)/ + is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ end it 'contains a link to the commit' do - should have_body_text commit.short_id + is_expected.to have_body_text commit.short_id end end describe 'on a merge request' do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:note_on_merge_request_path) { project_merge_request_path(project, merge_request, anchor: "note_#{note.id}") } - before(:each) { note.stub(:noteable).and_return(merge_request) } + before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } subject { Notify.note_merge_request_email(recipient.id, note.id) } @@ -486,18 +486,18 @@ describe Notify do it_behaves_like 'an answer to an existing thread', 'merge_request' it 'has the correct subject' do - should have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ end it 'contains a link to the merge request note' do - should have_body_text /#{note_on_merge_request_path}/ + is_expected.to have_body_text /#{note_on_merge_request_path}/ end end describe 'on an issue' do let(:issue) { create(:issue, project: project) } let(:note_on_issue_path) { project_issue_path(project, issue, anchor: "note_#{note.id}") } - before(:each) { note.stub(:noteable).and_return(issue) } + before(:each) { allow(note).to receive(:noteable).and_return(issue) } subject { Notify.note_issue_email(recipient.id, note.id) } @@ -505,11 +505,11 @@ describe Notify do it_behaves_like 'an answer to an existing thread', 'issue' it 'has the correct subject' do - should have_subject /#{issue.title} \(##{issue.iid}\)/ + is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ end it 'contains a link to the issue note' do - should have_body_text /#{note_on_issue_path}/ + is_expected.to have_body_text /#{note_on_issue_path}/ end end end @@ -525,15 +525,15 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'has the correct subject' do - should have_subject /Access to group was granted/ + is_expected.to have_subject /Access to group was granted/ end it 'contains name of project' do - should have_body_text /#{group.name}/ + is_expected.to have_body_text /#{group.name}/ end it 'contains new user role' do - should have_body_text /#{membership.human_access}/ + is_expected.to have_body_text /#{membership.human_access}/ end end @@ -551,15 +551,15 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it 'is sent to the new user' do - should deliver_to 'new-email@mail.com' + is_expected.to deliver_to 'new-email@mail.com' end it 'has the correct subject' do - should have_subject "Confirmation instructions" + is_expected.to have_subject "Confirmation instructions" end it 'includes a link to the site' do - should have_body_text /#{example_site_path}/ + is_expected.to have_body_text /#{example_site_path}/ end end @@ -574,28 +574,28 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(user.name) + expect(sender.address).to eq(gitlab_sender) end it 'is sent to recipient' do - should deliver_to 'devs@company.name' + is_expected.to deliver_to 'devs@company.name' end it 'has the correct subject' do - should have_subject /#{commits.length} new commits pushed to repository/ + is_expected.to have_subject /#{commits.length} new commits pushed to repository/ end it 'includes commits list' do - should have_body_text /Change some files/ + is_expected.to have_body_text /Change some files/ end it 'includes diffs' do - should have_body_text /def archive_formats_regex/ + is_expected.to have_body_text /def archive_formats_regex/ end it 'contains a link to the diff' do - should have_body_text /#{diff_path}/ + is_expected.to have_body_text /#{diff_path}/ end end @@ -610,28 +610,28 @@ describe Notify do it 'is sent as the author' do sender = subject.header[:from].addrs[0] - sender.display_name.should eq(user.name) - sender.address.should eq(gitlab_sender) + expect(sender.display_name).to eq(user.name) + expect(sender.address).to eq(gitlab_sender) end it 'is sent to recipient' do - should deliver_to 'devs@company.name' + is_expected.to deliver_to 'devs@company.name' end it 'has the correct subject' do - should have_subject /#{commits.first.title}/ + is_expected.to have_subject /#{commits.first.title}/ end it 'includes commits list' do - should have_body_text /Change some files/ + is_expected.to have_body_text /Change some files/ end it 'includes diffs' do - should have_body_text /def archive_formats_regex/ + is_expected.to have_body_text /def archive_formats_regex/ end it 'contains a link to the diff' do - should have_body_text /#{diff_path}/ + is_expected.to have_body_text /#{diff_path}/ end end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index cd6d03e6c1a..cb43fdb7fc7 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -17,5 +17,5 @@ require 'spec_helper' describe ApplicationSetting, models: true do - it { ApplicationSetting.create_from_defaults.should be_valid } + it { expect(ApplicationSetting.create_from_defaults).to be_valid } end diff --git a/spec/models/asana_service_spec.rb b/spec/models/asana_service_spec.rb index 6bebb76f8c7..83e39f87f33 100644 --- a/spec/models/asana_service_spec.rb +++ b/spec/models/asana_service_spec.rb @@ -16,8 +16,8 @@ require 'spec_helper' describe AsanaService, models: true do describe 'Associations' do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe 'Validations' do @@ -26,7 +26,7 @@ describe AsanaService, models: true do subject.active = true end - it { should validate_presence_of :api_key } + it { is_expected.to validate_presence_of :api_key } end end @@ -46,13 +46,13 @@ describe AsanaService, models: true do end it 'should call Asana service to created a story' do - Asana::Task.should_receive(:find).with('123456').once + expect(Asana::Task).to receive(:find).with('123456').once @asana.check_commit('related to #123456', 'pushed') end it 'should call Asana service to created a story and close a task' do - Asana::Task.should_receive(:find).with('456789').twice + expect(Asana::Task).to receive(:find).with('456789').twice @asana.check_commit('fix #456789', 'pushed') end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 0f31c407c90..8ab72151a69 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -18,22 +18,22 @@ require 'spec_helper' describe BroadcastMessage do subject { create(:broadcast_message) } - it { should be_valid } + it { is_expected.to be_valid } describe :current do it "should return last message if time match" do broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow) - BroadcastMessage.current.should == broadcast_message + expect(BroadcastMessage.current).to eq(broadcast_message) end it "should return nil if time not come" do broadcast_message = create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days) - BroadcastMessage.current.should be_nil + expect(BroadcastMessage.current).to be_nil end it "should return nil if time has passed" do broadcast_message = create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday) - BroadcastMessage.current.should be_nil + expect(BroadcastMessage.current).to be_nil end end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 7a2a7a4ce9b..8b3d88640da 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -6,22 +6,22 @@ describe Commit do describe '#title' do it "returns no_commit_message when safe_message is blank" do - commit.stub(:safe_message).and_return('') - commit.title.should == "--no commit message" + allow(commit).to receive(:safe_message).and_return('') + expect(commit.title).to eq("--no commit message") end it "truncates a message without a newline at 80 characters" do message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.' - commit.stub(:safe_message).and_return(message) - commit.title.should == "#{message[0..79]}…" + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.title).to eq("#{message[0..79]}…") end it "truncates a message with a newline before 80 characters at the newline" do message = commit.safe_message.split(" ").first - commit.stub(:safe_message).and_return(message + "\n" + message) - commit.title.should == message + allow(commit).to receive(:safe_message).and_return(message + "\n" + message) + expect(commit.title).to eq(message) end it "does not truncates a message with a newline after 80 but less 100 characters" do @@ -30,25 +30,25 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis Vivamus egestas lacinia lacus, sed rutrum mauris. eos - commit.stub(:safe_message).and_return(message) - commit.title.should == message.split("\n").first + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.title).to eq(message.split("\n").first) end end describe "delegation" do subject { commit } - it { should respond_to(:message) } - it { should respond_to(:authored_date) } - it { should respond_to(:committed_date) } - it { should respond_to(:committer_email) } - it { should respond_to(:author_email) } - it { should respond_to(:parents) } - it { should respond_to(:date) } - it { should respond_to(:diffs) } - it { should respond_to(:tree) } - it { should respond_to(:id) } - it { should respond_to(:to_patch) } + it { is_expected.to respond_to(:message) } + it { is_expected.to respond_to(:authored_date) } + it { is_expected.to respond_to(:committed_date) } + it { is_expected.to respond_to(:committer_email) } + it { is_expected.to respond_to(:author_email) } + it { is_expected.to respond_to(:parents) } + it { is_expected.to respond_to(:date) } + it { is_expected.to respond_to(:diffs) } + it { is_expected.to respond_to(:tree) } + it { is_expected.to respond_to(:id) } + it { is_expected.to respond_to(:to_patch) } end describe '#closes_issues' do @@ -58,13 +58,13 @@ eos it 'detects issues that this commit is marked as closing' do commit.stub(safe_message: "Fixes ##{issue.iid}") - commit.closes_issues(project).should == [issue] + expect(commit.closes_issues(project)).to eq([issue]) end it 'does not detect issues from other projects' do ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}" commit.stub(safe_message: "Fixes #{ext_ref}") - commit.closes_issues(project).should be_empty + expect(commit.closes_issues(project)).to be_empty end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 9cbc8990676..557c71b4d2c 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -4,63 +4,63 @@ describe Issue, "Issuable" do let(:issue) { create(:issue) } describe "Associations" do - it { should belong_to(:project) } - it { should belong_to(:author) } - it { should belong_to(:assignee) } - it { should have_many(:notes).dependent(:destroy) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:author) } + it { is_expected.to belong_to(:assignee) } + it { is_expected.to have_many(:notes).dependent(:destroy) } end describe "Validation" do before { subject.stub(set_iid: false) } - it { should validate_presence_of(:project) } - it { should validate_presence_of(:iid) } - it { should validate_presence_of(:author) } - it { should validate_presence_of(:title) } - it { should ensure_length_of(:title).is_at_least(0).is_at_most(255) } + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:iid) } + it { is_expected.to validate_presence_of(:author) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to ensure_length_of(:title).is_at_least(0).is_at_most(255) } end describe "Scope" do - it { described_class.should respond_to(:opened) } - it { described_class.should respond_to(:closed) } - it { described_class.should respond_to(:assigned) } + it { expect(described_class).to respond_to(:opened) } + it { expect(described_class).to respond_to(:closed) } + it { expect(described_class).to respond_to(:assigned) } end describe ".search" do let!(:searchable_issue) { create(:issue, title: "Searchable issue") } it "matches by title" do - described_class.search('able').should == [searchable_issue] + expect(described_class.search('able')).to eq([searchable_issue]) end end describe "#today?" do it "returns true when created today" do # Avoid timezone differences and just return exactly what we want - Date.stub(:today).and_return(issue.created_at.to_date) - issue.today?.should be_true + allow(Date).to receive(:today).and_return(issue.created_at.to_date) + expect(issue.today?).to be_truthy end it "returns false when not created today" do - Date.stub(:today).and_return(Date.yesterday) - issue.today?.should be_false + allow(Date).to receive(:today).and_return(Date.yesterday) + expect(issue.today?).to be_falsey end end describe "#new?" do it "returns true when created today and record hasn't been updated" do - issue.stub(:today?).and_return(true) - issue.new?.should be_true + allow(issue).to receive(:today?).and_return(true) + expect(issue.new?).to be_truthy end it "returns false when not created today" do - issue.stub(:today?).and_return(false) - issue.new?.should be_false + allow(issue).to receive(:today?).and_return(false) + expect(issue.new?).to be_falsey end it "returns false when record has been updated" do - issue.stub(:today?).and_return(true) + allow(issue).to receive(:today?).and_return(true) issue.touch - issue.new?.should be_false + expect(issue.new?).to be_falsey end end end diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index ca6f11b2a4d..eadb941a3fa 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -8,7 +8,7 @@ describe Issue, "Mentionable" do subject { issue.mentioned_users } - it { should include(user) } - it { should_not include(user2) } + it { is_expected.to include(user) } + it { is_expected.not_to include(user2) } end end diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb index adbbbac875f..b32be8d7a7c 100644 --- a/spec/models/deploy_key_spec.rb +++ b/spec/models/deploy_key_spec.rb @@ -19,7 +19,7 @@ describe DeployKey do let(:deploy_key) { create(:deploy_key, projects: [project]) } describe "Associations" do - it { should have_many(:deploy_keys_projects) } - it { should have_many(:projects) } + it { is_expected.to have_many(:deploy_keys_projects) } + it { is_expected.to have_many(:projects) } end end diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index 3e0e25ee39a..aacd9bf38bf 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -13,12 +13,12 @@ require 'spec_helper' describe DeployKeysProject do describe "Associations" do - it { should belong_to(:deploy_key) } - it { should belong_to(:project) } + it { is_expected.to belong_to(:deploy_key) } + it { is_expected.to belong_to(:project) } end describe "Validation" do - it { should validate_presence_of(:project_id) } - it { should validate_presence_of(:deploy_key_id) } + it { is_expected.to validate_presence_of(:project_id) } + it { is_expected.to validate_presence_of(:deploy_key_id) } end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 204ae9da704..0f32f162a10 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -18,16 +18,16 @@ require 'spec_helper' describe Event do describe "Associations" do - it { should belong_to(:project) } - it { should belong_to(:target) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:target) } end describe "Respond to" do - it { should respond_to(:author_name) } - it { should respond_to(:author_email) } - it { should respond_to(:issue_title) } - it { should respond_to(:merge_request_title) } - it { should respond_to(:commits) } + it { is_expected.to respond_to(:author_name) } + it { is_expected.to respond_to(:author_email) } + it { is_expected.to respond_to(:issue_title) } + it { is_expected.to respond_to(:merge_request_title) } + it { is_expected.to respond_to(:commits) } end describe "Push event" do @@ -58,10 +58,10 @@ describe Event do ) end - it { @event.push?.should be_true } - it { @event.proper?.should be_true } - it { @event.tag?.should be_false } - it { @event.branch_name.should == "master" } - it { @event.author.should == @user } + it { expect(@event.push?).to be_truthy } + it { expect(@event.proper?).to be_truthy } + it { expect(@event.tag?).to be_falsey } + it { expect(@event.branch_name).to eq("master") } + it { expect(@event.author).to eq(@user) } end end diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb index 1845c6103f5..7d0ad44a92c 100644 --- a/spec/models/forked_project_link_spec.rb +++ b/spec/models/forked_project_link_spec.rb @@ -21,11 +21,11 @@ describe ForkedProjectLink, "add link on fork" do end it "project_to should know it is forked" do - @project_to.forked?.should be_true + expect(@project_to.forked?).to be_truthy end it "project should know who it is forked from" do - @project_to.forked_from_project.should == project_from + expect(@project_to.forked_from_project).to eq(project_from) end end @@ -43,15 +43,15 @@ describe :forked_from_project do it "project_to should know it is forked" do - project_to.forked?.should be_true + expect(project_to.forked?).to be_truthy end it "project_from should not be forked" do - project_from.forked?.should be_false + expect(project_from.forked?).to be_falsey end it "project_to.destroy should destroy fork_link" do - forked_project_link.should_receive(:destroy) + expect(forked_project_link).to receive(:destroy) project_to.destroy end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 1d4ba8a2b85..9428224a64f 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -19,29 +19,29 @@ describe Group do let!(:group) { create(:group) } describe "Associations" do - it { should have_many :projects } - it { should have_many :group_members } + it { is_expected.to have_many :projects } + it { is_expected.to have_many :group_members } end - it { should validate_presence_of :name } - it { should validate_uniqueness_of(:name) } - it { should validate_presence_of :path } - it { should validate_uniqueness_of(:path) } - it { should_not validate_presence_of :owner } + it { is_expected.to validate_presence_of :name } + it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_presence_of :path } + it { is_expected.to validate_uniqueness_of(:path) } + it { is_expected.not_to validate_presence_of :owner } describe :users do - it { group.users.should == group.owners } + it { expect(group.users).to eq(group.owners) } end describe :human_name do - it { group.human_name.should == group.name } + it { expect(group.human_name).to eq(group.name) } end describe :add_users do let(:user) { create(:user) } before { group.add_user(user, GroupMember::MASTER) } - it { group.group_members.masters.map(&:user).should include(user) } + it { expect(group.group_members.masters.map(&:user)).to include(user) } end describe :add_users do @@ -49,10 +49,10 @@ describe Group do before { group.add_users([user.id], GroupMember::GUEST) } it "should update the group permission" do - group.group_members.guests.map(&:user).should include(user) + expect(group.group_members.guests.map(&:user)).to include(user) group.add_users([user.id], GroupMember::DEVELOPER) - group.group_members.developers.map(&:user).should include(user) - group.group_members.guests.map(&:user).should_not include(user) + expect(group.group_members.developers.map(&:user)).to include(user) + expect(group.group_members.guests.map(&:user)).not_to include(user) end end @@ -62,12 +62,12 @@ describe Group do it "should be true if avatar is image" do group.update_attribute(:avatar, 'uploads/avatar.png') - group.avatar_type.should be_true + expect(group.avatar_type).to be_truthy end it "should be false if avatar is html page" do group.update_attribute(:avatar, 'uploads/avatar.html') - group.avatar_type.should == ["only images allowed"] + expect(group.avatar_type).to eq(["only images allowed"]) end end end diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 6ec82438dfe..96bf74d45da 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -19,6 +19,6 @@ require "spec_helper" describe ServiceHook do describe "Associations" do - it { should belong_to :service } + it { is_expected.to belong_to :service } end end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 8deb732de9c..810b311a40b 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -26,32 +26,32 @@ describe SystemHook do it "project_create hook" do Projects::CreateService.new(create(:user), name: 'empty').execute - WebMock.should have_requested(:post, @system_hook.url).with(body: /project_create/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /project_create/).once end it "project_destroy hook" do user = create(:user) project = create(:empty_project, namespace: user.namespace) Projects::DestroyService.new(project, user, {}).execute - WebMock.should have_requested(:post, @system_hook.url).with(body: /project_destroy/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /project_destroy/).once end it "user_create hook" do create(:user) - WebMock.should have_requested(:post, @system_hook.url).with(body: /user_create/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /user_create/).once end it "user_destroy hook" do user = create(:user) user.destroy - WebMock.should have_requested(:post, @system_hook.url).with(body: /user_destroy/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /user_destroy/).once end it "project_create hook" do user = create(:user) project = create(:project) project.team << [user, :master] - WebMock.should have_requested(:post, @system_hook.url).with(body: /user_add_to_team/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /user_add_to_team/).once end it "project_destroy hook" do @@ -59,12 +59,12 @@ describe SystemHook do project = create(:project) project.team << [user, :master] project.project_members.destroy_all - WebMock.should have_requested(:post, @system_hook.url).with(body: /user_remove_from_team/).once + expect(WebMock).to have_requested(:post, @system_hook.url).with(body: /user_remove_from_team/).once end it 'group create hook' do create(:group) - WebMock.should have_requested(:post, @system_hook.url).with( + expect(WebMock).to have_requested(:post, @system_hook.url).with( body: /group_create/ ).once end @@ -72,7 +72,7 @@ describe SystemHook do it 'group destroy hook' do group = create(:group) group.destroy - WebMock.should have_requested(:post, @system_hook.url).with( + expect(WebMock).to have_requested(:post, @system_hook.url).with( body: /group_destroy/ ).once end @@ -81,7 +81,7 @@ describe SystemHook do group = create(:group) user = create(:user) group.add_user(user, Gitlab::Access::MASTER) - WebMock.should have_requested(:post, @system_hook.url).with( + expect(WebMock).to have_requested(:post, @system_hook.url).with( body: /user_add_to_group/ ).once end @@ -91,7 +91,7 @@ describe SystemHook do user = create(:user) group.add_user(user, Gitlab::Access::MASTER) group.group_members.destroy_all - WebMock.should have_requested(:post, @system_hook.url).with( + expect(WebMock).to have_requested(:post, @system_hook.url).with( body: /user_remove_from_group/ ).once end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index e9c04ee89cb..67ec9193ad7 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -19,25 +19,25 @@ require 'spec_helper' describe ProjectHook do describe "Associations" do - it { should belong_to :project } + it { is_expected.to belong_to :project } end describe "Mass assignment" do end describe "Validations" do - it { should validate_presence_of(:url) } + it { is_expected.to validate_presence_of(:url) } context "url format" do - it { should allow_value("http://example.com").for(:url) } - it { should allow_value("https://excample.com").for(:url) } - it { should allow_value("http://test.com/api").for(:url) } - it { should allow_value("http://test.com/api?key=abc").for(:url) } - it { should allow_value("http://test.com/api?key=abc&type=def").for(:url) } + it { is_expected.to allow_value("http://example.com").for(:url) } + it { is_expected.to allow_value("https://excample.com").for(:url) } + it { is_expected.to allow_value("http://test.com/api").for(:url) } + it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) } + it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) } - it { should_not allow_value("example.com").for(:url) } - it { should_not allow_value("ftp://example.com").for(:url) } - it { should_not allow_value("herp-and-derp").for(:url) } + it { is_expected.not_to allow_value("example.com").for(:url) } + it { is_expected.not_to allow_value("ftp://example.com").for(:url) } + it { is_expected.not_to allow_value("herp-and-derp").for(:url) } end end @@ -53,22 +53,22 @@ describe ProjectHook do it "POSTs to the web hook URL" do @project_hook.execute(@data) - WebMock.should have_requested(:post, @project_hook.url).once + expect(WebMock).to have_requested(:post, @project_hook.url).once end it "POSTs the data as JSON" do json = @data.to_json @project_hook.execute(@data) - WebMock.should have_requested(:post, @project_hook.url).with(body: json).once + expect(WebMock).to have_requested(:post, @project_hook.url).with(body: json).once end it "catches exceptions" do - WebHook.should_receive(:post).and_raise("Some HTTP Post error") + expect(WebHook).to receive(:post).and_raise("Some HTTP Post error") - lambda { + expect { @project_hook.execute(@data) - }.should raise_error + }.to raise_error end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 6b6efe832e5..087e40c3d84 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -21,14 +21,14 @@ require 'spec_helper' describe Issue do describe "Associations" do - it { should belong_to(:milestone) } + it { is_expected.to belong_to(:milestone) } end describe "Mass assignment" do end describe 'modules' do - it { should include_module(Issuable) } + it { is_expected.to include_module(Issuable) } end subject { create(:issue) } @@ -36,10 +36,10 @@ describe Issue do describe '#is_being_reassigned?' do it 'returns true if the issue assignee has changed' do subject.assignee = create(:user) - subject.is_being_reassigned?.should be_true + expect(subject.is_being_reassigned?).to be_truthy end it 'returns false if the issue assignee has not changed' do - subject.is_being_reassigned?.should be_false + expect(subject.is_being_reassigned?).to be_falsey end end @@ -51,7 +51,7 @@ describe Issue do issue = create :issue, assignee: user end - Issue.open_for(user).count.should eq 2 + expect(Issue.open_for(user).count).to eq 2 end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 95c0aed0ffe..a212b95a7d6 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -16,67 +16,67 @@ require 'spec_helper' describe Key do describe "Associations" do - it { should belong_to(:user) } + it { is_expected.to belong_to(:user) } end describe "Mass assignment" do end describe "Validation" do - it { should validate_presence_of(:title) } - it { should validate_presence_of(:key) } - it { should ensure_length_of(:title).is_within(0..255) } - it { should ensure_length_of(:key).is_within(0..5000) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_presence_of(:key) } + it { is_expected.to ensure_length_of(:title).is_within(0..255) } + it { is_expected.to ensure_length_of(:key).is_within(0..5000) } end describe "Methods" do - it { should respond_to :projects } + it { is_expected.to respond_to :projects } end context "validation of uniqueness" do let(:user) { create(:user) } it "accepts the key once" do - build(:key, user: user).should be_valid + expect(build(:key, user: user)).to be_valid end it "does not accept the exact same key twice" do create(:key, user: user) - build(:key, user: user).should_not be_valid + expect(build(:key, user: user)).not_to be_valid end it "does not accept a duplicate key with a different comment" do create(:key, user: user) duplicate = build(:key, user: user) duplicate.key << ' extra comment' - duplicate.should_not be_valid + expect(duplicate).not_to be_valid end end context "validate it is a fingerprintable key" do it "accepts the fingerprintable key" do - build(:key).should be_valid + expect(build(:key)).to be_valid end it "rejects the unfingerprintable key (contains space in middle)" do - build(:key_with_a_space_in_the_middle).should_not be_valid + expect(build(:key_with_a_space_in_the_middle)).not_to be_valid end it "rejects the unfingerprintable key (not a key)" do - build(:invalid_key).should_not be_valid + expect(build(:invalid_key)).not_to be_valid end end context 'callbacks' do it 'should add new key to authorized_file' do @key = build(:personal_key, id: 7) - GitlabShellWorker.should_receive(:perform_async).with(:add_key, @key.shell_id, @key.key) + expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, @key.shell_id, @key.key) @key.save end it 'should remove key from authorized_file' do @key = create(:personal_key) - GitlabShellWorker.should_receive(:perform_async).with(:remove_key, @key.shell_id, @key.key) + expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, @key.shell_id, @key.key) @key.destroy end end diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb index 0db60432ad3..8c240826582 100644 --- a/spec/models/label_link_spec.rb +++ b/spec/models/label_link_spec.rb @@ -14,8 +14,8 @@ require 'spec_helper' describe LabelLink do let(:label) { create(:label_link) } - it { label.should be_valid } + it { expect(label).to be_valid } - it { should belong_to(:label) } - it { should belong_to(:target) } + it { is_expected.to belong_to(:label) } + it { is_expected.to belong_to(:target) } end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 31634648f04..8644ac46605 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -14,30 +14,30 @@ require 'spec_helper' describe Label do let(:label) { create(:label) } - it { label.should be_valid } + it { expect(label).to be_valid } - it { should belong_to(:project) } + it { is_expected.to belong_to(:project) } describe 'Validation' do it 'should validate color code' do - build(:label, color: 'G-ITLAB').should_not be_valid - build(:label, color: 'AABBCC').should_not be_valid - build(:label, color: '#AABBCCEE').should_not be_valid - build(:label, color: '#GGHHII').should_not be_valid - build(:label, color: '#').should_not be_valid - build(:label, color: '').should_not be_valid + expect(build(:label, color: 'G-ITLAB')).not_to be_valid + expect(build(:label, color: 'AABBCC')).not_to be_valid + expect(build(:label, color: '#AABBCCEE')).not_to be_valid + expect(build(:label, color: '#GGHHII')).not_to be_valid + expect(build(:label, color: '#')).not_to be_valid + expect(build(:label, color: '')).not_to be_valid - build(:label, color: '#AABBCC').should be_valid + expect(build(:label, color: '#AABBCC')).to be_valid end it 'should validate title' do - build(:label, title: 'G,ITLAB').should_not be_valid - build(:label, title: 'G?ITLAB').should_not be_valid - build(:label, title: 'G&ITLAB').should_not be_valid - build(:label, title: '').should_not be_valid + expect(build(:label, title: 'G,ITLAB')).not_to be_valid + expect(build(:label, title: 'G?ITLAB')).not_to be_valid + expect(build(:label, title: 'G&ITLAB')).not_to be_valid + expect(build(:label, title: '')).not_to be_valid - build(:label, title: 'GITLAB').should be_valid - build(:label, title: 'gitlab').should be_valid + expect(build(:label, title: 'GITLAB')).to be_valid + expect(build(:label, title: 'gitlab')).to be_valid end end end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 38657de6793..e04f1741b24 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -21,7 +21,7 @@ describe GroupMember do it "should send email to user" do membership = build(:group_member) membership.stub(notification_service: double('NotificationService').as_null_object) - membership.should_receive(:notification_service) + expect(membership).to receive(:notification_service) membership.save end end @@ -33,12 +33,12 @@ describe GroupMember do end it "should send email to user" do - @membership.should_receive(:notification_service) + expect(@membership).to receive(:notification_service) @membership.update_attribute(:access_level, GroupMember::MASTER) end it "does not send an email when the access level has not changed" do - @membership.should_not_receive(:notification_service) + expect(@membership).not_to receive(:notification_service) @membership.update_attribute(:access_level, GroupMember::OWNER) end end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 9b5f89b6d7d..521721f3577 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -33,19 +33,19 @@ describe ProjectMember do @status = @project_2.team.import(@project_1) end - it { @status.should be_true } + it { expect(@status).to be_truthy } describe 'project 2 should get user 1 as developer. user_2 should not be changed' do - it { @project_2.users.should include(@user_1) } - it { @project_2.users.should include(@user_2) } + it { expect(@project_2.users).to include(@user_1) } + it { expect(@project_2.users).to include(@user_2) } - it { @abilities.allowed?(@user_1, :write_project, @project_2).should be_true } - it { @abilities.allowed?(@user_2, :read_project, @project_2).should be_true } + it { expect(@abilities.allowed?(@user_1, :write_project, @project_2)).to be_truthy } + it { expect(@abilities.allowed?(@user_2, :read_project, @project_2)).to be_truthy } end describe 'project 1 should not be changed' do - it { @project_1.users.should include(@user_1) } - it { @project_1.users.should_not include(@user_2) } + it { expect(@project_1.users).to include(@user_1) } + it { expect(@project_1.users).not_to include(@user_2) } end end @@ -64,12 +64,12 @@ describe ProjectMember do ) end - it { @project_1.users.should include(@user_1) } - it { @project_1.users.should include(@user_2) } + it { expect(@project_1.users).to include(@user_1) } + it { expect(@project_1.users).to include(@user_2) } - it { @project_2.users.should include(@user_1) } - it { @project_2.users.should include(@user_2) } + it { expect(@project_2.users).to include(@user_1) } + it { expect(@project_2.users).to include(@user_2) } end describe :truncate_teams do @@ -86,7 +86,7 @@ describe ProjectMember do ProjectMember.truncate_teams([@project_1.id, @project_2.id]) end - it { @project_1.users.should be_empty } - it { @project_2.users.should be_empty } + it { expect(@project_1.users).to be_empty } + it { expect(@project_2.users).to be_empty } end end diff --git a/spec/models/members_spec.rb b/spec/models/members_spec.rb index cea653ec285..dfd3f7feb6b 100644 --- a/spec/models/members_spec.rb +++ b/spec/models/members_spec.rb @@ -2,19 +2,19 @@ require 'spec_helper' describe Member do describe "Associations" do - it { should belong_to(:user) } + it { is_expected.to belong_to(:user) } end describe "Validation" do subject { Member.new(access_level: Member::GUEST) } - it { should validate_presence_of(:user) } - it { should validate_presence_of(:source) } - it { should validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } end describe "Delegate methods" do - it { should respond_to(:user_name) } - it { should respond_to(:user_email) } + it { is_expected.to respond_to(:user_name) } + it { is_expected.to respond_to(:user_email) } end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 9585cf09768..d40503d791c 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -25,35 +25,35 @@ require 'spec_helper' describe MergeRequest do describe "Validation" do - it { should validate_presence_of(:target_branch) } - it { should validate_presence_of(:source_branch) } + it { is_expected.to validate_presence_of(:target_branch) } + it { is_expected.to validate_presence_of(:source_branch) } end describe "Mass assignment" do end describe "Respond to" do - it { should respond_to(:unchecked?) } - it { should respond_to(:can_be_merged?) } - it { should respond_to(:cannot_be_merged?) } + it { is_expected.to respond_to(:unchecked?) } + it { is_expected.to respond_to(:can_be_merged?) } + it { is_expected.to respond_to(:cannot_be_merged?) } end describe 'modules' do - it { should include_module(Issuable) } + it { is_expected.to include_module(Issuable) } end describe "#mr_and_commit_notes" do let!(:merge_request) { create(:merge_request) } before do - merge_request.stub(:commits) { [merge_request.source_project.repository.commit] } + allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] } create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.project) create(:note, noteable: merge_request, project: merge_request.project) end it "should include notes for commits" do - merge_request.commits.should_not be_empty - merge_request.mr_and_commit_notes.count.should == 2 + expect(merge_request.commits).not_to be_empty + expect(merge_request.mr_and_commit_notes.count).to eq(2) end end @@ -62,10 +62,10 @@ describe MergeRequest do describe '#is_being_reassigned?' do it 'returns true if the merge_request assignee has changed' do subject.assignee = create(:user) - subject.is_being_reassigned?.should be_true + expect(subject.is_being_reassigned?).to be_truthy end it 'returns false if the merge request assignee has not changed' do - subject.is_being_reassigned?.should be_false + expect(subject.is_being_reassigned?).to be_falsey end end @@ -74,11 +74,11 @@ describe MergeRequest do subject.source_project = create(:project, namespace: create(:group)) subject.target_project = create(:project, namespace: create(:group)) - subject.for_fork?.should be_true + expect(subject.for_fork?).to be_truthy end it 'returns false if is not for a fork' do - subject.for_fork?.should be_false + expect(subject.for_fork?).to be_falsey end end @@ -96,14 +96,14 @@ describe MergeRequest do it 'accesses the set of issues that will be closed on acceptance' do subject.project.stub(default_branch: subject.target_branch) - subject.closes_issues.should == [issue0, issue1].sort_by(&:id) + expect(subject.closes_issues).to eq([issue0, issue1].sort_by(&:id)) end it 'only lists issues as to be closed if it targets the default branch' do subject.project.stub(default_branch: 'master') subject.target_branch = 'something-else' - subject.closes_issues.should be_empty + expect(subject.closes_issues).to be_empty end it 'detects issues mentioned in the description' do @@ -111,7 +111,7 @@ describe MergeRequest do subject.description = "Closes ##{issue2.iid}" subject.project.stub(default_branch: subject.target_branch) - subject.closes_issues.should include(issue2) + expect(subject.closes_issues).to include(issue2) end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index a3071c3251a..45171e1bf64 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -17,8 +17,8 @@ require 'spec_helper' describe Milestone do describe "Associations" do - it { should belong_to(:project) } - it { should have_many(:issues) } + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:issues) } end describe "Mass assignment" do @@ -26,8 +26,8 @@ describe Milestone do describe "Validation" do before { subject.stub(set_iid: false) } - it { should validate_presence_of(:title) } - it { should validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_presence_of(:project) } end let(:milestone) { create(:milestone) } @@ -36,30 +36,30 @@ describe Milestone do describe "#percent_complete" do it "should not count open issues" do milestone.issues << issue - milestone.percent_complete.should == 0 + expect(milestone.percent_complete).to eq(0) end it "should count closed issues" do issue.close milestone.issues << issue - milestone.percent_complete.should == 100 + expect(milestone.percent_complete).to eq(100) end it "should recover from dividing by zero" do - milestone.issues.should_receive(:count).and_return(0) - milestone.percent_complete.should == 100 + expect(milestone.issues).to receive(:count).and_return(0) + expect(milestone.percent_complete).to eq(100) end end describe "#expires_at" do it "should be nil when due_date is unset" do milestone.update_attributes(due_date: nil) - milestone.expires_at.should be_nil + expect(milestone.expires_at).to be_nil end it "should not be nil when due_date is set" do milestone.update_attributes(due_date: Date.tomorrow) - milestone.expires_at.should be_present + expect(milestone.expires_at).to be_present end end @@ -69,7 +69,7 @@ describe Milestone do milestone.stub(due_date: Date.today.prev_year) end - it { milestone.expired?.should be_true } + it { expect(milestone.expired?).to be_truthy } end context "not expired" do @@ -77,7 +77,7 @@ describe Milestone do milestone.stub(due_date: Date.today.next_year) end - it { milestone.expired?.should be_false } + it { expect(milestone.expired?).to be_falsey } end end @@ -89,7 +89,7 @@ describe Milestone do ) end - it { milestone.percent_complete.should == 75 } + it { expect(milestone.percent_complete).to eq(75) } end describe :items_count do @@ -99,14 +99,14 @@ describe Milestone do milestone.merge_requests << create(:merge_request) end - it { milestone.closed_items_count.should == 1 } - it { milestone.open_items_count.should == 2 } - it { milestone.total_items_count.should == 3 } - it { milestone.is_empty?.should be_false } + it { expect(milestone.closed_items_count).to eq(1) } + it { expect(milestone.open_items_count).to eq(2) } + it { expect(milestone.total_items_count).to eq(3) } + it { expect(milestone.is_empty?).to be_falsey } end describe :can_be_closed? do - it { milestone.can_be_closed?.should be_true } + it { expect(milestone.can_be_closed?).to be_truthy } end describe :is_empty? do @@ -116,7 +116,7 @@ describe Milestone do end it 'Should return total count of issues and merge requests assigned to milestone' do - milestone.total_items_count.should eq 2 + expect(milestone.total_items_count).to eq 2 end end @@ -129,14 +129,14 @@ describe Milestone do end it 'should be true if milestone active and all nested issues closed' do - milestone.can_be_closed?.should be_true + expect(milestone.can_be_closed?).to be_truthy end it 'should be false if milestone active and not all nested issues closed' do issue.milestone = milestone issue.save - milestone.can_be_closed?.should be_false + expect(milestone.can_be_closed?).to be_falsey end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 3562ebed1ff..4e268f8d8fa 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -18,29 +18,29 @@ require 'spec_helper' describe Namespace do let!(:namespace) { create(:namespace) } - it { should have_many :projects } - it { should validate_presence_of :name } - it { should validate_uniqueness_of(:name) } - it { should validate_presence_of :path } - it { should validate_uniqueness_of(:path) } - it { should validate_presence_of :owner } + it { is_expected.to have_many :projects } + it { is_expected.to validate_presence_of :name } + it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_presence_of :path } + it { is_expected.to validate_uniqueness_of(:path) } + it { is_expected.to validate_presence_of :owner } describe "Mass assignment" do end describe "Respond to" do - it { should respond_to(:human_name) } - it { should respond_to(:to_param) } + it { is_expected.to respond_to(:human_name) } + it { is_expected.to respond_to(:to_param) } end - it { Namespace.global_id.should == 'GLN' } + it { expect(Namespace.global_id).to eq('GLN') } describe :to_param do - it { namespace.to_param.should == namespace.path } + it { expect(namespace.to_param).to eq(namespace.path) } end describe :human_name do - it { namespace.human_name.should == namespace.owner_name } + it { expect(namespace.human_name).to eq(namespace.owner_name) } end describe :search do @@ -48,8 +48,8 @@ describe Namespace do @namespace = create :namespace end - it { Namespace.search(@namespace.path).should == [@namespace] } - it { Namespace.search('unknown').should == [] } + it { expect(Namespace.search(@namespace.path)).to eq([@namespace]) } + it { expect(Namespace.search('unknown')).to eq([]) } end describe :move_dir do @@ -66,13 +66,13 @@ describe Namespace do new_path = @namespace.path + "_new" @namespace.stub(path_was: @namespace.path) @namespace.stub(path: new_path) - @namespace.move_dir.should be_true + expect(@namespace.move_dir).to be_truthy end end describe :rm_dir do it "should remove dir" do - namespace.rm_dir.should be_true + expect(namespace.rm_dir).to be_truthy end end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 6ab7162c15c..17cb439c90e 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -21,17 +21,17 @@ require 'spec_helper' describe Note do describe "Associations" do - it { should belong_to(:project) } - it { should belong_to(:noteable) } - it { should belong_to(:author).class_name('User') } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:noteable) } + it { is_expected.to belong_to(:author).class_name('User') } end describe "Mass assignment" do end describe "Validation" do - it { should validate_presence_of(:note) } - it { should validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:note) } + it { is_expected.to validate_presence_of(:project) } end describe "Voting score" do @@ -39,44 +39,44 @@ describe Note do it "recognizes a neutral note" do note = create(:votable_note, note: "This is not a +1 note") - note.should_not be_upvote - note.should_not be_downvote + expect(note).not_to be_upvote + expect(note).not_to be_downvote end it "recognizes a neutral emoji note" do note = build(:votable_note, note: "I would :+1: this, but I don't want to") - note.should_not be_upvote - note.should_not be_downvote + expect(note).not_to be_upvote + expect(note).not_to be_downvote end it "recognizes a +1 note" do note = create(:votable_note, note: "+1 for this") - note.should be_upvote + expect(note).to be_upvote end it "recognizes a +1 emoji as a vote" do note = build(:votable_note, note: ":+1: for this") - note.should be_upvote + expect(note).to be_upvote end it "recognizes a thumbsup emoji as a vote" do note = build(:votable_note, note: ":thumbsup: for this") - note.should be_upvote + expect(note).to be_upvote end it "recognizes a -1 note" do note = create(:votable_note, note: "-1 for this") - note.should be_downvote + expect(note).to be_downvote end it "recognizes a -1 emoji as a vote" do note = build(:votable_note, note: ":-1: for this") - note.should be_downvote + expect(note).to be_downvote end it "recognizes a thumbsdown emoji as a vote" do note = build(:votable_note, note: ":thumbsdown: for this") - note.should be_downvote + expect(note).to be_downvote end end @@ -87,22 +87,22 @@ describe Note do let!(:commit) { note.noteable } it "should be accessible through #noteable" do - note.commit_id.should == commit.id - note.noteable.should be_a(Commit) - note.noteable.should == commit + expect(note.commit_id).to eq(commit.id) + expect(note.noteable).to be_a(Commit) + expect(note.noteable).to eq(commit) end it "should save a valid note" do - note.commit_id.should == commit.id + expect(note.commit_id).to eq(commit.id) note.noteable == commit end it "should be recognized by #for_commit?" do - note.should be_for_commit + expect(note).to be_for_commit end it "should not be votable" do - note.should_not be_votable + expect(note).not_to be_votable end end @@ -111,20 +111,20 @@ describe Note do let!(:commit) { note.noteable } it "should save a valid note" do - note.commit_id.should == commit.id - note.noteable.id.should == commit.id + expect(note.commit_id).to eq(commit.id) + expect(note.noteable.id).to eq(commit.id) end it "should be recognized by #for_diff_line?" do - note.should be_for_diff_line + expect(note).to be_for_diff_line end it "should be recognized by #for_commit_diff_line?" do - note.should be_for_commit_diff_line + expect(note).to be_for_commit_diff_line end it "should not be votable" do - note.should_not be_votable + expect(note).not_to be_votable end end @@ -132,7 +132,7 @@ describe Note do let!(:note) { create(:note_on_issue, note: "+1 from me") } it "should not be votable" do - note.should be_votable + expect(note).to be_votable end end @@ -140,7 +140,7 @@ describe Note do let!(:note) { create(:note_on_merge_request, note: "+1 from me") } it "should be votable" do - note.should be_votable + expect(note).to be_votable end end @@ -148,7 +148,7 @@ describe Note do let!(:note) { create(:note_on_merge_request_diff, note: "+1 from me") } it "should not be votable" do - note.should_not be_votable + expect(note).not_to be_votable end end @@ -161,20 +161,35 @@ describe Note do subject { Note.create_status_change_note(thing, project, author, status, nil) } it 'creates and saves a Note' do - should be_a Note - subject.id.should_not be_nil + is_expected.to be_a Note + expect(subject.id).not_to be_nil end - its(:noteable) { should == thing } - its(:project) { should == thing.project } - its(:author) { should == author } - its(:note) { should =~ /Status changed to #{status}/ } + describe '#noteable' do + subject { super().noteable } + it { is_expected.to eq(thing) } + end + + describe '#project' do + subject { super().project } + it { is_expected.to eq(thing.project) } + end + + describe '#author' do + subject { super().author } + it { is_expected.to eq(author) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to match(/Status changed to #{status}/) } + end it 'appends a back-reference if a closing mentionable is supplied' do commit = double('commit', gfm_reference: 'commit 123456') n = Note.create_status_change_note(thing, project, author, status, commit) - n.note.should =~ /Status changed to #{status} by commit 123456/ + expect(n.note).to match(/Status changed to #{status} by commit 123456/) end end @@ -187,19 +202,41 @@ describe Note do subject { Note.create_assignee_change_note(thing, project, author, assignee) } context 'creates and saves a Note' do - it { should be_a Note } - its(:id) { should_not be_nil } + it { is_expected.to be_a Note } + + describe '#id' do + subject { super().id } + it { is_expected.not_to be_nil } + end + end + + describe '#noteable' do + subject { super().noteable } + it { is_expected.to eq(thing) } end - its(:noteable) { should == thing } - its(:project) { should == thing.project } - its(:author) { should == author } - its(:note) { should =~ /Reassigned to @#{assignee.username}/ } + describe '#project' do + subject { super().project } + it { is_expected.to eq(thing.project) } + end + + describe '#author' do + subject { super().author } + it { is_expected.to eq(author) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to match(/Reassigned to @#{assignee.username}/) } + end context 'assignee is removed' do let(:assignee) { nil } - its(:note) { should =~ /Assignee removed/ } + describe '#note' do + subject { super().note } + it { is_expected.to match(/Assignee removed/) } + end end end @@ -216,64 +253,144 @@ describe Note do context 'issue from a merge request' do subject { Note.create_cross_reference_note(issue, mergereq, author, project) } - it { should be_valid } - its(:noteable) { should == issue } - its(:project) { should == issue.project } - its(:author) { should == author } - its(:note) { should == "_mentioned in merge request !#{mergereq.iid}_" } + it { is_expected.to be_valid } + + describe '#noteable' do + subject { super().noteable } + it { is_expected.to eq(issue) } + end + + describe '#project' do + subject { super().project } + it { is_expected.to eq(issue.project) } + end + + describe '#author' do + subject { super().author } + it { is_expected.to eq(author) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to eq("_mentioned in merge request !#{mergereq.iid}_") } + end end context 'issue from a commit' do subject { Note.create_cross_reference_note(issue, commit, author, project) } - it { should be_valid } - its(:noteable) { should == issue } - its(:note) { should == "_mentioned in commit #{commit.sha}_" } + it { is_expected.to be_valid } + + describe '#noteable' do + subject { super().noteable } + it { is_expected.to eq(issue) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to eq("_mentioned in commit #{commit.sha}_") } + end end context 'merge request from an issue' do subject { Note.create_cross_reference_note(mergereq, issue, author, project) } - it { should be_valid } - its(:noteable) { should == mergereq } - its(:project) { should == mergereq.project } - its(:note) { should == "_mentioned in issue ##{issue.iid}_" } + it { is_expected.to be_valid } + + describe '#noteable' do + subject { super().noteable } + it { is_expected.to eq(mergereq) } + end + + describe '#project' do + subject { super().project } + it { is_expected.to eq(mergereq.project) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to eq("_mentioned in issue ##{issue.iid}_") } + end end context 'commit from a merge request' do subject { Note.create_cross_reference_note(commit, mergereq, author, project) } - it { should be_valid } - its(:noteable) { should == commit } - its(:project) { should == project } - its(:note) { should == "_mentioned in merge request !#{mergereq.iid}_" } + it { is_expected.to be_valid } + + describe '#noteable' do + subject { super().noteable } + it { is_expected.to eq(commit) } + end + + describe '#project' do + subject { super().project } + it { is_expected.to eq(project) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to eq("_mentioned in merge request !#{mergereq.iid}_") } + end end context 'commit contained in a merge request' do subject { Note.create_cross_reference_note(mergereq.commits.first, mergereq, author, project) } - it { should be_nil } + it { is_expected.to be_nil } end context 'commit from issue' do subject { Note.create_cross_reference_note(commit, issue, author, project) } - it { should be_valid } - its(:noteable_type) { should == "Commit" } - its(:noteable_id) { should be_nil } - its(:commit_id) { should == commit.id } - its(:note) { should == "_mentioned in issue ##{issue.iid}_" } + it { is_expected.to be_valid } + + describe '#noteable_type' do + subject { super().noteable_type } + it { is_expected.to eq("Commit") } + end + + describe '#noteable_id' do + subject { super().noteable_id } + it { is_expected.to be_nil } + end + + describe '#commit_id' do + subject { super().commit_id } + it { is_expected.to eq(commit.id) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to eq("_mentioned in issue ##{issue.iid}_") } + end end context 'commit from commit' do let(:parent_commit) { commit.parents.first } subject { Note.create_cross_reference_note(commit, parent_commit, author, project) } - it { should be_valid } - its(:noteable_type) { should == "Commit" } - its(:noteable_id) { should be_nil } - its(:commit_id) { should == commit.id } - its(:note) { should == "_mentioned in commit #{parent_commit.id}_" } + it { is_expected.to be_valid } + + describe '#noteable_type' do + subject { super().noteable_type } + it { is_expected.to eq("Commit") } + end + + describe '#noteable_id' do + subject { super().noteable_id } + it { is_expected.to be_nil } + end + + describe '#commit_id' do + subject { super().commit_id } + it { is_expected.to eq(commit.id) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to eq("_mentioned in commit #{parent_commit.id}_") } + end end end @@ -289,11 +406,11 @@ describe Note do end it 'detects if a mentionable has already been mentioned' do - Note.cross_reference_exists?(issue, commit0).should be_true + expect(Note.cross_reference_exists?(issue, commit0)).to be_truthy end it 'detects if a mentionable has not already been mentioned' do - Note.cross_reference_exists?(issue, commit1).should be_false + expect(Note.cross_reference_exists?(issue, commit1)).to be_falsey end context 'commit on commit' do @@ -301,8 +418,8 @@ describe Note do Note.create_cross_reference_note(commit0, commit1, author, project) end - it { Note.cross_reference_exists?(commit0, commit1).should be_true } - it { Note.cross_reference_exists?(commit1, commit0).should be_false } + it { expect(Note.cross_reference_exists?(commit0, commit1)).to be_truthy } + it { expect(Note.cross_reference_exists?(commit1, commit0)).to be_falsey } end end @@ -315,22 +432,22 @@ describe Note do it 'should recognize user-supplied notes as non-system' do @note = create(:note_on_issue) - @note.should_not be_system + expect(@note).not_to be_system end it 'should identify status-change notes as system notes' do @note = Note.create_status_change_note(issue, project, author, 'closed', nil) - @note.should be_system + expect(@note).to be_system end it 'should identify cross-reference notes as system notes' do @note = Note.create_cross_reference_note(issue, other, author, project) - @note.should be_system + expect(@note).to be_system end it 'should identify assignee-change notes as system notes' do @note = Note.create_assignee_change_note(issue, project, author, assignee) - @note.should be_system + expect(@note).to be_system end end @@ -351,9 +468,9 @@ describe Note do @p2.project_members.create(user: @u3, access_level: ProjectMember::GUEST) end - it { @abilities.allowed?(@u1, :read_note, @p1).should be_false } - it { @abilities.allowed?(@u2, :read_note, @p1).should be_true } - it { @abilities.allowed?(@u3, :read_note, @p1).should be_false } + it { expect(@abilities.allowed?(@u1, :read_note, @p1)).to be_falsey } + it { expect(@abilities.allowed?(@u2, :read_note, @p1)).to be_truthy } + it { expect(@abilities.allowed?(@u3, :read_note, @p1)).to be_falsey } end describe :write do @@ -362,9 +479,9 @@ describe Note do @p2.project_members.create(user: @u3, access_level: ProjectMember::DEVELOPER) end - it { @abilities.allowed?(@u1, :write_note, @p1).should be_false } - it { @abilities.allowed?(@u2, :write_note, @p1).should be_true } - it { @abilities.allowed?(@u3, :write_note, @p1).should be_false } + it { expect(@abilities.allowed?(@u1, :write_note, @p1)).to be_falsey } + it { expect(@abilities.allowed?(@u2, :write_note, @p1)).to be_truthy } + it { expect(@abilities.allowed?(@u3, :write_note, @p1)).to be_falsey } end describe :admin do @@ -374,9 +491,9 @@ describe Note do @p2.project_members.create(user: @u3, access_level: ProjectMember::MASTER) end - it { @abilities.allowed?(@u1, :admin_note, @p1).should be_false } - it { @abilities.allowed?(@u2, :admin_note, @p1).should be_true } - it { @abilities.allowed?(@u3, :admin_note, @p1).should be_false } + it { expect(@abilities.allowed?(@u1, :admin_note, @p1)).to be_falsey } + it { expect(@abilities.allowed?(@u2, :admin_note, @p1)).to be_truthy } + it { expect(@abilities.allowed?(@u3, :admin_note, @p1)).to be_falsey } end end diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb index 5c8d1e7438b..1ee19003543 100644 --- a/spec/models/project_security_spec.rb +++ b/spec/models/project_security_spec.rb @@ -23,7 +23,7 @@ describe Project do describe "Non member rules" do it "should deny for non-project users any actions" do admin_actions.each do |action| - @abilities.allowed?(@u1, action, @p1).should be_false + expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey end end end @@ -35,7 +35,7 @@ describe Project do it "should allow for project user any guest actions" do guest_actions.each do |action| - @abilities.allowed?(@u2, action, @p1).should be_true + expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy end end end @@ -47,7 +47,7 @@ describe Project do it "should allow for project user any report actions" do report_actions.each do |action| - @abilities.allowed?(@u2, action, @p1).should be_true + expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy end end end @@ -60,13 +60,13 @@ describe Project do it "should deny for developer master-specific actions" do [dev_actions - report_actions].each do |action| - @abilities.allowed?(@u2, action, @p1).should be_false + expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey end end it "should allow for project user any dev actions" do dev_actions.each do |action| - @abilities.allowed?(@u3, action, @p1).should be_true + expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy end end end @@ -79,13 +79,13 @@ describe Project do it "should deny for developer master-specific actions" do [master_actions - dev_actions].each do |action| - @abilities.allowed?(@u2, action, @p1).should be_false + expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey end end it "should allow for project user any master actions" do master_actions.each do |action| - @abilities.allowed?(@u3, action, @p1).should be_true + expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy end end end @@ -98,13 +98,13 @@ describe Project do it "should deny for masters admin-specific actions" do [admin_actions - master_actions].each do |action| - @abilities.allowed?(@u2, action, @p1).should be_false + expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey end end it "should allow for project owner any admin actions" do admin_actions.each do |action| - @abilities.allowed?(@u4, action, @p1).should be_true + expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy end end end diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb index 005dd41fea9..ee7f780c8f6 100644 --- a/spec/models/project_services/assembla_service_spec.rb +++ b/spec/models/project_services/assembla_service_spec.rb @@ -16,8 +16,8 @@ require 'spec_helper' describe AssemblaService, models: true do describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe "Execute" do @@ -40,7 +40,7 @@ describe AssemblaService, models: true do it "should call Assembla API" do @assembla_service.execute(@sample_data) - WebMock.should have_requested(:post, @api_url).with( + expect(WebMock).to have_requested(:post, @api_url).with( body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/ ).once end diff --git a/spec/models/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildbox_service_spec.rb index 1d9ca51be16..050363e14c7 100644 --- a/spec/models/project_services/buildbox_service_spec.rb +++ b/spec/models/project_services/buildbox_service_spec.rb @@ -16,8 +16,8 @@ require 'spec_helper' describe BuildboxService do describe 'Associations' do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe 'commits methods' do @@ -38,35 +38,39 @@ describe BuildboxService do describe :webhook_url do it 'returns the webhook url' do - @service.webhook_url.should == + expect(@service.webhook_url).to eq( 'https://webhook.buildbox.io/deliver/secret-sauce-webhook-token' + ) end end describe :commit_status_path do it 'returns the correct status page' do - @service.commit_status_path('2ab7834c').should == + expect(@service.commit_status_path('2ab7834c')).to eq( 'https://gitlab.buildbox.io/status/secret-sauce-status-token.json?commit=2ab7834c' + ) end end describe :build_page do it 'returns the correct build page' do - @service.build_page('2ab7834c').should == + expect(@service.build_page('2ab7834c')).to eq( 'https://buildbox.io/account-name/example-project/builds?commit=2ab7834c' + ) end end describe :builds_page do it 'returns the correct path to the builds page' do - @service.builds_path.should == + expect(@service.builds_path).to eq( 'https://buildbox.io/account-name/example-project/builds?branch=default-brancho' + ) end end describe :status_img_path do it 'returns the correct path to the status image' do - @service.status_img_path.should == 'https://badge.buildbox.io/secret-sauce-status-token.svg' + expect(@service.status_img_path).to eq('https://badge.buildbox.io/secret-sauce-status-token.svg') end end end diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index ac156719b43..b34e36bc940 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -16,8 +16,8 @@ require 'spec_helper' describe FlowdockService do describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe "Execute" do @@ -39,7 +39,7 @@ describe FlowdockService do it "should call FlowDock API" do @flowdock_service.execute(@sample_data) - WebMock.should have_requested(:post, @api_url).with( + expect(WebMock).to have_requested(:post, @api_url).with( body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/ ).once end diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index 2c560c11dac..fe5d62b2f53 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -16,8 +16,8 @@ require 'spec_helper' describe GemnasiumService do describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe "Execute" do @@ -36,7 +36,7 @@ describe GemnasiumService do @sample_data = Gitlab::PushDataBuilder.build_sample(project, user) end it "should call Gemnasium service" do - Gemnasium::GitlabService.should_receive(:execute).with(an_instance_of(Hash)).once + expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once @gemnasium_service.execute(@sample_data) end end diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index 83277058fbb..0cd255f08ea 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -16,8 +16,8 @@ require 'spec_helper' describe GitlabCiService do describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe "Mass assignment" do @@ -34,11 +34,11 @@ describe GitlabCiService do end describe :commit_status_path do - it { @service.commit_status_path("2ab7834c").should == "http://ci.gitlab.org/projects/2/commits/2ab7834c/status.json?token=verySecret"} + it { expect(@service.commit_status_path("2ab7834c")).to eq("http://ci.gitlab.org/projects/2/commits/2ab7834c/status.json?token=verySecret")} end describe :build_page do - it { @service.build_page("2ab7834c").should == "http://ci.gitlab.org/projects/2/commits/2ab7834c"} + it { expect(@service.build_page("2ab7834c")).to eq("http://ci.gitlab.org/projects/2/commits/2ab7834c")} end end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 99ca04eff6e..6ef4d036c3f 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -16,8 +16,8 @@ require 'spec_helper' describe JiraService do describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe "Validations" do @@ -26,9 +26,9 @@ describe JiraService do subject.active = true end - it { should validate_presence_of :project_url } - it { should validate_presence_of :issues_url } - it { should validate_presence_of :new_issue_url } + it { is_expected.to validate_presence_of :project_url } + it { is_expected.to validate_presence_of :issues_url } + it { is_expected.to validate_presence_of :new_issue_url } end end @@ -79,7 +79,7 @@ describe JiraService do "new_issue_url" => "http://jira.sample/projects/project_a/issues/new" } } - Gitlab.config.stub(:issues_tracker).and_return(settings) + allow(Gitlab.config).to receive(:issues_tracker).and_return(settings) @service = project.create_jira_service(active: true) end diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb index f2813d66c7d..188626a7a27 100644 --- a/spec/models/project_services/pushover_service_spec.rb +++ b/spec/models/project_services/pushover_service_spec.rb @@ -16,8 +16,8 @@ require 'spec_helper' describe PushoverService do describe 'Associations' do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe 'Validations' do @@ -26,9 +26,9 @@ describe PushoverService do subject.active = true end - it { should validate_presence_of :api_key } - it { should validate_presence_of :user_key } - it { should validate_presence_of :priority } + it { is_expected.to validate_presence_of :api_key } + it { is_expected.to validate_presence_of :user_key } + it { is_expected.to validate_presence_of :priority } end end @@ -63,7 +63,7 @@ describe PushoverService do it 'should call Pushover API' do pushover.execute(sample_data) - WebMock.should have_requested(:post, api_url).once + expect(WebMock).to have_requested(:post, api_url).once end end end diff --git a/spec/models/project_services/slack_message_spec.rb b/spec/models/project_services/slack_message_spec.rb index c530fad619b..7197a94e53f 100644 --- a/spec/models/project_services/slack_message_spec.rb +++ b/spec/models/project_services/slack_message_spec.rb @@ -25,16 +25,17 @@ describe SlackMessage do end it 'returns a message regarding pushes' do - subject.pretext.should == + expect(subject.pretext).to eq( 'user_name pushed to branch of '\ ' ()' - subject.attachments.should == [ + ) + expect(subject.attachments).to eq([ { text: ": message1 - author1\n"\ ": message2 - author2", color: color, } - ] + ]) end end @@ -44,10 +45,11 @@ describe SlackMessage do end it 'returns a message regarding a new branch' do - subject.pretext.should == + expect(subject.pretext).to eq( 'user_name pushed new branch to '\ '' - subject.attachments.should be_empty + ) + expect(subject.attachments).to be_empty end end @@ -57,9 +59,10 @@ describe SlackMessage do end it 'returns a message regarding a removed branch' do - subject.pretext.should == + expect(subject.pretext).to eq( 'user_name removed branch master from ' - subject.attachments.should be_empty + ) + expect(subject.attachments).to be_empty end end end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 34594072409..90b385423f1 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -16,8 +16,8 @@ require 'spec_helper' describe SlackService do describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe "Validations" do @@ -26,7 +26,7 @@ describe SlackService do subject.active = true end - it { should validate_presence_of :webhook } + it { is_expected.to validate_presence_of :webhook } end end @@ -51,7 +51,7 @@ describe SlackService do it "should call Slack API" do slack.execute(sample_data) - WebMock.should have_requested(:post, webhook_url).once + expect(WebMock).to have_requested(:post, webhook_url).once end end end diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index a6e1d9eef50..3e8f106d27f 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -19,13 +19,13 @@ require 'spec_helper' describe ProjectSnippet do describe "Associations" do - it { should belong_to(:project) } + it { is_expected.to belong_to(:project) } end describe "Mass assignment" do end describe "Validation" do - it { should validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:project) } end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e2197420018..ad7a0f0a1e3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -33,25 +33,25 @@ require 'spec_helper' describe Project do describe 'Associations' do - it { should belong_to(:group) } - it { should belong_to(:namespace) } - it { should belong_to(:creator).class_name('User') } - it { should have_many(:users) } - it { should have_many(:events).dependent(:destroy) } - it { should have_many(:merge_requests).dependent(:destroy) } - it { should have_many(:issues).dependent(:destroy) } - it { should have_many(:milestones).dependent(:destroy) } - it { should have_many(:project_members).dependent(:destroy) } - it { should have_many(:notes).dependent(:destroy) } - it { should have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) } - it { should have_many(:deploy_keys_projects).dependent(:destroy) } - it { should have_many(:deploy_keys) } - it { should have_many(:hooks).dependent(:destroy) } - it { should have_many(:protected_branches).dependent(:destroy) } - it { should have_one(:forked_project_link).dependent(:destroy) } - it { should have_one(:slack_service).dependent(:destroy) } - it { should have_one(:pushover_service).dependent(:destroy) } - it { should have_one(:asana_service).dependent(:destroy) } + it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:namespace) } + it { is_expected.to belong_to(:creator).class_name('User') } + it { is_expected.to have_many(:users) } + it { is_expected.to have_many(:events).dependent(:destroy) } + it { is_expected.to have_many(:merge_requests).dependent(:destroy) } + it { is_expected.to have_many(:issues).dependent(:destroy) } + it { is_expected.to have_many(:milestones).dependent(:destroy) } + it { is_expected.to have_many(:project_members).dependent(:destroy) } + it { is_expected.to have_many(:notes).dependent(:destroy) } + it { is_expected.to have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) } + it { is_expected.to have_many(:deploy_keys_projects).dependent(:destroy) } + it { is_expected.to have_many(:deploy_keys) } + it { is_expected.to have_many(:hooks).dependent(:destroy) } + it { is_expected.to have_many(:protected_branches).dependent(:destroy) } + it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } + it { is_expected.to have_one(:slack_service).dependent(:destroy) } + it { is_expected.to have_one(:pushover_service).dependent(:destroy) } + it { is_expected.to have_one(:asana_service).dependent(:destroy) } end describe 'Mass assignment' do @@ -60,50 +60,50 @@ describe Project do describe 'Validation' do let!(:project) { create(:project) } - it { should validate_presence_of(:name) } - it { should validate_uniqueness_of(:name).scoped_to(:namespace_id) } - it { should ensure_length_of(:name).is_within(0..255) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) } + it { is_expected.to ensure_length_of(:name).is_within(0..255) } - it { should validate_presence_of(:path) } - it { should validate_uniqueness_of(:path).scoped_to(:namespace_id) } - it { should ensure_length_of(:path).is_within(0..255) } - it { should ensure_length_of(:description).is_within(0..2000) } - it { should validate_presence_of(:creator) } - it { should ensure_length_of(:issues_tracker_id).is_within(0..255) } - it { should validate_presence_of(:namespace) } + it { is_expected.to validate_presence_of(:path) } + it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) } + it { is_expected.to ensure_length_of(:path).is_within(0..255) } + it { is_expected.to ensure_length_of(:description).is_within(0..2000) } + it { is_expected.to validate_presence_of(:creator) } + it { is_expected.to ensure_length_of(:issues_tracker_id).is_within(0..255) } + it { is_expected.to validate_presence_of(:namespace) } it 'should not allow new projects beyond user limits' do project2 = build(:project) - project2.stub(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object) - project2.should_not be_valid - project2.errors[:limit_reached].first.should match(/Your project limit is 0/) + allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object) + expect(project2).not_to be_valid + expect(project2.errors[:limit_reached].first).to match(/Your project limit is 0/) end end describe 'Respond to' do - it { should respond_to(:url_to_repo) } - it { should respond_to(:repo_exists?) } - it { should respond_to(:satellite) } - it { should respond_to(:update_merge_requests) } - it { should respond_to(:execute_hooks) } - it { should respond_to(:name_with_namespace) } - it { should respond_to(:owner) } - it { should respond_to(:path_with_namespace) } + it { is_expected.to respond_to(:url_to_repo) } + it { is_expected.to respond_to(:repo_exists?) } + it { is_expected.to respond_to(:satellite) } + it { is_expected.to respond_to(:update_merge_requests) } + it { is_expected.to respond_to(:execute_hooks) } + it { is_expected.to respond_to(:name_with_namespace) } + it { is_expected.to respond_to(:owner) } + it { is_expected.to respond_to(:path_with_namespace) } end it 'should return valid url to repo' do project = Project.new(path: 'somewhere') - project.url_to_repo.should == Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git' + expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git') end it 'returns the full web URL for this repo' do project = Project.new(path: 'somewhere') - project.web_url.should == "#{Gitlab.config.gitlab.url}/somewhere" + expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/somewhere") end it 'returns the web URL without the protocol for this repo' do project = Project.new(path: 'somewhere') - project.web_url_without_protocol.should == "#{Gitlab.config.gitlab.url.split('://')[1]}/somewhere" + expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/somewhere") end describe 'last_activity methods' do @@ -113,18 +113,18 @@ describe Project do describe 'last_activity' do it 'should alias last_activity to last_event' do project.stub(last_event: last_event) - project.last_activity.should == last_event + expect(project.last_activity).to eq(last_event) end end describe 'last_activity_date' do it 'returns the creation date of the project\'s last event if present' do last_activity_event = create(:event, project: project) - project.last_activity_at.to_i.should == last_event.created_at.to_i + expect(project.last_activity_at.to_i).to eq(last_event.created_at.to_i) end it 'returns the project\'s last update date if it has no events' do - project.last_activity_date.should == project.updated_at + expect(project.last_activity_date).to eq(project.updated_at) end end end @@ -139,13 +139,13 @@ describe Project do it 'should close merge request if last commit from source branch was pushed to target branch' do project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user) merge_request.reload - merge_request.merged?.should be_true + expect(merge_request.merged?).to be_truthy end it 'should update merge request commits with new one if pushed to source branch' do project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user) merge_request.reload - merge_request.last_commit.id.should == commit_id + expect(merge_request.last_commit.id).to eq(commit_id) end end @@ -156,8 +156,8 @@ describe Project do @project = create(:project, name: 'gitlabhq', namespace: @group) end - it { Project.find_with_namespace('gitlab/gitlabhq').should == @project } - it { Project.find_with_namespace('gitlab-ci').should be_nil } + it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) } + it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil } end end @@ -168,7 +168,7 @@ describe Project do @project = create(:project, name: 'gitlabhq', namespace: @group) end - it { @project.to_param.should == 'gitlab/gitlabhq' } + it { expect(@project.to_param).to eq('gitlab/gitlabhq') } end end @@ -176,7 +176,7 @@ describe Project do let(:project) { create(:project) } it 'should return valid repo' do - project.repository.should be_kind_of(Repository) + expect(project.repository).to be_kind_of(Repository) end end @@ -187,15 +187,15 @@ describe Project do let(:ext_project) { create(:redmine_project) } it 'should be true or if used internal tracker and issue exists' do - project.issue_exists?(existed_issue.iid).should be_true + expect(project.issue_exists?(existed_issue.iid)).to be_truthy end it 'should be false or if used internal tracker and issue not exists' do - project.issue_exists?(not_existed_issue.iid).should be_false + expect(project.issue_exists?(not_existed_issue.iid)).to be_falsey end it 'should always be true if used other tracker' do - ext_project.issue_exists?(rand(100)).should be_true + expect(ext_project.issue_exists?(rand(100))).to be_truthy end end @@ -204,11 +204,11 @@ describe Project do let(:ext_project) { create(:redmine_project) } it "should be true if used internal tracker" do - project.default_issues_tracker?.should be_true + expect(project.default_issues_tracker?).to be_truthy end it "should be false if used other tracker" do - ext_project.default_issues_tracker?.should be_false + expect(ext_project.default_issues_tracker?).to be_falsey end end @@ -217,19 +217,19 @@ describe Project do let(:ext_project) { create(:redmine_project) } it 'should be true for projects with external issues tracker if issues enabled' do - ext_project.can_have_issues_tracker_id?.should be_true + expect(ext_project.can_have_issues_tracker_id?).to be_truthy end it 'should be false for projects with internal issue tracker if issues enabled' do - project.can_have_issues_tracker_id?.should be_false + expect(project.can_have_issues_tracker_id?).to be_falsey end it 'should be always false if issues disabled' do project.issues_enabled = false ext_project.issues_enabled = false - project.can_have_issues_tracker_id?.should be_false - ext_project.can_have_issues_tracker_id?.should be_false + expect(project.can_have_issues_tracker_id?).to be_falsey + expect(ext_project.can_have_issues_tracker_id?).to be_falsey end end @@ -240,8 +240,8 @@ describe Project do project.protected_branches.create(name: 'master') end - it { project.open_branches.map(&:name).should include('feature') } - it { project.open_branches.map(&:name).should_not include('master') } + it { expect(project.open_branches.map(&:name)).to include('feature') } + it { expect(project.open_branches.map(&:name)).not_to include('master') } end describe '#star_count' do @@ -318,12 +318,12 @@ describe Project do it 'should be true if avatar is image' do project.update_attribute(:avatar, 'uploads/avatar.png') - project.avatar_type.should be_true + expect(project.avatar_type).to be_truthy end it 'should be false if avatar is html page' do project.update_attribute(:avatar, 'uploads/avatar.html') - project.avatar_type.should == ['only images allowed'] + expect(project.avatar_type).to eq(['only images allowed']) end end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index bbf50b654f4..19201cc15a7 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -16,19 +16,19 @@ describe ProjectTeam do end describe 'members collection' do - it { project.team.masters.should include(master) } - it { project.team.masters.should_not include(guest) } - it { project.team.masters.should_not include(reporter) } - it { project.team.masters.should_not include(nonmember) } + it { expect(project.team.masters).to include(master) } + it { expect(project.team.masters).not_to include(guest) } + it { expect(project.team.masters).not_to include(reporter) } + it { expect(project.team.masters).not_to include(nonmember) } end describe 'access methods' do - it { project.team.master?(master).should be_true } - it { project.team.master?(guest).should be_false } - it { project.team.master?(reporter).should be_false } - it { project.team.master?(nonmember).should be_false } - it { project.team.member?(nonmember).should be_false } - it { project.team.member?(guest).should be_true } + it { expect(project.team.master?(master)).to be_truthy } + it { expect(project.team.master?(guest)).to be_falsey } + it { expect(project.team.master?(reporter)).to be_falsey } + it { expect(project.team.master?(nonmember)).to be_falsey } + it { expect(project.team.member?(nonmember)).to be_falsey } + it { expect(project.team.member?(guest)).to be_truthy } end end @@ -49,21 +49,21 @@ describe ProjectTeam do end describe 'members collection' do - it { project.team.reporters.should include(reporter) } - it { project.team.masters.should include(master) } - it { project.team.masters.should include(guest) } - it { project.team.masters.should_not include(reporter) } - it { project.team.masters.should_not include(nonmember) } + it { expect(project.team.reporters).to include(reporter) } + it { expect(project.team.masters).to include(master) } + it { expect(project.team.masters).to include(guest) } + it { expect(project.team.masters).not_to include(reporter) } + it { expect(project.team.masters).not_to include(nonmember) } end describe 'access methods' do - it { project.team.reporter?(reporter).should be_true } - it { project.team.master?(master).should be_true } - it { project.team.master?(guest).should be_true } - it { project.team.master?(reporter).should be_false } - it { project.team.master?(nonmember).should be_false } - it { project.team.member?(nonmember).should be_false } - it { project.team.member?(guest).should be_true } + it { expect(project.team.reporter?(reporter)).to be_truthy } + it { expect(project.team.master?(master)).to be_truthy } + it { expect(project.team.master?(guest)).to be_truthy } + it { expect(project.team.master?(reporter)).to be_falsey } + it { expect(project.team.master?(nonmember)).to be_falsey } + it { expect(project.team.member?(nonmember)).to be_falsey } + it { expect(project.team.member?(guest)).to be_truthy } end end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index e4ee2fc5b13..2acdb7dfddc 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -12,19 +12,19 @@ describe ProjectWiki do 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" + expect(subject.path_with_namespace).to eq(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) + expect(subject.url_to_repo).to eq(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 + expect(subject.ssh_url_to_repo).to eq(subject.url_to_repo) end end @@ -32,21 +32,21 @@ describe ProjectWiki 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 + expect(subject.http_url_to_repo).to eq(repo_http_url) end end describe "#wiki" do it "contains a Gollum::Wiki instance" do - subject.wiki.should be_a Gollum::Wiki + expect(subject.wiki).to be_a Gollum::Wiki end it "creates a new wiki repo if one does not yet exist" do - project_wiki.create_page("index", "test content").should be_true + expect(project_wiki.create_page("index", "test content")).to be_truthy end it "raises CouldNotCreateWikiError if it can't create the wiki repository" do - project_wiki.stub(:init_repo).and_return(false) + allow(project_wiki).to receive(:init_repo).and_return(false) expect { project_wiki.send(:create_repo!) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError) end end @@ -54,21 +54,27 @@ describe ProjectWiki do describe "#empty?" do context "when the wiki repository is empty" do before do - Gitlab::Shell.any_instance.stub(:add_repository) do + allow_any_instance_of(Gitlab::Shell).to receive(: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") + allow(project).to receive(:path_with_namespace).and_return("non-existant") end - its(:empty?) { should be_true } + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_truthy } + end end context "when the wiki has pages" do before do - create_page("index", "This is an awesome new Gollum Wiki") + project_wiki.create_page("index", "This is an awesome new Gollum Wiki") end - its(:empty?) { should be_false } + describe '#empty?' do + subject { super().empty? } + it { is_expected.to be_falsey } + end end end @@ -83,11 +89,11 @@ describe ProjectWiki do end it "returns an array of WikiPage instances" do - @pages.first.should be_a WikiPage + expect(@pages.first).to be_a WikiPage end it "returns the correct number of pages" do - @pages.count.should == 1 + expect(@pages.count).to eq(1) end end @@ -102,55 +108,55 @@ describe ProjectWiki do it "returns the latest version of the page if it exists" do page = subject.find_page("index page") - page.title.should == "index page" + expect(page.title).to eq("index page") end it "returns nil if the page does not exist" do - subject.find_page("non-existant").should == nil + expect(subject.find_page("non-existant")).to eq(nil) end it "can find a page by slug" do page = subject.find_page("index-page") - page.title.should == "index page" + expect(page.title).to eq("index page") end it "returns a WikiPage instance" do page = subject.find_page("index page") - page.should be_a WikiPage + expect(page).to be_a WikiPage end end describe '#find_file' do before do file = Gollum::File.new(subject.wiki) - Gollum::Wiki.any_instance. - stub(:file).with('image.jpg', 'master', true). + allow_any_instance_of(Gollum::Wiki). + to receive(:file).with('image.jpg', 'master', true). and_return(file) - Gollum::File.any_instance. - stub(:mime_type). + allow_any_instance_of(Gollum::File). + to receive(:mime_type). and_return('image/jpeg') - Gollum::Wiki.any_instance. - stub(:file).with('non-existant', 'master', true). + allow_any_instance_of(Gollum::Wiki). + to receive(:file).with('non-existant', 'master', true). and_return(nil) end after do - Gollum::Wiki.any_instance.unstub(:file) - Gollum::File.any_instance.unstub(:mime_type) + allow_any_instance_of(Gollum::Wiki).to receive(:file).and_call_original + allow_any_instance_of(Gollum::File).to receive(:mime_type).and_call_original end it 'returns the latest version of the file if it exists' do file = subject.find_file('image.jpg') - file.mime_type.should == 'image/jpeg' + expect(file.mime_type).to eq('image/jpeg') end it 'returns nil if the page does not exist' do - subject.find_file('non-existant').should == nil + expect(subject.find_file('non-existant')).to eq(nil) end it 'returns a Gollum::File instance' do file = subject.find_file('image.jpg') - file.should be_a Gollum::File + expect(file).to be_a Gollum::File end end @@ -160,23 +166,23 @@ describe ProjectWiki do end it "creates a new wiki page" do - subject.create_page("test page", "this is content").should_not == false - subject.pages.count.should == 1 + expect(subject.create_page("test page", "this is content")).not_to eq(false) + expect(subject.pages.count).to eq(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 + expect(subject.create_page("test page", "content")).to eq(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:/ + expect(subject.error_message).to match(/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" + expect(subject.pages.first.page.version.message).to eq("commit message") end end @@ -193,11 +199,11 @@ describe ProjectWiki do end it "updates the content of the page" do - @page.raw_data.should == "some other content" + expect(@page.raw_data).to eq("some other content") end it "sets the correct commit message" do - @page.version.message.should == "updated page" + expect(@page.version.message).to eq("updated page") end end @@ -209,7 +215,7 @@ describe ProjectWiki do it "deletes the page" do subject.delete_page(@page) - subject.pages.count.should == 0 + expect(subject.pages.count).to eq(0) end end diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index b0f57e8a206..1e6937b536c 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -14,14 +14,14 @@ require 'spec_helper' describe ProtectedBranch do describe 'Associations' do - it { should belong_to(:project) } + it { is_expected.to belong_to(:project) } end describe "Mass assignment" do end describe 'Validation' do - it { should validate_presence_of(:project) } - it { should validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:name) } end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 6c3e221f343..eeb0f3d9ee0 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -8,14 +8,14 @@ describe Repository do describe :branch_names_contains do subject { repository.branch_names_contains(sample_commit.id) } - it { should include('master') } - it { should_not include('feature') } - it { should_not include('fix') } + it { is_expected.to include('master') } + it { is_expected.not_to include('feature') } + it { is_expected.not_to include('fix') } end describe :last_commit_for_path do subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } - it { should eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } + it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index c96f2b20529..1129bd1c76d 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -17,8 +17,8 @@ require 'spec_helper' describe Service do describe "Associations" do - it { should belong_to :project } - it { should have_one :service_hook } + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } end describe "Mass assignment" do @@ -40,7 +40,7 @@ describe Service do end describe :can_test do - it { @testable.should == true } + it { expect(@testable).to eq(true) } end end @@ -55,7 +55,7 @@ describe Service do end describe :can_test do - it { @testable.should == true } + it { expect(@testable).to eq(true) } end end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 1ef2c512c1f..e37dcc75230 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -19,22 +19,22 @@ require 'spec_helper' describe Snippet do describe "Associations" do - it { should belong_to(:author).class_name('User') } - it { should have_many(:notes).dependent(:destroy) } + it { is_expected.to belong_to(:author).class_name('User') } + it { is_expected.to have_many(:notes).dependent(:destroy) } end describe "Mass assignment" do end describe "Validation" do - it { should validate_presence_of(:author) } + it { is_expected.to validate_presence_of(:author) } - it { should validate_presence_of(:title) } - it { should ensure_length_of(:title).is_within(0..255) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to ensure_length_of(:title).is_within(0..255) } - it { should validate_presence_of(:file_name) } - it { should ensure_length_of(:title).is_within(0..255) } + it { is_expected.to validate_presence_of(:file_name) } + it { is_expected.to ensure_length_of(:title).is_within(0..255) } - it { should validate_presence_of(:content) } + it { is_expected.to validate_presence_of(:content) } end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 629d51b960d..e853262e00f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -49,32 +49,32 @@ require 'spec_helper' describe User do describe "Associations" do - it { should have_one(:namespace) } - it { should have_many(:snippets).class_name('Snippet').dependent(:destroy) } - it { should have_many(:project_members).dependent(:destroy) } - it { should have_many(:groups) } - it { should have_many(:keys).dependent(:destroy) } - it { should have_many(:events).class_name('Event').dependent(:destroy) } - it { should have_many(:recent_events).class_name('Event') } - it { should have_many(:issues).dependent(:destroy) } - it { should have_many(:notes).dependent(:destroy) } - it { should have_many(:assigned_issues).dependent(:destroy) } - it { should have_many(:merge_requests).dependent(:destroy) } - it { should have_many(:assigned_merge_requests).dependent(:destroy) } - it { should have_many(:identities).dependent(:destroy) } + it { is_expected.to have_one(:namespace) } + it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } + it { is_expected.to have_many(:project_members).dependent(:destroy) } + it { is_expected.to have_many(:groups) } + it { is_expected.to have_many(:keys).dependent(:destroy) } + it { is_expected.to have_many(:events).class_name('Event').dependent(:destroy) } + it { is_expected.to have_many(:recent_events).class_name('Event') } + it { is_expected.to have_many(:issues).dependent(:destroy) } + it { is_expected.to have_many(:notes).dependent(:destroy) } + it { is_expected.to have_many(:assigned_issues).dependent(:destroy) } + it { is_expected.to have_many(:merge_requests).dependent(:destroy) } + it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) } + it { is_expected.to have_many(:identities).dependent(:destroy) } end describe "Mass assignment" do end describe 'validations' do - it { should validate_presence_of(:username) } - it { should validate_presence_of(:projects_limit) } - it { should validate_numericality_of(:projects_limit) } - it { should allow_value(0).for(:projects_limit) } - it { should_not allow_value(-1).for(:projects_limit) } + it { is_expected.to validate_presence_of(:username) } + it { is_expected.to validate_presence_of(:projects_limit) } + it { is_expected.to validate_numericality_of(:projects_limit) } + it { is_expected.to allow_value(0).for(:projects_limit) } + it { is_expected.not_to allow_value(-1).for(:projects_limit) } - it { should ensure_length_of(:bio).is_within(0..255) } + it { is_expected.to ensure_length_of(:bio).is_within(0..255) } describe 'email' do it 'accepts info@example.com' do @@ -110,34 +110,34 @@ describe User do end describe "Respond to" do - it { should respond_to(:is_admin?) } - it { should respond_to(:name) } - it { should respond_to(:private_token) } + it { is_expected.to respond_to(:is_admin?) } + it { is_expected.to respond_to(:name) } + it { is_expected.to respond_to(:private_token) } end describe '#generate_password' do it "should execute callback when force_random_password specified" do user = build(:user, force_random_password: true) - user.should_receive(:generate_password) + expect(user).to receive(:generate_password) user.save end it "should not generate password by default" do user = create(:user, password: 'abcdefghe') - user.password.should == 'abcdefghe' + expect(user.password).to eq('abcdefghe') end it "should generate password when forcing random password" do - Devise.stub(:friendly_token).and_return('123456789') + allow(Devise).to receive(:friendly_token).and_return('123456789') user = create(:user, password: 'abcdefg', force_random_password: true) - user.password.should == '12345678' + expect(user.password).to eq('12345678') end end describe 'authentication token' do it "should have authentication token" do user = create(:user) - user.authentication_token.should_not be_blank + expect(user.authentication_token).not_to be_blank end end @@ -152,15 +152,15 @@ describe User do @project_3.team << [@user, :developer] end - it { @user.authorized_projects.should include(@project) } - it { @user.authorized_projects.should include(@project_2) } - it { @user.authorized_projects.should include(@project_3) } - it { @user.owned_projects.should include(@project) } - it { @user.owned_projects.should_not include(@project_2) } - it { @user.owned_projects.should_not include(@project_3) } - it { @user.personal_projects.should include(@project) } - it { @user.personal_projects.should_not include(@project_2) } - it { @user.personal_projects.should_not include(@project_3) } + it { expect(@user.authorized_projects).to include(@project) } + it { expect(@user.authorized_projects).to include(@project_2) } + it { expect(@user.authorized_projects).to include(@project_3) } + it { expect(@user.owned_projects).to include(@project) } + it { expect(@user.owned_projects).not_to include(@project_2) } + it { expect(@user.owned_projects).not_to include(@project_3) } + it { expect(@user.personal_projects).to include(@project) } + it { expect(@user.personal_projects).not_to include(@project_2) } + it { expect(@user.personal_projects).not_to include(@project_3) } end describe 'groups' do @@ -170,9 +170,9 @@ describe User do @group.add_owner(@user) end - it { @user.several_namespaces?.should be_true } - it { @user.authorized_groups.should == [@group] } - it { @user.owned_groups.should == [@group] } + it { expect(@user.several_namespaces?).to be_truthy } + it { expect(@user.authorized_groups).to eq([@group]) } + it { expect(@user.owned_groups).to eq([@group]) } end describe 'group multiple owners' do @@ -185,7 +185,7 @@ describe User do @group.add_user(@user2, GroupMember::OWNER) end - it { @user2.several_namespaces?.should be_true } + it { expect(@user2.several_namespaces?).to be_truthy } end describe 'namespaced' do @@ -194,7 +194,7 @@ describe User do @project = create :project, namespace: @user.namespace end - it { @user.several_namespaces?.should be_false } + it { expect(@user.several_namespaces?).to be_falsey } end describe 'blocking user' do @@ -202,7 +202,7 @@ describe User do it "should block user" do user.block - user.blocked?.should be_true + expect(user.blocked?).to be_truthy end end @@ -214,10 +214,10 @@ describe User do @blocked = create :user, state: :blocked end - it { User.filter("admins").should == [@admin] } - it { User.filter("blocked").should == [@blocked] } - it { User.filter("wop").should include(@user, @admin, @blocked) } - it { User.filter(nil).should include(@user, @admin) } + it { expect(User.filter("admins")).to eq([@admin]) } + it { expect(User.filter("blocked")).to eq([@blocked]) } + it { expect(User.filter("wop")).to include(@user, @admin, @blocked) } + it { expect(User.filter(nil)).to include(@user, @admin) } end describe :not_in_project do @@ -227,27 +227,27 @@ describe User do @project = create :project end - it { User.not_in_project(@project).should include(@user, @project.owner) } + it { expect(User.not_in_project(@project)).to include(@user, @project.owner) } end describe 'user creation' do describe 'normal user' do let(:user) { create(:user, name: 'John Smith') } - it { user.is_admin?.should be_false } - it { user.require_ssh_key?.should be_true } - it { user.can_create_group?.should be_true } - it { user.can_create_project?.should be_true } - it { user.first_name.should == 'John' } + it { expect(user.is_admin?).to be_falsey } + it { expect(user.require_ssh_key?).to be_truthy } + it { expect(user.can_create_group?).to be_truthy } + it { expect(user.can_create_project?).to be_truthy } + it { expect(user.first_name).to eq('John') } end describe 'with defaults' do let(:user) { User.new } it "should apply defaults to user" do - user.projects_limit.should == Gitlab.config.gitlab.default_projects_limit - user.can_create_group.should == Gitlab.config.gitlab.default_can_create_group - user.theme_id.should == Gitlab.config.gitlab.default_theme + expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) + expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) + expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) end end @@ -255,9 +255,9 @@ describe User do let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: Gitlab::Theme::BASIC) } it "should apply defaults to user" do - user.projects_limit.should == 123 - user.can_create_group.should be_false - user.theme_id.should == Gitlab::Theme::BASIC + expect(user.projects_limit).to eq(123) + expect(user.can_create_group).to be_falsey + expect(user.theme_id).to eq(Gitlab::Theme::BASIC) end end end @@ -267,12 +267,12 @@ describe User do let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') } it "should be case insensitive" do - User.search(user1.username.upcase).to_a.should == [user1] - User.search(user1.username.downcase).to_a.should == [user1] - User.search(user2.username.upcase).to_a.should == [user2] - User.search(user2.username.downcase).to_a.should == [user2] - User.search(user1.username.downcase).to_a.count.should == 2 - User.search(user2.username.downcase).to_a.count.should == 1 + expect(User.search(user1.username.upcase).to_a).to eq([user1]) + expect(User.search(user1.username.downcase).to_a).to eq([user1]) + expect(User.search(user2.username.upcase).to_a).to eq([user2]) + expect(User.search(user2.username.downcase).to_a).to eq([user2]) + expect(User.search(user1.username.downcase).to_a.count).to eq(2) + expect(User.search(user2.username.downcase).to_a.count).to eq(1) end end @@ -280,10 +280,10 @@ describe User do let(:user1) { create(:user, username: 'foo') } it "should get the correct user" do - User.by_username_or_id(user1.id).should == user1 - User.by_username_or_id('foo').should == user1 - User.by_username_or_id(-1).should be_nil - User.by_username_or_id('bar').should be_nil + expect(User.by_username_or_id(user1.id)).to eq(user1) + expect(User.by_username_or_id('foo')).to eq(user1) + expect(User.by_username_or_id(-1)).to be_nil + expect(User.by_username_or_id('bar')).to be_nil end end @@ -302,13 +302,13 @@ describe User do end describe 'all_ssh_keys' do - it { should have_many(:keys).dependent(:destroy) } + it { is_expected.to have_many(:keys).dependent(:destroy) } it "should have all ssh keys" do user = create :user key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id - user.all_ssh_keys.should include(key.key) + expect(user.all_ssh_keys).to include(key.key) end end @@ -317,12 +317,12 @@ describe User do it "should be true if avatar is image" do user.update_attribute(:avatar, 'uploads/avatar.png') - user.avatar_type.should be_true + expect(user.avatar_type).to be_truthy end it "should be false if avatar is html page" do user.update_attribute(:avatar, 'uploads/avatar.html') - user.avatar_type.should == ["only images allowed"] + expect(user.avatar_type).to eq(["only images allowed"]) end end @@ -333,7 +333,7 @@ describe User do # Create a condition which would otherwise cause 'true' to be returned user.stub(ldap_user?: true) user.last_credential_check_at = nil - expect(user.requires_ldap_check?).to be_false + expect(user.requires_ldap_check?).to be_falsey end context 'when LDAP is enabled' do @@ -341,7 +341,7 @@ describe User do it 'is false for non-LDAP users' do user.stub(ldap_user?: false) - expect(user.requires_ldap_check?).to be_false + expect(user.requires_ldap_check?).to be_falsey end context 'and when the user is an LDAP user' do @@ -349,12 +349,12 @@ describe User do it 'is true when the user has never had an LDAP check before' do user.last_credential_check_at = nil - expect(user.requires_ldap_check?).to be_true + expect(user.requires_ldap_check?).to be_truthy end it 'is true when the last LDAP check happened over 1 hour ago' do user.last_credential_check_at = 2.hours.ago - expect(user.requires_ldap_check?).to be_true + expect(user.requires_ldap_check?).to be_truthy end end end @@ -363,24 +363,24 @@ describe User do describe :ldap_user? do it "is true if provider name starts with ldap" do user = create(:omniauth_user, provider: 'ldapmain') - expect( user.ldap_user? ).to be_true + expect( user.ldap_user? ).to be_truthy end it "is false for other providers" do user = create(:omniauth_user, provider: 'other-provider') - expect( user.ldap_user? ).to be_false + expect( user.ldap_user? ).to be_falsey end it "is false if no extern_uid is provided" do user = create(:omniauth_user, extern_uid: nil) - expect( user.ldap_user? ).to be_false + expect( user.ldap_user? ).to be_falsey end end describe :ldap_identity do it "returns ldap identity" do user = create :omniauth_user - user.ldap_identity.provider.should_not be_empty + expect(user.ldap_identity.provider).not_to be_empty end end @@ -434,24 +434,24 @@ describe User do project1 = create :project, :public project2 = create :project, :public - expect(user.starred?(project1)).to be_false - expect(user.starred?(project2)).to be_false + expect(user.starred?(project1)).to be_falsey + expect(user.starred?(project2)).to be_falsey star1 = UsersStarProject.create!(project: project1, user: user) - expect(user.starred?(project1)).to be_true - expect(user.starred?(project2)).to be_false + expect(user.starred?(project1)).to be_truthy + expect(user.starred?(project2)).to be_falsey star2 = UsersStarProject.create!(project: project2, user: user) - expect(user.starred?(project1)).to be_true - expect(user.starred?(project2)).to be_true + expect(user.starred?(project1)).to be_truthy + expect(user.starred?(project2)).to be_truthy star1.destroy - expect(user.starred?(project1)).to be_false - expect(user.starred?(project2)).to be_true + expect(user.starred?(project1)).to be_falsey + expect(user.starred?(project2)).to be_truthy star2.destroy - expect(user.starred?(project1)).to be_false - expect(user.starred?(project2)).to be_false + expect(user.starred?(project1)).to be_falsey + expect(user.starred?(project2)).to be_falsey end end @@ -460,11 +460,11 @@ describe User do user = create :user project = create :project, :public - expect(user.starred?(project)).to be_false + expect(user.starred?(project)).to be_falsey user.toggle_star(project) - expect(user.starred?(project)).to be_true + expect(user.starred?(project)).to be_truthy user.toggle_star(project) - expect(user.starred?(project)).to be_false + expect(user.starred?(project)).to be_falsey end end @@ -476,23 +476,23 @@ describe User do end it "sorts users as recently_signed_in" do - User.sort('recent_sign_in').first.should == @user + expect(User.sort('recent_sign_in').first).to eq(@user) end it "sorts users as late_signed_in" do - User.sort('oldest_sign_in').first.should == @user1 + expect(User.sort('oldest_sign_in').first).to eq(@user1) end it "sorts users as recently_created" do - User.sort('created_desc').first.should == @user + expect(User.sort('created_desc').first).to eq(@user) end it "sorts users as late_created" do - User.sort('created_asc').first.should == @user1 + expect(User.sort('created_asc').first).to eq(@user1) end it "sorts users by name when nil is passed" do - User.sort(nil).first.should == @user + expect(User.sort(nil).first).to eq(@user) end end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 78877db61b7..f3fd805783f 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -16,27 +16,27 @@ describe WikiPage do end it "sets the slug attribute" do - @wiki_page.slug.should == "test-page" + expect(@wiki_page.slug).to eq("test-page") end it "sets the title attribute" do - @wiki_page.title.should == "test page" + expect(@wiki_page.title).to eq("test page") end it "sets the formatted content attribute" do - @wiki_page.content.should == "test content" + expect(@wiki_page.content).to eq("test content") end it "sets the format attribute" do - @wiki_page.format.should == :markdown + expect(@wiki_page.format).to eq(:markdown) end it "sets the message attribute" do - @wiki_page.message.should == "test commit" + expect(@wiki_page.message).to eq("test commit") end it "sets the version attribute" do - @wiki_page.version.should be_a Gollum::Git::Commit + expect(@wiki_page.version).to be_a Gollum::Git::Commit end end end @@ -48,12 +48,12 @@ describe WikiPage do it "validates presence of title" do subject.attributes.delete(:title) - subject.valid?.should be_false + expect(subject.valid?).to be_falsey end it "validates presence of content" do subject.attributes.delete(:content) - subject.valid?.should be_false + expect(subject.valid?).to be_falsey end end @@ -69,11 +69,11 @@ describe WikiPage do context "with valid attributes" do it "saves the wiki page" do subject.create(@wiki_attr) - wiki.find_page("Index").should_not be_nil + expect(wiki.find_page("Index")).not_to be_nil end it "returns true" do - subject.create(@wiki_attr).should == true + expect(subject.create(@wiki_attr)).to eq(true) end end end @@ -95,7 +95,7 @@ describe WikiPage do end it "returns true" do - @page.update("more content").should be_true + expect(@page.update("more content")).to be_truthy end end end @@ -108,11 +108,11 @@ describe WikiPage do it "should delete the page" do @page.delete - wiki.pages.should be_empty + expect(wiki.pages).to be_empty end it "should return true" do - @page.delete.should == true + expect(@page.delete).to eq(true) end end @@ -128,7 +128,7 @@ describe WikiPage do it "returns an array of all commits for the page" do 3.times { |i| @page.update("content #{i}") } - @page.versions.count.should == 4 + expect(@page.versions.count).to eq(4) end end @@ -144,7 +144,7 @@ describe WikiPage do it "should be replace a hyphen to a space" do @page.title = "Import-existing-repositories-into-GitLab" - @page.title.should == "Import existing repositories into GitLab" + expect(@page.title).to eq("Import existing repositories into GitLab") end end diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index cc071342d7c..20cb30a39bb 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -41,33 +41,33 @@ describe API, api: true do describe ".current_user" do it "should return nil for an invalid token" do env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - self.class.any_instance.stub(:doorkeeper_guard){ false } - current_user.should be_nil + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil end it "should return nil for a user without access" do env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token Gitlab::UserAccess.stub(allowed?: false) - current_user.should be_nil + expect(current_user).to be_nil end it "should leave user as is when sudo not specified" do env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token - current_user.should == user + expect(current_user).to eq(user) clear_env params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = user.private_token - current_user.should == user + expect(current_user).to eq(user) end it "should change current user to sudo when admin" do set_env(admin, user.id) - current_user.should == user + expect(current_user).to eq(user) set_param(admin, user.id) - current_user.should == user + expect(current_user).to eq(user) set_env(admin, user.username) - current_user.should == user + expect(current_user).to eq(user) set_param(admin, user.username) - current_user.should == user + expect(current_user).to eq(user) end it "should throw an error when the current user is not an admin and attempting to sudo" do @@ -83,8 +83,8 @@ describe API, api: true do it "should throw an error when the user cannot be found for a given id" do id = user.id + admin.id - user.id.should_not == id - admin.id.should_not == id + expect(user.id).not_to eq(id) + expect(admin.id).not_to eq(id) set_env(admin, id) expect { current_user }.to raise_error @@ -94,8 +94,8 @@ describe API, api: true do it "should throw an error when the user cannot be found for a given username" do username = "#{user.username}#{admin.username}" - user.username.should_not == username - admin.username.should_not == username + expect(user.username).not_to eq(username) + expect(admin.username).not_to eq(username) set_env(admin, username) expect { current_user }.to raise_error @@ -105,69 +105,69 @@ describe API, api: true do it "should handle sudo's to oneself" do set_env(admin, admin.id) - current_user.should == admin + expect(current_user).to eq(admin) set_param(admin, admin.id) - current_user.should == admin + expect(current_user).to eq(admin) set_env(admin, admin.username) - current_user.should == admin + expect(current_user).to eq(admin) set_param(admin, admin.username) - current_user.should == admin + expect(current_user).to eq(admin) end it "should handle multiple sudo's to oneself" do set_env(admin, user.id) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) set_env(admin, user.username) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) set_param(admin, user.id) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) set_param(admin, user.username) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) end it "should handle multiple sudo's to oneself using string ids" do set_env(admin, user.id.to_s) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) set_param(admin, user.id.to_s) - current_user.should == user - current_user.should == user + expect(current_user).to eq(user) + expect(current_user).to eq(user) end end describe '.sudo_identifier' do it "should return integers when input is an int" do set_env(admin, '123') - sudo_identifier.should == 123 + expect(sudo_identifier).to eq(123) set_env(admin, '0001234567890') - sudo_identifier.should == 1234567890 + expect(sudo_identifier).to eq(1234567890) set_param(admin, '123') - sudo_identifier.should == 123 + expect(sudo_identifier).to eq(123) set_param(admin, '0001234567890') - sudo_identifier.should == 1234567890 + expect(sudo_identifier).to eq(1234567890) end it "should return string when input is an is not an int" do set_env(admin, '12.30') - sudo_identifier.should == "12.30" + expect(sudo_identifier).to eq("12.30") set_env(admin, 'hello') - sudo_identifier.should == 'hello' + expect(sudo_identifier).to eq('hello') set_env(admin, ' 123') - sudo_identifier.should == ' 123' + expect(sudo_identifier).to eq(' 123') set_param(admin, '12.30') - sudo_identifier.should == "12.30" + expect(sudo_identifier).to eq("12.30") set_param(admin, 'hello') - sudo_identifier.should == 'hello' + expect(sudo_identifier).to eq('hello') set_param(admin, ' 123') - sudo_identifier.should == ' 123' + expect(sudo_identifier).to eq(' 123') end end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index b45572c39fd..f40d68b75a4 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -15,79 +15,79 @@ describe API::API, api: true do describe "GET /projects/:id/repository/branches" do it "should return an array of project branches" do get api("/projects/#{project.id}/repository/branches", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['name'].should == project.repository.branch_names.first + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project.repository.branch_names.first) end end describe "GET /projects/:id/repository/branches/:branch" do it "should return the branch information for a single branch" do get api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response['name'].should == branch_name - json_response['commit']['id'].should == branch_sha - json_response['protected'].should == false + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(false) end it "should return a 403 error if guest" do get api("/projects/#{project.id}/repository/branches", user2) - response.status.should == 403 + expect(response.status).to eq(403) end it "should return a 404 error if branch is not available" do get api("/projects/#{project.id}/repository/branches/unknown", user) - response.status.should == 404 + expect(response.status).to eq(404) end end describe "PUT /projects/:id/repository/branches/:branch/protect" do it "should protect a single branch" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response['name'].should == branch_name - json_response['commit']['id'].should == branch_sha - json_response['protected'].should == true + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) end it "should return a 404 error if branch not found" do put api("/projects/#{project.id}/repository/branches/unknown/protect", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should return a 403 error if guest" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2) - response.status.should == 403 + expect(response.status).to eq(403) end it "should return success when protect branch again" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - response.status.should == 200 + expect(response.status).to eq(200) end end describe "PUT /projects/:id/repository/branches/:branch/unprotect" do it "should unprotect a single branch" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response['name'].should == branch_name - json_response['commit']['id'].should == branch_sha - json_response['protected'].should == false + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(false) end it "should return success when unprotect branch" do put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should return success when unprotect branch again" do put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user) - response.status.should == 200 + expect(response.status).to eq(200) end end @@ -97,46 +97,46 @@ describe API::API, api: true do branch_name: 'feature1', ref: branch_sha - response.status.should == 201 + expect(response.status).to eq(201) - json_response['name'].should == 'feature1' - json_response['commit']['id'].should == branch_sha + expect(json_response['name']).to eq('feature1') + expect(json_response['commit']['id']).to eq(branch_sha) end it "should deny for user without push access" do post api("/projects/#{project.id}/repository/branches", user2), branch_name: branch_name, ref: branch_sha - response.status.should == 403 + expect(response.status).to eq(403) end it 'should return 400 if branch name is invalid' do post api("/projects/#{project.id}/repository/branches", user), branch_name: 'new design', ref: branch_sha - response.status.should == 400 - json_response['message'].should == 'Branch name invalid' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Branch name invalid') end it 'should return 400 if branch already exists' do post api("/projects/#{project.id}/repository/branches", user), branch_name: 'new_design1', ref: branch_sha - response.status.should == 201 + expect(response.status).to eq(201) post api("/projects/#{project.id}/repository/branches", user), branch_name: 'new_design1', ref: branch_sha - response.status.should == 400 - json_response['message'].should == 'Branch already exists' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Branch already exists') end it 'should return 400 if ref name is invalid' do post api("/projects/#{project.id}/repository/branches", user), branch_name: 'new_design3', ref: 'foo' - response.status.should == 400 - json_response['message'].should == 'Invalid reference name' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Invalid reference name') end end @@ -145,26 +145,26 @@ describe API::API, api: true do it "should remove branch" do delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - response.status.should == 200 - json_response['branch_name'].should == branch_name + expect(response.status).to eq(200) + expect(json_response['branch_name']).to eq(branch_name) end it 'should return 404 if branch not exists' do delete api("/projects/#{project.id}/repository/branches/foobar", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should remove protected branch" do project.protected_branches.create(name: branch_name) delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user) - response.status.should == 405 - json_response['message'].should == 'Protected branch cant be removed' + expect(response.status).to eq(405) + expect(json_response['message']).to eq('Protected branch cant be removed') end it "should not remove HEAD branch" do delete api("/projects/#{project.id}/repository/branches/master", user) - response.status.should == 405 - json_response['message'].should == 'Cannot remove HEAD branch' + expect(response.status).to eq(405) + expect(json_response['message']).to eq('Cannot remove HEAD branch') end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index a3f58f50913..9ea60e1a4ad 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -18,17 +18,17 @@ describe API::API, api: true do it "should return project commits" do get api("/projects/#{project.id}/repository/commits", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response.should be_an Array - json_response.first['id'].should == project.repository.commit.id + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(project.repository.commit.id) end end context "unauthorized user" do it "should not return project commits" do get api("/projects/#{project.id}/repository/commits") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -37,21 +37,21 @@ describe API::API, api: true do context "authorized user" do it "should return a commit by sha" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) - response.status.should == 200 - json_response['id'].should == project.repository.commit.id - json_response['title'].should == project.repository.commit.title + expect(response.status).to eq(200) + expect(json_response['id']).to eq(project.repository.commit.id) + expect(json_response['title']).to eq(project.repository.commit.title) end it "should return a 404 error if not found" do get api("/projects/#{project.id}/repository/commits/invalid_sha", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context "unauthorized user" do it "should not return the selected commit" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -62,23 +62,23 @@ describe API::API, api: true do it "should return the diff of the selected commit" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response.should be_an Array - json_response.length.should >= 1 - json_response.first.keys.should include "diff" + expect(json_response).to be_an Array + expect(json_response.length).to be >= 1 + expect(json_response.first.keys).to include "diff" end it "should return a 404 error if invalid commit" do get api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context "unauthorized user" do it "should not return the diff of the selected commit" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -87,23 +87,23 @@ describe API::API, api: true do context 'authorized user' do it 'should return merge_request comments' do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['note'].should == 'a comment on a commit' - json_response.first['author']['id'].should == user.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['note']).to eq('a comment on a commit') + expect(json_response.first['author']['id']).to eq(user.id) end it 'should return a 404 error if merge_request_id not found' do get api("/projects/#{project.id}/repository/commits/1234ab/comments", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context 'unauthorized user' do it 'should not return the diff of the selected commit' do get api("/projects/#{project.id}/repository/commits/1234ab/comments") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -112,37 +112,37 @@ describe API::API, api: true do context 'authorized user' do it 'should return comment' do post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment' - response.status.should == 201 - json_response['note'].should == 'My comment' - json_response['path'].should be_nil - json_response['line'].should be_nil - json_response['line_type'].should be_nil + expect(response.status).to eq(201) + expect(json_response['note']).to eq('My comment') + expect(json_response['path']).to be_nil + expect(json_response['line']).to be_nil + expect(json_response['line_type']).to be_nil end it 'should return the inline comment' do post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.diffs.first.new_path, line: 7, line_type: 'new' - response.status.should == 201 - json_response['note'].should == 'My comment' - json_response['path'].should == project.repository.commit.diffs.first.new_path - json_response['line'].should == 7 - json_response['line_type'].should == 'new' + expect(response.status).to eq(201) + expect(json_response['note']).to eq('My comment') + expect(json_response['path']).to eq(project.repository.commit.diffs.first.new_path) + expect(json_response['line']).to eq(7) + expect(json_response['line_type']).to eq('new') end it 'should return 400 if note is missing' do post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user) - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 404 if note is attached to non existent commit' do post api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment' - response.status.should == 404 + expect(response.status).to eq(404) end end context 'unauthorized user' do it 'should not return the diff of the selected commit' do post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments") - response.status.should == 401 + expect(response.status).to eq(401) end end end diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index ddef99d77af..39949a90422 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -11,21 +11,21 @@ describe API::API, api: true do describe "when unauthenticated" do it "returns authentication success" do get api("/user"), :access_token => token.token - response.status.should == 200 + expect(response.status).to eq(200) end end describe "when token invalid" do it "returns authentication error" do get api("/user"), :access_token => "123a" - response.status.should == 401 + expect(response.status).to eq(401) end end describe "authorization by private token" do it "returns authentication success" do get api("/user", user) - response.status.should == 200 + expect(response.status).to eq(200) end end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index b43a202aec0..cfac7d289ec 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -16,15 +16,15 @@ describe API::API, api: true do } get api("/projects/#{project.id}/repository/files", user), params - response.status.should == 200 - json_response['file_path'].should == file_path - json_response['file_name'].should == 'popen.rb' - Base64.decode64(json_response['content']).lines.first.should == "require 'fileutils'\n" + expect(response.status).to eq(200) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq('popen.rb') + expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n") end it "should return a 400 bad request if no params given" do get api("/projects/#{project.id}/repository/files", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 404 if such file does not exist" do @@ -34,7 +34,7 @@ describe API::API, api: true do } get api("/projects/#{project.id}/repository/files", user), params - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -54,13 +54,13 @@ describe API::API, api: true do ) post api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 201 - json_response['file_path'].should == 'newfile.rb' + expect(response.status).to eq(201) + expect(json_response['file_path']).to eq('newfile.rb') end it "should return a 400 bad request if no params given" do post api("/projects/#{project.id}/repository/files", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 400 if satellite fails to create file" do @@ -69,7 +69,7 @@ describe API::API, api: true do ) post api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -89,13 +89,13 @@ describe API::API, api: true do ) put api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 200 - json_response['file_path'].should == file_path + expect(response.status).to eq(200) + expect(json_response['file_path']).to eq(file_path) end it "should return a 400 bad request if no params given" do put api("/projects/#{project.id}/repository/files", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 400 if satellite fails to create file" do @@ -104,7 +104,7 @@ describe API::API, api: true do ) put api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -123,13 +123,13 @@ describe API::API, api: true do ) delete api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 200 - json_response['file_path'].should == file_path + expect(response.status).to eq(200) + expect(json_response['file_path']).to eq(file_path) end it "should return a 400 bad request if no params given" do delete api("/projects/#{project.id}/repository/files", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 400 if satellite fails to create file" do @@ -138,7 +138,7 @@ describe API::API, api: true do ) delete api("/projects/#{project.id}/repository/files", user), valid_params - response.status.should == 400 + expect(response.status).to eq(400) end end end diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb index 5921b3e0698..fb3ff552c8d 100644 --- a/spec/requests/api/fork_spec.rb +++ b/spec/requests/api/fork_spec.rb @@ -23,50 +23,50 @@ describe API::API, api: true do context 'when authenticated' do it 'should fork if user has sufficient access to project' do post api("/projects/fork/#{project.id}", user2) - response.status.should == 201 - json_response['name'].should == project.name - json_response['path'].should == project.path - json_response['owner']['id'].should == user2.id - json_response['namespace']['id'].should == user2.namespace.id - json_response['forked_from_project']['id'].should == project.id + expect(response.status).to eq(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(user2.id) + expect(json_response['namespace']['id']).to eq(user2.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) end it 'should fork if user is admin' do post api("/projects/fork/#{project.id}", admin) - response.status.should == 201 - json_response['name'].should == project.name - json_response['path'].should == project.path - json_response['owner']['id'].should == admin.id - json_response['namespace']['id'].should == admin.namespace.id - json_response['forked_from_project']['id'].should == project.id + expect(response.status).to eq(201) + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to eq(project.path) + expect(json_response['owner']['id']).to eq(admin.id) + expect(json_response['namespace']['id']).to eq(admin.namespace.id) + expect(json_response['forked_from_project']['id']).to eq(project.id) end it 'should fail on missing project access for the project to fork' do post api("/projects/fork/#{project.id}", user3) - response.status.should == 404 - json_response['message'].should == '404 Project Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Project Not Found') end it 'should fail if forked project exists in the user namespace' do post api("/projects/fork/#{project.id}", user) - response.status.should == 409 - json_response['message']['base'].should == ['Invalid fork destination'] - json_response['message']['name'].should == ['has already been taken'] - json_response['message']['path'].should == ['has already been taken'] + expect(response.status).to eq(409) + expect(json_response['message']['base']).to eq(['Invalid fork destination']) + expect(json_response['message']['name']).to eq(['has already been taken']) + expect(json_response['message']['path']).to eq(['has already been taken']) end it 'should fail if project to fork from does not exist' do post api('/projects/fork/424242', user) - response.status.should == 404 - json_response['message'].should == '404 Project Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Project Not Found') end end context 'when unauthenticated' do it 'should return authentication error' do post api("/projects/fork/#{project.id}") - response.status.should == 401 - json_response['message'].should == '401 Unauthorized' + expect(response.status).to eq(401) + expect(json_response['message']).to eq('401 Unauthorized') end end end diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb index 4957186f605..b070bf01dbb 100644 --- a/spec/requests/api/group_members_spec.rb +++ b/spec/requests/api/group_members_spec.rb @@ -31,20 +31,20 @@ describe API::API, api: true do it "each user: should return an array of members groups of group3" do [owner, master, developer, reporter, guest].each do |user| get api("/groups/#{group_with_members.id}/members", user) - response.status.should == 200 - json_response.should be_an Array - json_response.size.should == 5 - json_response.find { |e| e['id']==owner.id }['access_level'].should == GroupMember::OWNER - json_response.find { |e| e['id']==reporter.id }['access_level'].should == GroupMember::REPORTER - json_response.find { |e| e['id']==developer.id }['access_level'].should == GroupMember::DEVELOPER - json_response.find { |e| e['id']==master.id }['access_level'].should == GroupMember::MASTER - json_response.find { |e| e['id']==guest.id }['access_level'].should == GroupMember::GUEST + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(5) + expect(json_response.find { |e| e['id']==owner.id }['access_level']).to eq(GroupMember::OWNER) + expect(json_response.find { |e| e['id']==reporter.id }['access_level']).to eq(GroupMember::REPORTER) + expect(json_response.find { |e| e['id']==developer.id }['access_level']).to eq(GroupMember::DEVELOPER) + expect(json_response.find { |e| e['id']==master.id }['access_level']).to eq(GroupMember::MASTER) + expect(json_response.find { |e| e['id']==guest.id }['access_level']).to eq(GroupMember::GUEST) end end it "users not part of the group should get access error" do get api("/groups/#{group_with_members.id}/members", stranger) - response.status.should == 403 + expect(response.status).to eq(403) end end end @@ -53,7 +53,7 @@ describe API::API, api: true do context "when not a member of the group" do it "should not add guest as member of group_no_members when adding being done by person outside the group" do post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER - response.status.should == 403 + expect(response.status).to eq(403) end end @@ -66,9 +66,9 @@ describe API::API, api: true do user_id: new_user.id, access_level: GroupMember::MASTER }.to change { group_no_members.members.count }.by(1) - response.status.should == 201 - json_response['name'].should == new_user.name - json_response['access_level'].should == GroupMember::MASTER + expect(response.status).to eq(201) + expect(json_response['name']).to eq(new_user.name) + expect(json_response['access_level']).to eq(GroupMember::MASTER) end it "should not allow guest to modify group members" do @@ -79,27 +79,27 @@ describe API::API, api: true do user_id: new_user.id, access_level: GroupMember::MASTER }.not_to change { group_with_members.members.count } - response.status.should == 403 + expect(response.status).to eq(403) end it "should return error if member already exists" do post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER - response.status.should == 409 + expect(response.status).to eq(409) end it "should return a 400 error when user id is not given" do post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 400 error when access level is not given" do post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error when access level is not known" do post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234 - response.status.should == 422 + expect(response.status).to eq(422) end end end @@ -109,7 +109,7 @@ describe API::API, api: true do it "should not delete guest's membership of group_with_members" do random_user = create(:user) delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user) - response.status.should == 403 + expect(response.status).to eq(403) end end @@ -119,17 +119,17 @@ describe API::API, api: true do delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner) }.to change { group_with_members.members.count }.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return a 404 error when user id is not known" do delete api("/groups/#{group_with_members.id}/members/1328", owner) - response.status.should == 404 + expect(response.status).to eq(404) end it "should not allow guest to modify group members" do delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest) - response.status.should == 403 + expect(response.status).to eq(403) end end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 8465d765294..d963dbac9f1 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -18,26 +18,26 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/groups") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when authenticated as user" do it "normal user: should return an array of groups of user1" do get api("/groups", user1) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['name'].should == group1.name + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['name']).to eq(group1.name) end end context "when authenticated as admin" do it "admin: should return an array of all groups" do get api("/groups", admin) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 2 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) end end end @@ -46,49 +46,49 @@ describe API::API, api: true do context "when authenticated as user" do it "should return one of user1's groups" do get api("/groups/#{group1.id}", user1) - response.status.should == 200 + expect(response.status).to eq(200) json_response['name'] == group1.name end it "should not return a non existing group" do get api("/groups/1328", user1) - response.status.should == 404 + expect(response.status).to eq(404) end it "should not return a group not attached to user1" do get api("/groups/#{group2.id}", user1) - response.status.should == 403 + expect(response.status).to eq(403) end end context "when authenticated as admin" do it "should return any existing group" do get api("/groups/#{group2.id}", admin) - response.status.should == 200 + expect(response.status).to eq(200) json_response['name'] == group2.name end it "should not return a non existing group" do get api("/groups/1328", admin) - response.status.should == 404 + expect(response.status).to eq(404) end end context 'when using group path in URL' do it 'should return any existing group' do get api("/groups/#{group1.path}", admin) - response.status.should == 200 + expect(response.status).to eq(200) json_response['name'] == group2.name end it 'should not return a non existing group' do get api('/groups/unknown', admin) - response.status.should == 404 + expect(response.status).to eq(404) end it 'should not return a group not attached to user1' do get api("/groups/#{group2.path}", user1) - response.status.should == 403 + expect(response.status).to eq(403) end end end @@ -97,30 +97,30 @@ describe API::API, api: true do context "when authenticated as user" do it "should not create group" do post api("/groups", user1), attributes_for(:group) - response.status.should == 403 + expect(response.status).to eq(403) end end context "when authenticated as admin" do it "should create group" do post api("/groups", admin), attributes_for(:group) - response.status.should == 201 + expect(response.status).to eq(201) end it "should not create group, duplicate" do post api("/groups", admin), {name: "Duplicate Test", path: group2.path} - response.status.should == 400 - response.message.should == "Bad Request" + expect(response.status).to eq(400) + expect(response.message).to eq("Bad Request") end it "should return 400 bad request error if name not given" do post api("/groups", admin), {path: group2.path} - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 bad request error if path not given" do post api("/groups", admin), { name: 'test' } - response.status.should == 400 + expect(response.status).to eq(400) end end end @@ -129,36 +129,36 @@ describe API::API, api: true do context "when authenticated as user" do it "should remove group" do delete api("/groups/#{group1.id}", user1) - response.status.should == 200 + expect(response.status).to eq(200) end it "should not remove a group if not an owner" do user3 = create(:user) group1.add_user(user3, Gitlab::Access::MASTER) delete api("/groups/#{group1.id}", user3) - response.status.should == 403 + expect(response.status).to eq(403) end it "should not remove a non existing group" do delete api("/groups/1328", user1) - response.status.should == 404 + expect(response.status).to eq(404) end it "should not remove a group not attached to user1" do delete api("/groups/#{group2.id}", user1) - response.status.should == 403 + expect(response.status).to eq(403) end end context "when authenticated as admin" do it "should remove any existing group" do delete api("/groups/#{group2.id}", admin) - response.status.should == 200 + expect(response.status).to eq(200) end it "should not remove a non existing group" do delete api("/groups/1328", admin) - response.status.should == 404 + expect(response.status).to eq(404) end end end @@ -167,20 +167,20 @@ describe API::API, api: true do let(:project) { create(:project) } before(:each) do Projects::TransferService.any_instance.stub(execute: true) - Project.stub(:find).and_return(project) + allow(Project).to receive(:find).and_return(project) end context "when authenticated as user" do it "should not transfer project to group" do post api("/groups/#{group1.id}/projects/#{project.id}", user2) - response.status.should == 403 + expect(response.status).to eq(403) end end context "when authenticated as admin" do it "should transfer project to group" do post api("/groups/#{group1.id}/projects/#{project.id}", admin) - response.status.should == 201 + expect(response.status).to eq(201) end end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 1e8e9eb38d6..10b467d85fd 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -11,8 +11,8 @@ describe API::API, api: true do it do get api("/internal/check"), secret_token: secret_token - response.status.should == 200 - json_response['api_version'].should == API::API.version + expect(response.status).to eq(200) + expect(json_response['api_version']).to eq(API::API.version) end end @@ -23,8 +23,8 @@ describe API::API, api: true do it do get api("/internal/broadcast_message"), secret_token: secret_token - response.status.should == 200 - json_response["message"].should == broadcast_message.message + expect(response.status).to eq(200) + expect(json_response["message"]).to eq(broadcast_message.message) end end @@ -32,7 +32,7 @@ describe API::API, api: true do it do get api("/internal/broadcast_message"), secret_token: secret_token - response.status.should == 404 + expect(response.status).to eq(404) end end end @@ -41,9 +41,9 @@ describe API::API, api: true do it do get(api("/internal/discover"), key_id: key.id, secret_token: secret_token) - response.status.should == 200 + expect(response.status).to eq(200) - json_response['name'].should == user.name + expect(json_response['name']).to eq(user.name) end end @@ -57,8 +57,8 @@ describe API::API, api: true do it do pull(key, project) - response.status.should == 200 - json_response["status"].should be_true + expect(response.status).to eq(200) + expect(json_response["status"]).to be_truthy end end @@ -66,8 +66,8 @@ describe API::API, api: true do it do push(key, project) - response.status.should == 200 - json_response["status"].should be_true + expect(response.status).to eq(200) + expect(json_response["status"]).to be_truthy end end end @@ -81,8 +81,8 @@ describe API::API, api: true do it do pull(key, project) - response.status.should == 200 - json_response["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end @@ -90,8 +90,8 @@ describe API::API, api: true do it do push(key, project) - response.status.should == 200 - json_response["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end @@ -107,8 +107,8 @@ describe API::API, api: true do it do pull(key, personal_project) - response.status.should == 200 - json_response["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end @@ -116,8 +116,8 @@ describe API::API, api: true do it do push(key, personal_project) - response.status.should == 200 - json_response["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end @@ -134,8 +134,8 @@ describe API::API, api: true do it do pull(key, project) - response.status.should == 200 - json_response["status"].should be_true + expect(response.status).to eq(200) + expect(json_response["status"]).to be_truthy end end @@ -143,8 +143,8 @@ describe API::API, api: true do it do push(key, project) - response.status.should == 200 - json_response["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end @@ -160,8 +160,8 @@ describe API::API, api: true do it do archive(key, project) - response.status.should == 200 - json_response["status"].should be_true + expect(response.status).to eq(200) + expect(json_response["status"]).to be_truthy end end @@ -169,8 +169,8 @@ describe API::API, api: true do it do archive(key, project) - response.status.should == 200 - json_response["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end @@ -179,8 +179,8 @@ describe API::API, api: true do it do pull(key, OpenStruct.new(path_with_namespace: 'gitlab/notexists')) - response.status.should == 200 - json_response["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end @@ -188,8 +188,8 @@ describe API::API, api: true do it do pull(OpenStruct.new(id: 0), project) - response.status.should == 200 - json_response["status"].should be_false + expect(response.status).to eq(200) + expect(json_response["status"]).to be_falsey end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 775d7b4e18d..b6b0427debf 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -34,86 +34,87 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/issues") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when authenticated" do it "should return an array of issues" do get api("/issues", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == issue.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(issue.title) end it "should add pagination headers" do get api("/issues?per_page=3", user) - response.headers['Link'].should == + expect(response.headers['Link']).to eq( '; rel="first", ; rel="last"' + ) end it 'should return an array of closed issues' do get api('/issues?state=closed', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['id'].should == closed_issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) end it 'should return an array of opened issues' do get api('/issues?state=opened', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['id'].should == issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) end it 'should return an array of all issues' do get api('/issues?state=all', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 2 - json_response.first['id'].should == issue.id - json_response.second['id'].should == closed_issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) end it 'should return an array of labeled issues' do get api("/issues?labels=#{label.title}", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) end it 'should return an array of labeled issues when at least one label matches' do get api("/issues?labels=#{label.title},foo,bar", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) end it 'should return an empty array if no issue matches labels' do get api('/issues?labels=foo,bar', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'should return an array of labeled issues matching given state' do get api("/issues?labels=#{label.title}&state=opened", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] - json_response.first['state'].should == 'opened' + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['state']).to eq('opened') end it 'should return an empty array if no issue matches labels and state filters' do get api("/issues?labels=#{label.title}&state=closed", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end end end @@ -124,78 +125,78 @@ describe API::API, api: true do it "should return project issues" do get api("#{base_url}/issues", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == issue.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(issue.title) end it 'should return an array of labeled project issues' do get api("#{base_url}/issues?labels=#{label.title}", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) end it 'should return an array of labeled project issues when at least one label matches' do get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) end it 'should return an empty array if no project issue matches labels' do get api("#{base_url}/issues?labels=foo,bar", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'should return an empty array if no issue matches milestone' do get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'should return an empty array if milestone does not exist' do get api("#{base_url}/issues?milestone=foo", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 0 + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) end it 'should return an array of issues in given milestone' do get api("#{base_url}/issues?milestone=#{title}", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 2 - json_response.first['id'].should == issue.id - json_response.second['id'].should == closed_issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) end it 'should return an array of issues matching state in milestone' do get api("#{base_url}/issues?milestone=#{milestone.title}"\ '&state=closed', user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['id'].should == closed_issue.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) end end describe "GET /projects/:id/issues/:issue_id" do it "should return a project issue by id" do get api("/projects/#{project.id}/issues/#{issue.id}", user) - response.status.should == 200 - json_response['title'].should == issue.title - json_response['iid'].should == issue.iid + expect(response.status).to eq(200) + expect(json_response['title']).to eq(issue.title) + expect(json_response['iid']).to eq(issue.iid) end it "should return 404 if issue id not found" do get api("/projects/#{project.id}/issues/54321", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -203,32 +204,32 @@ describe API::API, api: true do it "should create a new project issue" do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: 'label, label2' - response.status.should == 201 - json_response['title'].should == 'new issue' - json_response['description'].should be_nil - json_response['labels'].should == ['label', 'label2'] + expect(response.status).to eq(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['description']).to be_nil + expect(json_response['labels']).to eq(['label', 'label2']) end it "should return a 400 bad request if title not given" do post api("/projects/#{project.id}/issues", user), labels: 'label, label2' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 on invalid label names' do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end it 'should return 400 if title is too long' do post api("/projects/#{project.id}/issues", user), title: 'g' * 256 - response.status.should == 400 - json_response['message']['title'].should == [ + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq([ 'is too long (maximum is 255 characters)' - ] + ]) end end @@ -236,23 +237,23 @@ describe API::API, api: true do it "should update a project issue" do put api("/projects/#{project.id}/issues/#{issue.id}", user), title: 'updated title' - response.status.should == 200 + expect(response.status).to eq(200) - json_response['title'].should == 'updated title' + expect(json_response['title']).to eq('updated title') end it "should return 404 error if issue id not found" do put api("/projects/#{project.id}/issues/44444", user), title: 'updated title' - response.status.should == 404 + expect(response.status).to eq(404) end it 'should return 400 on invalid label names' do put api("/projects/#{project.id}/issues/#{issue.id}", user), title: 'updated title', labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end end @@ -263,49 +264,49 @@ describe API::API, api: true do it 'should not update labels if not present' do put api("/projects/#{project.id}/issues/#{issue.id}", user), title: 'updated title' - response.status.should == 200 - json_response['labels'].should == [label.title] + expect(response.status).to eq(200) + expect(json_response['labels']).to eq([label.title]) end it 'should remove all labels' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' - response.status.should == 200 - json_response['labels'].should == [] + expect(response.status).to eq(200) + expect(json_response['labels']).to eq([]) end it 'should update labels' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'foo,bar' - response.status.should == 200 - json_response['labels'].should include 'foo' - json_response['labels'].should include 'bar' + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'foo' + expect(json_response['labels']).to include 'bar' end it 'should return 400 on invalid label names' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end it 'should allow special label names' do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'label:foo, label-bar,label_bar,label/bar' - response.status.should == 200 - json_response['labels'].should include 'label:foo' - json_response['labels'].should include 'label-bar' - json_response['labels'].should include 'label_bar' - json_response['labels'].should include 'label/bar' + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label:foo' + expect(json_response['labels']).to include 'label-bar' + expect(json_response['labels']).to include 'label_bar' + expect(json_response['labels']).to include 'label/bar' end it 'should return 400 if title is too long' do put api("/projects/#{project.id}/issues/#{issue.id}", user), title: 'g' * 256 - response.status.should == 400 - json_response['message']['title'].should == [ + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq([ 'is too long (maximum is 255 characters)' - ] + ]) end end @@ -313,17 +314,17 @@ describe API::API, api: true do it "should update a project issue" do put api("/projects/#{project.id}/issues/#{issue.id}", user), labels: 'label2', state_event: "close" - response.status.should == 200 + expect(response.status).to eq(200) - json_response['labels'].should include 'label2' - json_response['state'].should eq "closed" + expect(json_response['labels']).to include 'label2' + expect(json_response['state']).to eq "closed" end end describe "DELETE /projects/:id/issues/:issue_id" do it "should delete a project issue" do delete api("/projects/#{project.id}/issues/#{issue.id}", user) - response.status.should == 405 + expect(response.status).to eq(405) end end end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index dbddc8a7da4..aff109a9424 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -15,10 +15,10 @@ describe API::API, api: true do describe 'GET /projects/:id/labels' do it 'should return project labels' do get api("/projects/#{project.id}/labels", user) - response.status.should == 200 - json_response.should be_an Array - json_response.size.should == 1 - json_response.first['name'].should == label1.name + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['name']).to eq(label1.name) end end @@ -27,69 +27,69 @@ describe API::API, api: true do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAABB' - response.status.should == 201 - json_response['name'].should == 'Foo' - json_response['color'].should == '#FFAABB' + expect(response.status).to eq(201) + expect(json_response['name']).to eq('Foo') + expect(json_response['color']).to eq('#FFAABB') end it 'should return a 400 bad request if name not given' do post api("/projects/#{project.id}/labels", user), color: '#FFAABB' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return a 400 bad request if color not given' do post api("/projects/#{project.id}/labels", user), name: 'Foobar' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 for invalid color' do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAA' - response.status.should == 400 - json_response['message']['color'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['color']).to eq(['is invalid']) end it 'should return 400 for too long color code' do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAAFFFF' - response.status.should == 400 - json_response['message']['color'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['color']).to eq(['is invalid']) end it 'should return 400 for invalid name' do post api("/projects/#{project.id}/labels", user), name: '?', color: '#FFAABB' - response.status.should == 400 - json_response['message']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq(['is invalid']) end it 'should return 409 if label already exists' do post api("/projects/#{project.id}/labels", user), name: 'label1', color: '#FFAABB' - response.status.should == 409 - json_response['message'].should == 'Label already exists' + expect(response.status).to eq(409) + expect(json_response['message']).to eq('Label already exists') end end describe 'DELETE /projects/:id/labels' do it 'should return 200 for existing label' do delete api("/projects/#{project.id}/labels", user), name: 'label1' - response.status.should == 200 + expect(response.status).to eq(200) end it 'should return 404 for non existing label' do delete api("/projects/#{project.id}/labels", user), name: 'label2' - response.status.should == 404 - json_response['message'].should == '404 Label Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Label Not Found') end it 'should return 400 for wrong parameters' do delete api("/projects/#{project.id}/labels", user) - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -99,47 +99,47 @@ describe API::API, api: true do name: 'label1', new_name: 'New Label', color: '#FFFFFF' - response.status.should == 200 - json_response['name'].should == 'New Label' - json_response['color'].should == '#FFFFFF' + expect(response.status).to eq(200) + expect(json_response['name']).to eq('New Label') + expect(json_response['color']).to eq('#FFFFFF') end it 'should return 200 if name is changed' do put api("/projects/#{project.id}/labels", user), name: 'label1', new_name: 'New Label' - response.status.should == 200 - json_response['name'].should == 'New Label' - json_response['color'].should == label1.color + expect(response.status).to eq(200) + expect(json_response['name']).to eq('New Label') + expect(json_response['color']).to eq(label1.color) end it 'should return 200 if colors is changed' do put api("/projects/#{project.id}/labels", user), name: 'label1', color: '#FFFFFF' - response.status.should == 200 - json_response['name'].should == label1.name - json_response['color'].should == '#FFFFFF' + expect(response.status).to eq(200) + expect(json_response['name']).to eq(label1.name) + expect(json_response['color']).to eq('#FFFFFF') end it 'should return 404 if label does not exist' do put api("/projects/#{project.id}/labels", user), name: 'label2', new_name: 'label3' - response.status.should == 404 + expect(response.status).to eq(404) end it 'should return 400 if no label name given' do put api("/projects/#{project.id}/labels", user), new_name: 'label2' - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "name" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "name" not given') end it 'should return 400 if no new parameters given' do put api("/projects/#{project.id}/labels", user), name: 'label1' - response.status.should == 400 - json_response['message'].should == 'Required parameters '\ - '"new_name" or "color" missing' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Required parameters '\ + '"new_name" or "color" missing') end it 'should return 400 for invalid name' do @@ -147,24 +147,24 @@ describe API::API, api: true do name: 'label1', new_name: '?', color: '#FFFFFF' - response.status.should == 400 - json_response['message']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq(['is invalid']) end it 'should return 400 for invalid name' do put api("/projects/#{project.id}/labels", user), name: 'label1', color: '#FF' - response.status.should == 400 - json_response['message']['color'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['color']).to eq(['is invalid']) end it 'should return 400 for too long color code' do post api("/projects/#{project.id}/labels", user), name: 'Foo', color: '#FFAAFFFF' - response.status.should == 400 - json_response['message']['color'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['color']).to eq(['is invalid']) end end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index b5deb072cd1..9e252441a4f 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -16,50 +16,50 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/projects/#{project.id}/merge_requests") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when authenticated" do it "should return an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.last['title'].should == merge_request.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['title']).to eq(merge_request.title) end it "should return an array of all merge_requests" do get api("/projects/#{project.id}/merge_requests?state", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.last['title'].should == merge_request.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['title']).to eq(merge_request.title) end it "should return an array of open merge_requests" do get api("/projects/#{project.id}/merge_requests?state=opened", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.last['title'].should == merge_request.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.last['title']).to eq(merge_request.title) end it "should return an array of closed merge_requests" do get api("/projects/#{project.id}/merge_requests?state=closed", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 2 - json_response.second['title'].should == merge_request_closed.title - json_response.first['title'].should == merge_request_merged.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.second['title']).to eq(merge_request_closed.title) + expect(json_response.first['title']).to eq(merge_request_merged.title) end it "should return an array of merged merge_requests" do get api("/projects/#{project.id}/merge_requests?state=merged", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['title'].should == merge_request_merged.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(merge_request_merged.title) end context "with ordering" do @@ -70,38 +70,38 @@ describe API::API, api: true do it "should return an array of merge_requests in ascending order" do get api("/projects/#{project.id}/merge_requests?sort=asc", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.last['id'].should == @mr_earlier.id - json_response.first['id'].should == @mr_later.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['id']).to eq(@mr_earlier.id) + expect(json_response.first['id']).to eq(@mr_later.id) end it "should return an array of merge_requests in descending order" do get api("/projects/#{project.id}/merge_requests?sort=desc", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.first['id'].should == @mr_later.id - json_response.last['id'].should == @mr_earlier.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['id']).to eq(@mr_later.id) + expect(json_response.last['id']).to eq(@mr_earlier.id) end it "should return an array of merge_requests ordered by updated_at" do get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.last['id'].should == @mr_earlier.id - json_response.first['id'].should == @mr_later.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['id']).to eq(@mr_earlier.id) + expect(json_response.first['id']).to eq(@mr_later.id) end it "should return an array of merge_requests ordered by created_at" do get api("/projects/#{project.id}/merge_requests?sort=created_at", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 3 - json_response.last['id'].should == @mr_earlier.id - json_response.first['id'].should == @mr_later.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['id']).to eq(@mr_earlier.id) + expect(json_response.first['id']).to eq(@mr_later.id) end end end @@ -110,14 +110,14 @@ describe API::API, api: true do describe "GET /projects/:id/merge_request/:merge_request_id" do it "should return merge_request" do get api("/projects/#{project.id}/merge_request/#{merge_request.id}", user) - response.status.should == 200 - json_response['title'].should == merge_request.title - json_response['iid'].should == merge_request.iid + expect(response.status).to eq(200) + expect(json_response['title']).to eq(merge_request.title) + expect(json_response['iid']).to eq(merge_request.iid) end it "should return a 404 error if merge_request_id not found" do get api("/projects/#{project.id}/merge_request/999", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -143,33 +143,33 @@ describe API::API, api: true do target_branch: 'master', author: user, labels: 'label, label2' - response.status.should == 201 - json_response['title'].should == 'Test merge_request' - json_response['labels'].should == ['label', 'label2'] + expect(response.status).to eq(201) + expect(json_response['title']).to eq('Test merge_request') + expect(json_response['labels']).to eq(['label', 'label2']) end it "should return 422 when source_branch equals target_branch" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", source_branch: "master", target_branch: "master", author: user - response.status.should == 422 + expect(response.status).to eq(422) end it "should return 400 when source_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", target_branch: "master", author: user - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 when target_branch is missing" do post api("/projects/#{project.id}/merge_requests", user), title: "Test merge_request", source_branch: "stable", author: user - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 when title is missing" do post api("/projects/#{project.id}/merge_requests", user), target_branch: 'master', source_branch: 'stable' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 on invalid label names' do @@ -179,9 +179,10 @@ describe API::API, api: true do target_branch: 'master', author: user, labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq( ['is invalid'] + ) end context 'with existing MR' do @@ -202,7 +203,7 @@ describe API::API, api: true do target_branch: 'master', author: user end.to change { MergeRequest.count }.by(0) - response.status.should == 409 + expect(response.status).to eq(409) end end end @@ -219,37 +220,37 @@ describe API::API, api: true do it "should return merge_request" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "stable", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' - response.status.should == 201 - json_response['title'].should == 'Test merge_request' - json_response['description'].should == 'Test description for Test merge_request' + expect(response.status).to eq(201) + expect(json_response['title']).to eq('Test merge_request') + expect(json_response['description']).to eq('Test description for Test merge_request') end it "should not return 422 when source_branch equals target_branch" do - project.id.should_not == fork_project.id - fork_project.forked?.should be_true - fork_project.forked_from_project.should == project + expect(project.id).not_to eq(fork_project.id) + expect(fork_project.forked?).to be_truthy + expect(fork_project.forked_from_project).to eq(project) post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id - response.status.should == 201 - json_response['title'].should == 'Test merge_request' + expect(response.status).to eq(201) + expect(json_response['title']).to eq('Test merge_request') end it "should return 400 when source_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 when target_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 when title is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'stable', author: user2, target_project_id: project.id - response.status.should == 400 + expect(response.status).to eq(400) end context 'when target_branch is specified' do @@ -260,7 +261,7 @@ describe API::API, api: true do source_branch: 'stable', author: user, target_project_id: fork_project.id - response.status.should == 422 + expect(response.status).to eq(422) end it 'should return 422 if targeting a different fork' do @@ -270,14 +271,14 @@ describe API::API, api: true do source_branch: 'stable', author: user2, target_project_id: unrelated_project.id - response.status.should == 422 + expect(response.status).to eq(422) end end it "should return 201 when target_branch is specified and for the same project" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'stable', author: user2, target_project_id: fork_project.id - response.status.should == 201 + expect(response.status).to eq(201) end end end @@ -285,8 +286,8 @@ describe API::API, api: true do describe "PUT /projects/:id/merge_request/:merge_request_id to close MR" do it "should return merge_request" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), state_event: "close" - response.status.should == 200 - json_response['state'].should == 'closed' + expect(response.status).to eq(200) + expect(json_response['state']).to eq('closed') end end @@ -294,55 +295,55 @@ describe API::API, api: true do it "should return merge_request in case of success" do MergeRequest.any_instance.stub(can_be_merged?: true, automerge!: true) put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return 405 if branch can't be merged" do MergeRequest.any_instance.stub(can_be_merged?: false) put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) - response.status.should == 405 - json_response['message'].should == 'Branch cannot be merged' + expect(response.status).to eq(405) + expect(json_response['message']).to eq('Branch cannot be merged') end it "should return 405 if merge_request is not open" do merge_request.close put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) - response.status.should == 405 - json_response['message'].should == '405 Method Not Allowed' + expect(response.status).to eq(405) + expect(json_response['message']).to eq('405 Method Not Allowed') end it "should return 401 if user has no permissions to merge" do user2 = create(:user) project.team << [user2, :reporter] put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user2) - response.status.should == 401 - json_response['message'].should == '401 Unauthorized' + expect(response.status).to eq(401) + expect(json_response['message']).to eq('401 Unauthorized') end end describe "PUT /projects/:id/merge_request/:merge_request_id" do it "should return merge_request" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), title: "New title" - response.status.should == 200 - json_response['title'].should == 'New title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('New title') end it "should return merge_request" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), description: "New description" - response.status.should == 200 - json_response['description'].should == 'New description' + expect(response.status).to eq(200) + expect(json_response['description']).to eq('New description') end it "should return 422 when source_branch and target_branch are renamed the same" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), source_branch: "master", target_branch: "master" - response.status.should == 422 + expect(response.status).to eq(422) end it "should return merge_request with renamed target_branch" do put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), target_branch: "wiki" - response.status.should == 200 - json_response['target_branch'].should == 'wiki' + expect(response.status).to eq(200) + expect(json_response['target_branch']).to eq('wiki') end it 'should return 400 on invalid label names' do @@ -350,43 +351,43 @@ describe API::API, api: true do user), title: 'new issue', labels: 'label, ?' - response.status.should == 400 - json_response['message']['labels']['?']['title'].should == ['is invalid'] + expect(response.status).to eq(400) + expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end end describe "POST /projects/:id/merge_request/:merge_request_id/comments" do it "should return comment" do post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user), note: "My comment" - response.status.should == 201 - json_response['note'].should == 'My comment' + expect(response.status).to eq(201) + expect(json_response['note']).to eq('My comment') end it "should return 400 if note is missing" do post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 404 if note is attached to non existent merge request" do post api("/projects/#{project.id}/merge_request/404/comments", user), note: 'My comment' - response.status.should == 404 + expect(response.status).to eq(404) end end describe "GET :id/merge_request/:merge_request_id/comments" do it "should return merge_request comments" do get api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) - response.status.should == 200 - json_response.should be_an Array - json_response.length.should == 1 - json_response.first['note'].should == "a comment on a MR" - json_response.first['author']['id'].should == user.id + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['note']).to eq("a comment on a MR") + expect(json_response.first['author']['id']).to eq(user.id) end it "should return a 404 error if merge_request_id not found" do get api("/projects/#{project.id}/merge_request/999/comments", user) - response.status.should == 404 + expect(response.status).to eq(404) end end diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 647033309bd..effb0723476 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -11,55 +11,55 @@ describe API::API, api: true do describe 'GET /projects/:id/milestones' do it 'should return project milestones' do get api("/projects/#{project.id}/milestones", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == milestone.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(milestone.title) end it 'should return a 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones") - response.status.should == 401 + expect(response.status).to eq(401) end end describe 'GET /projects/:id/milestones/:milestone_id' do it 'should return a project milestone by id' do get api("/projects/#{project.id}/milestones/#{milestone.id}", user) - response.status.should == 200 - json_response['title'].should == milestone.title - json_response['iid'].should == milestone.iid + expect(response.status).to eq(200) + expect(json_response['title']).to eq(milestone.title) + expect(json_response['iid']).to eq(milestone.iid) end it 'should return 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}") - response.status.should == 401 + expect(response.status).to eq(401) end it 'should return a 404 error if milestone id not found' do get api("/projects/#{project.id}/milestones/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end describe 'POST /projects/:id/milestones' do it 'should create a new project milestone' do post api("/projects/#{project.id}/milestones", user), title: 'new milestone' - response.status.should == 201 - json_response['title'].should == 'new milestone' - json_response['description'].should be_nil + expect(response.status).to eq(201) + expect(json_response['title']).to eq('new milestone') + expect(json_response['description']).to be_nil end it 'should create a new project milestone with description and due date' do post api("/projects/#{project.id}/milestones", user), title: 'new milestone', description: 'release', due_date: '2013-03-02' - response.status.should == 201 - json_response['description'].should == 'release' - json_response['due_date'].should == '2013-03-02' + expect(response.status).to eq(201) + expect(json_response['description']).to eq('release') + expect(json_response['due_date']).to eq('2013-03-02') end it 'should return a 400 error if title is missing' do post api("/projects/#{project.id}/milestones", user) - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -67,14 +67,14 @@ describe API::API, api: true do it 'should update a project milestone' do put api("/projects/#{project.id}/milestones/#{milestone.id}", user), title: 'updated title' - response.status.should == 200 - json_response['title'].should == 'updated title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('updated title') end it 'should return a 404 error if milestone id not found' do put api("/projects/#{project.id}/milestones/1234", user), title: 'updated title' - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -82,15 +82,15 @@ describe API::API, api: true do it 'should update a project milestone' do put api("/projects/#{project.id}/milestones/#{milestone.id}", user), state_event: 'close' - response.status.should == 200 + expect(response.status).to eq(200) - json_response['state'].should == 'closed' + expect(json_response['state']).to eq('closed') end end describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do it 'should create an activity event when an milestone is closed' do - Event.should_receive(:create) + expect(Event).to receive(:create) put api("/projects/#{project.id}/milestones/#{milestone.id}", user), state_event: 'close' @@ -103,14 +103,14 @@ describe API::API, api: true do end it 'should return project issues for a particular milestone' do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['milestone']['title'].should == milestone.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['milestone']['title']).to eq(milestone.title) end it 'should return a 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") - response.status.should == 401 + expect(response.status).to eq(401) end end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index b8943ea0762..6ddaaa0a6dd 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -10,17 +10,17 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/namespaces") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when authenticated as admin" do it "admin: should return an array of all namespaces" do get api("/namespaces", admin) - response.status.should == 200 - json_response.should be_an Array + expect(response.status).to eq(200) + expect(json_response).to be_an Array - json_response.length.should == Namespace.count + expect(json_response.length).to eq(Namespace.count) end end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 429824e829a..8b177af4689 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -16,42 +16,42 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should return an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['body'].should == issue_note.note + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(issue_note.note) end it "should return a 404 error when issue id not found" do get api("/projects/#{project.id}/issues/123/notes", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context "when noteable is a Snippet" do it "should return an array of snippet notes" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['body'].should == snippet_note.note + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(snippet_note.note) end it "should return a 404 error when snippet id not found" do get api("/projects/#{project.id}/snippets/42/notes", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context "when noteable is a Merge Request" do it "should return an array of merge_requests notes" do get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['body'].should == merge_request_note.note + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(merge_request_note.note) end it "should return a 404 error if merge request id not found" do get api("/projects/#{project.id}/merge_requests/4444/notes", user) - response.status.should == 404 + expect(response.status).to eq(404) end end end @@ -60,26 +60,26 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should return an issue note by id" do get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) - response.status.should == 200 - json_response['body'].should == issue_note.note + expect(response.status).to eq(200) + expect(json_response['body']).to eq(issue_note.note) end it "should return a 404 error if issue note not found" do get api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context "when noteable is a Snippet" do it "should return a snippet note by id" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) - response.status.should == 200 - json_response['body'].should == snippet_note.note + expect(response.status).to eq(200) + expect(json_response['body']).to eq(snippet_note.note) end it "should return a 404 error if snippet note not found" do get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/123", user) - response.status.should == 404 + expect(response.status).to eq(404) end end end @@ -88,45 +88,45 @@ describe API::API, api: true do context "when noteable is an Issue" do it "should create a new issue note" do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' - response.status.should == 201 - json_response['body'].should == 'hi!' - json_response['author']['username'].should == user.username + expect(response.status).to eq(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) end it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 401 unauthorized error if user not authenticated" do post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' - response.status.should == 401 + expect(response.status).to eq(401) end end context "when noteable is a Snippet" do it "should create a new snippet note" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' - response.status.should == 201 - json_response['body'].should == 'hi!' - json_response['author']['username'].should == user.username + expect(response.status).to eq(201) + expect(json_response['body']).to eq('hi!') + expect(json_response['author']['username']).to eq(user.username) end it "should return a 400 bad request error if body not given" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 401 unauthorized error if user not authenticated" do post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!' - response.status.should == 401 + expect(response.status).to eq(401) end end end describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do it "should create an activity event when an issue note is created" do - Event.should_receive(:create) + expect(Event).to receive(:create) post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' end @@ -137,20 +137,20 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user), body: 'Hello!' - response.status.should == 200 - json_response['body'].should == 'Hello!' + expect(response.status).to eq(200) + expect(json_response['body']).to eq('Hello!') end it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user), body: 'Hello!' - response.status.should == 404 + expect(response.status).to eq(404) end it 'should return a 400 bad request error if body not given' do put api("/projects/#{project.id}/issues/#{issue.id}/"\ "notes/#{issue_note.id}", user) - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -158,14 +158,14 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/#{snippet_note.id}", user), body: 'Hello!' - response.status.should == 200 - json_response['body'].should == 'Hello!' + expect(response.status).to eq(200) + expect(json_response['body']).to eq('Hello!') end it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ "notes/123", user), body: "Hello!" - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -173,14 +173,14 @@ describe API::API, api: true do it 'should return modified note' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ "notes/#{merge_request_note.id}", user), body: 'Hello!' - response.status.should == 200 - json_response['body'].should == 'Hello!' + expect(response.status).to eq(200) + expect(json_response['body']).to eq('Hello!') end it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ "notes/123", user), body: "Hello!" - response.status.should == 404 + expect(response.status).to eq(404) end end end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index cdb5e3d0612..81fe68de662 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -16,18 +16,18 @@ describe API::API, 'ProjectHooks', api: true do context "authorized user" do it "should return project hooks" do get api("/projects/#{project.id}/hooks", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response.should be_an Array - json_response.count.should == 1 - json_response.first['url'].should == "http://example.com" + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['url']).to eq("http://example.com") end end context "unauthorized user" do it "should not access project hooks" do get api("/projects/#{project.id}/hooks", user3) - response.status.should == 403 + expect(response.status).to eq(403) end end end @@ -36,26 +36,26 @@ describe API::API, 'ProjectHooks', api: true do context "authorized user" do it "should return a project hook" do get api("/projects/#{project.id}/hooks/#{hook.id}", user) - response.status.should == 200 - json_response['url'].should == hook.url + expect(response.status).to eq(200) + expect(json_response['url']).to eq(hook.url) end it "should return a 404 error if hook id is not available" do get api("/projects/#{project.id}/hooks/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end context "unauthorized user" do it "should not access an existing hook" do get api("/projects/#{project.id}/hooks/#{hook.id}", user3) - response.status.should == 403 + expect(response.status).to eq(403) end end it "should return a 404 error if hook id is not available" do get api("/projects/#{project.id}/hooks/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -65,17 +65,17 @@ describe API::API, 'ProjectHooks', api: true do post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true }.to change {project.hooks.count}.by(1) - response.status.should == 201 + expect(response.status).to eq(201) end it "should return a 400 error if url not given" do post api("/projects/#{project.id}/hooks", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error if url not valid" do post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" - response.status.should == 422 + expect(response.status).to eq(422) end end @@ -83,23 +83,23 @@ describe API::API, 'ProjectHooks', api: true do it "should update an existing project hook" do put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'http://example.org', push_events: false - response.status.should == 200 - json_response['url'].should == 'http://example.org' + expect(response.status).to eq(200) + expect(json_response['url']).to eq('http://example.org') end it "should return 404 error if hook id not found" do put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org' - response.status.should == 404 + expect(response.status).to eq(404) end it "should return 400 error if url is not given" do put api("/projects/#{project.id}/hooks/#{hook.id}", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error if url is not valid" do put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com' - response.status.should == 422 + expect(response.status).to eq(422) end end @@ -108,22 +108,22 @@ describe API::API, 'ProjectHooks', api: true do expect { delete api("/projects/#{project.id}/hooks/#{hook.id}", user) }.to change {project.hooks.count}.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return success when deleting hook" do delete api("/projects/#{project.id}/hooks/#{hook.id}", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return success when deleting non existent hook" do delete api("/projects/#{project.id}/hooks/42", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return a 405 error if hook id not given" do delete api("/projects/#{project.id}/hooks", user) - response.status.should == 405 + expect(response.status).to eq(405) end end end diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb index 836f21f3e0b..8419a364ed1 100644 --- a/spec/requests/api/project_members_spec.rb +++ b/spec/requests/api/project_members_spec.rb @@ -15,23 +15,23 @@ describe API::API, api: true do it "should return project team members" do get api("/projects/#{project.id}/members", user) - response.status.should == 200 - json_response.should be_an Array - json_response.count.should == 2 - json_response.map { |u| u['username'] }.should include user.username + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.count).to eq(2) + expect(json_response.map { |u| u['username'] }).to include user.username end it "finds team members with query string" do get api("/projects/#{project.id}/members", user), query: user.username - response.status.should == 200 - json_response.should be_an Array - json_response.count.should == 1 - json_response.first['username'].should == user.username + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.count).to eq(1) + expect(json_response.first['username']).to eq(user.username) end it "should return a 404 error if id not found" do get api("/projects/9999/members", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -40,14 +40,14 @@ describe API::API, api: true do it "should return project team member" do get api("/projects/#{project.id}/members/#{user.id}", user) - response.status.should == 200 - json_response['username'].should == user.username - json_response['access_level'].should == ProjectMember::MASTER + expect(response.status).to eq(200) + expect(json_response['username']).to eq(user.username) + expect(json_response['access_level']).to eq(ProjectMember::MASTER) end it "should return a 404 error if user id not found" do get api("/projects/#{project.id}/members/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -58,9 +58,9 @@ describe API::API, api: true do access_level: ProjectMember::DEVELOPER }.to change { ProjectMember.count }.by(1) - response.status.should == 201 - json_response['username'].should == user2.username - json_response['access_level'].should == ProjectMember::DEVELOPER + expect(response.status).to eq(201) + expect(json_response['username']).to eq(user2.username) + expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER) end it "should return a 201 status if user is already project member" do @@ -69,26 +69,26 @@ describe API::API, api: true do expect { post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER - }.not_to change { ProjectMember.count }.by(1) + }.not_to change { ProjectMember.count } - response.status.should == 201 - json_response['username'].should == user2.username - json_response['access_level'].should == ProjectMember::DEVELOPER + expect(response.status).to eq(201) + expect(json_response['username']).to eq(user2.username) + expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER) end it "should return a 400 error when user id is not given" do post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 400 error when access level is not given" do post api("/projects/#{project.id}/members", user), user_id: user2.id - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error when access level is not known" do post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234 - response.status.should == 422 + expect(response.status).to eq(422) end end @@ -97,24 +97,24 @@ describe API::API, api: true do it "should update project team member" do put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER - response.status.should == 200 - json_response['username'].should == user3.username - json_response['access_level'].should == ProjectMember::MASTER + expect(response.status).to eq(200) + expect(json_response['username']).to eq(user3.username) + expect(json_response['access_level']).to eq(ProjectMember::MASTER) end it "should return a 404 error if user_id is not found" do put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER - response.status.should == 404 + expect(response.status).to eq(404) end it "should return a 400 error when access level is not given" do put api("/projects/#{project.id}/members/#{user3.id}", user) - response.status.should == 400 + expect(response.status).to eq(400) end it "should return a 422 error when access level is not known" do put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123 - response.status.should == 422 + expect(response.status).to eq(422) end end @@ -132,22 +132,22 @@ describe API::API, api: true do delete api("/projects/#{project.id}/members/#{user3.id}", user) expect { delete api("/projects/#{project.id}/members/#{user3.id}", user) - }.to_not change { ProjectMember.count }.by(1) + }.to_not change { ProjectMember.count } end it "should return 200 if team member already removed" do delete api("/projects/#{project.id}/members/#{user3.id}", user) delete api("/projects/#{project.id}/members/#{user3.id}", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return 200 OK when the user was not member" do expect { delete api("/projects/#{project.id}/members/1000000", user) }.to change { ProjectMember.count }.by(0) - response.status.should == 200 - json_response['message'].should == "Access revoked" - json_response['id'].should == 1000000 + expect(response.status).to eq(200) + expect(json_response['message']).to eq("Access revoked") + expect(json_response['id']).to eq(1000000) end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 65c894ac0c2..170ede57310 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -44,25 +44,25 @@ describe API::API, api: true do context 'when unauthenticated' do it 'should return authentication error' do get api('/projects') - response.status.should == 401 + expect(response.status).to eq(401) end end context 'when authenticated' do it 'should return an array of projects' do get api('/projects', user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['name'].should == project.name - json_response.first['owner']['username'].should == user.username + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project.name) + expect(json_response.first['owner']['username']).to eq(user.username) end context 'and using search' do it 'should return searched project' do get api('/projects', user), { search: project.name } - response.status.should eq(200) - json_response.should be_an Array - json_response.length.should eq(1) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) end end @@ -74,9 +74,9 @@ describe API::API, api: true do it 'should return the correct order when sorted by id' do get api('/projects', user), { order_by: 'id', sort: 'desc'} - response.status.should eq(200) - json_response.should be_an Array - json_response.first['id'].should eq(project3.id) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(project3.id) end end end @@ -88,31 +88,31 @@ describe API::API, api: true do context 'when unauthenticated' do it 'should return authentication error' do get api('/projects/all') - response.status.should == 401 + expect(response.status).to eq(401) end end context 'when authenticated as regular user' do it 'should return authentication error' do get api('/projects/all', user) - response.status.should == 403 + expect(response.status).to eq(403) end end context 'when authenticated as admin' do it 'should return an array of all projects' do get api('/projects/all', admin) - response.status.should == 200 - json_response.should be_an Array + expect(response.status).to eq(200) + expect(json_response).to be_an Array project_name = project.name - json_response.detect { + expect(json_response.detect { |project| project['name'] == project_name - }['name'].should == project_name + }['name']).to eq(project_name) - json_response.detect { + expect(json_response.detect { |project| project['owner']['username'] == user.username - }['owner']['username'].should == user.username + }['owner']['username']).to eq(user.username) end end end @@ -120,29 +120,29 @@ describe API::API, api: true do describe 'POST /projects' do context 'maximum number of projects reached' do it 'should not create new project and respond with 403' do - User.any_instance.stub(:projects_limit_left).and_return(0) + allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) expect { post api('/projects', user2), name: 'foo' }.to change {Project.count}.by(0) - response.status.should == 403 + expect(response.status).to eq(403) end end it 'should create new project without path and return 201' do expect { post api('/projects', user), name: 'foo' }. to change { Project.count }.by(1) - response.status.should == 201 + expect(response.status).to eq(201) end it 'should create last project before reaching project limit' do - User.any_instance.stub(:projects_limit_left).and_return(1) + allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1) post api('/projects', user2), name: 'foo' - response.status.should == 201 + expect(response.status).to eq(201) end it 'should not create new project without name and return 400' do expect { post api('/projects', user) }.to_not change { Project.count } - response.status.should == 400 + expect(response.status).to eq(400) end it "should assign attributes to project" do @@ -157,50 +157,50 @@ describe API::API, api: true do post api('/projects', user), project project.each_pair do |k,v| - json_response[k.to_s].should == v + expect(json_response[k.to_s]).to eq(v) end end it 'should set a project as public' do project = attributes_for(:project, :public) post api('/projects', user), project - json_response['public'].should be_true - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end it 'should set a project as public using :public' do project = attributes_for(:project, { public: true }) post api('/projects', user), project - json_response['public'].should be_true - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end it 'should set a project as internal' do project = attributes_for(:project, :internal) post api('/projects', user), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end it 'should set a project as internal overriding :public' do project = attributes_for(:project, :internal, { public: true }) post api('/projects', user), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end it 'should set a project as private' do project = attributes_for(:project, :private) post api('/projects', user), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end it 'should set a project as private using :public' do project = attributes_for(:project, { public: false }) post api('/projects', user), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end end @@ -210,24 +210,24 @@ describe API::API, api: true do it 'should create new project without path and return 201' do expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) - response.status.should == 201 + expect(response.status).to eq(201) end it 'should respond with 400 on failure and not project' do expect { post api("/projects/user/#{user.id}", admin) }. to_not change { Project.count } - response.status.should == 400 - json_response['message']['name'].should == [ + expect(response.status).to eq(400) + expect(json_response['message']['name']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', Gitlab::Regex.project_regex_message - ] - json_response['message']['path'].should == [ + ]) + expect(json_response['message']['path']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', Gitlab::Regex.send(:default_regex_message) - ] + ]) end it 'should assign attributes to project' do @@ -242,50 +242,50 @@ describe API::API, api: true do project.each_pair do |k,v| next if k == :path - json_response[k.to_s].should == v + expect(json_response[k.to_s]).to eq(v) end end it 'should set a project as public' do project = attributes_for(:project, :public) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_true - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end it 'should set a project as public using :public' do project = attributes_for(:project, { public: true }) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_true - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) end it 'should set a project as internal' do project = attributes_for(:project, :internal) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end it 'should set a project as internal overriding :public' do project = attributes_for(:project, :internal, { public: true }) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) end it 'should set a project as private' do project = attributes_for(:project, :private) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end it 'should set a project as private using :public' do project = attributes_for(:project, { public: false }) post api("/projects/user/#{user.id}", admin), project - json_response['public'].should be_false - json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end end @@ -295,27 +295,27 @@ describe API::API, api: true do it 'should return a project by id' do get api("/projects/#{project.id}", user) - response.status.should == 200 - json_response['name'].should == project.name - json_response['owner']['username'].should == user.username + expect(response.status).to eq(200) + expect(json_response['name']).to eq(project.name) + expect(json_response['owner']['username']).to eq(user.username) end it 'should return a project by path name' do get api("/projects/#{project.id}", user) - response.status.should == 200 - json_response['name'].should == project.name + expect(response.status).to eq(200) + expect(json_response['name']).to eq(project.name) end it 'should return a 404 error if not found' do get api('/projects/42', user) - response.status.should == 404 - json_response['message'].should == '404 Project Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Project Not Found') end it 'should return a 404 error if user is not a member' do other_user = create(:user) get api("/projects/#{project.id}", other_user) - response.status.should == 404 + expect(response.status).to eq(404) end describe 'permissions' do @@ -351,24 +351,24 @@ describe API::API, api: true do it 'should return a project events' do get api("/projects/#{project.id}/events", user) - response.status.should == 200 + expect(response.status).to eq(200) json_event = json_response.first - json_event['action_name'].should == 'joined' - json_event['project_id'].to_i.should == project.id - json_event['author_username'].should == user.username + expect(json_event['action_name']).to eq('joined') + expect(json_event['project_id'].to_i).to eq(project.id) + expect(json_event['author_username']).to eq(user.username) end it 'should return a 404 error if not found' do get api('/projects/42/events', user) - response.status.should == 404 - json_response['message'].should == '404 Project Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Project Not Found') end it 'should return a 404 error if user is not a member' do other_user = create(:user) get api("/projects/#{project.id}/events", other_user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -377,22 +377,22 @@ describe API::API, api: true do it 'should return an array of project snippets' do get api("/projects/#{project.id}/snippets", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == snippet.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(snippet.title) end end describe 'GET /projects/:id/snippets/:snippet_id' do it 'should return a project snippet' do get api("/projects/#{project.id}/snippets/#{snippet.id}", user) - response.status.should == 200 - json_response['title'].should == snippet.title + expect(response.status).to eq(200) + expect(json_response['title']).to eq(snippet.title) end it 'should return a 404 error if snippet id not found' do get api("/projects/#{project.id}/snippets/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -400,8 +400,8 @@ describe API::API, api: true do it 'should create a new project snippet' do post api("/projects/#{project.id}/snippets", user), title: 'api test', file_name: 'sample.rb', code: 'test' - response.status.should == 201 - json_response['title'].should == 'api test' + expect(response.status).to eq(201) + expect(json_response['title']).to eq('api test') end it 'should return a 400 error if invalid snippet is given' do @@ -414,16 +414,16 @@ describe API::API, api: true do it 'should update an existing project snippet' do put api("/projects/#{project.id}/snippets/#{snippet.id}", user), code: 'updated code' - response.status.should == 200 - json_response['title'].should == 'example' - snippet.reload.content.should == 'updated code' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('example') + expect(snippet.reload.content).to eq('updated code') end it 'should update an existing project snippet with new title' do put api("/projects/#{project.id}/snippets/#{snippet.id}", user), title: 'other api test' - response.status.should == 200 - json_response['title'].should == 'other api test' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('other api test') end end @@ -434,24 +434,24 @@ describe API::API, api: true do expect { delete api("/projects/#{project.id}/snippets/#{snippet.id}", user) }.to change { Snippet.count }.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it 'should return 404 when deleting unknown snippet id' do delete api("/projects/#{project.id}/snippets/1234", user) - response.status.should == 404 + expect(response.status).to eq(404) end end describe 'GET /projects/:id/snippets/:snippet_id/raw' do it 'should get a raw project snippet' do get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) - response.status.should == 200 + expect(response.status).to eq(200) end it 'should return a 404 error if raw project snippet not found' do get api("/projects/#{project.id}/snippets/5555/raw", user) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -464,43 +464,43 @@ describe API::API, api: true do it 'should return array of ssh keys' do get api("/projects/#{project.id}/keys", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == deploy_key.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(deploy_key.title) end end describe 'GET /projects/:id/keys/:key_id' do it 'should return a single key' do get api("/projects/#{project.id}/keys/#{deploy_key.id}", user) - response.status.should == 200 - json_response['title'].should == deploy_key.title + expect(response.status).to eq(200) + expect(json_response['title']).to eq(deploy_key.title) end it 'should return 404 Not Found with invalid ID' do get api("/projects/#{project.id}/keys/404", user) - response.status.should == 404 + expect(response.status).to eq(404) end end describe 'POST /projects/:id/keys' do it 'should not create an invalid ssh key' do post api("/projects/#{project.id}/keys", user), { title: 'invalid key' } - response.status.should == 400 - json_response['message']['key'].should == [ + expect(response.status).to eq(400) + expect(json_response['message']['key']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', 'is invalid' - ] + ]) end it 'should not create a key without title' do post api("/projects/#{project.id}/keys", user), key: 'some key' - response.status.should == 400 - json_response['message']['title'].should == [ + expect(response.status).to eq(400) + expect(json_response['message']['title']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)' - ] + ]) end it 'should create new ssh key' do @@ -522,7 +522,7 @@ describe API::API, api: true do it 'should return 404 Not Found with invalid ID' do delete api("/projects/#{project.id}/keys/404", user) - response.status.should == 404 + expect(response.status).to eq(404) end end end @@ -536,33 +536,33 @@ describe API::API, api: true do it "shouldn't available for non admin users" do post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) - response.status.should == 403 + expect(response.status).to eq(403) end it 'should allow project to be forked from an existing project' do - project_fork_target.forked?.should_not be_true + expect(project_fork_target.forked?).not_to be_truthy post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) - response.status.should == 201 + expect(response.status).to eq(201) project_fork_target.reload - project_fork_target.forked_from_project.id.should == project_fork_source.id - project_fork_target.forked_project_link.should_not be_nil - project_fork_target.forked?.should be_true + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + expect(project_fork_target.forked_project_link).not_to be_nil + expect(project_fork_target.forked?).to be_truthy end it 'should fail if forked_from project which does not exist' do post api("/projects/#{project_fork_target.id}/fork/9999", admin) - response.status.should == 404 + expect(response.status).to eq(404) end it 'should fail with 409 if already forked' do post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) project_fork_target.reload - project_fork_target.forked_from_project.id.should == project_fork_source.id + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) post api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin) - response.status.should == 409 + expect(response.status).to eq(409) project_fork_target.reload - project_fork_target.forked_from_project.id.should == project_fork_source.id - project_fork_target.forked?.should be_true + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + expect(project_fork_target.forked?).to be_truthy end end @@ -570,26 +570,26 @@ describe API::API, api: true do it "shouldn't available for non admin users" do delete api("/projects/#{project_fork_target.id}/fork", user) - response.status.should == 403 + expect(response.status).to eq(403) end it 'should make forked project unforked' do post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) project_fork_target.reload - project_fork_target.forked_from_project.should_not be_nil - project_fork_target.forked?.should be_true + expect(project_fork_target.forked_from_project).not_to be_nil + expect(project_fork_target.forked?).to be_truthy delete api("/projects/#{project_fork_target.id}/fork", admin) - response.status.should == 200 + expect(response.status).to eq(200) project_fork_target.reload - project_fork_target.forked_from_project.should be_nil - project_fork_target.forked?.should_not be_true + expect(project_fork_target.forked_from_project).to be_nil + expect(project_fork_target.forked?).not_to be_truthy end it 'should be idempotent if not forked' do - project_fork_target.forked_from_project.should be_nil + expect(project_fork_target.forked_from_project).to be_nil delete api("/projects/#{project_fork_target.id}/fork", admin) - response.status.should == 200 - project_fork_target.reload.forked_from_project.should be_nil + expect(response.status).to eq(200) + expect(project_fork_target.reload.forked_from_project).to be_nil end end end @@ -609,27 +609,27 @@ describe API::API, api: true do context 'when unauthenticated' do it 'should return authentication error' do get api("/projects/search/#{query}") - response.status.should == 401 + expect(response.status).to eq(401) end end context 'when authenticated' do it 'should return an array of projects' do get api("/projects/search/#{query}",user) - response.status.should == 200 - json_response.should be_an Array - json_response.size.should == 6 - json_response.each {|project| project['name'].should =~ /.*query.*/} + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(6) + json_response.each {|project| expect(project['name']).to match(/.*query.*/)} end end context 'when authenticated as a different user' do it 'should return matching public projects' do get api("/projects/search/#{query}", user2) - response.status.should == 200 - json_response.should be_an Array - json_response.size.should == 2 - json_response.each {|project| project['name'].should =~ /(internal|public) query/} + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + json_response.each {|project| expect(project['name']).to match(/(internal|public) query/)} end end end @@ -648,7 +648,7 @@ describe API::API, api: true do it 'should return authentication error' do project_param = { name: 'bar' } put api("/projects/#{project.id}"), project_param - response.status.should == 401 + expect(response.status).to eq(401) end end @@ -656,34 +656,34 @@ describe API::API, api: true do it 'should update name' do project_param = { name: 'bar' } put api("/projects/#{project.id}", user), project_param - response.status.should == 200 + expect(response.status).to eq(200) project_param.each_pair do |k, v| - json_response[k.to_s].should == v + expect(json_response[k.to_s]).to eq(v) end end it 'should update visibility_level' do project_param = { visibility_level: 20 } put api("/projects/#{project3.id}", user), project_param - response.status.should == 200 + expect(response.status).to eq(200) project_param.each_pair do |k, v| - json_response[k.to_s].should == v + expect(json_response[k.to_s]).to eq(v) end end it 'should not update name to existing name' do project_param = { name: project3.name } put api("/projects/#{project.id}", user), project_param - response.status.should == 400 - json_response['message']['name'].should == ['has already been taken'] + expect(response.status).to eq(400) + expect(json_response['message']['name']).to eq(['has already been taken']) end it 'should update path & name to existing path & name in different namespace' do project_param = { path: project4.path, name: project4.name } put api("/projects/#{project3.id}", user), project_param - response.status.should == 200 + expect(response.status).to eq(200) project_param.each_pair do |k, v| - json_response[k.to_s].should == v + expect(json_response[k.to_s]).to eq(v) end end end @@ -692,9 +692,9 @@ describe API::API, api: true do it 'should update path' do project_param = { path: 'bar' } put api("/projects/#{project3.id}", user4), project_param - response.status.should == 200 + expect(response.status).to eq(200) project_param.each_pair do |k, v| - json_response[k.to_s].should == v + expect(json_response[k.to_s]).to eq(v) end end @@ -706,29 +706,29 @@ describe API::API, api: true do description: 'new description' } put api("/projects/#{project3.id}", user4), project_param - response.status.should == 200 + expect(response.status).to eq(200) project_param.each_pair do |k, v| - json_response[k.to_s].should == v + expect(json_response[k.to_s]).to eq(v) end end it 'should not update path to existing path' do project_param = { path: project.path } put api("/projects/#{project3.id}", user4), project_param - response.status.should == 400 - json_response['message']['path'].should == ['has already been taken'] + expect(response.status).to eq(400) + expect(json_response['message']['path']).to eq(['has already been taken']) end it 'should not update name' do project_param = { name: 'bar' } put api("/projects/#{project3.id}", user4), project_param - response.status.should == 403 + expect(response.status).to eq(403) end it 'should not update visibility_level' do project_param = { visibility_level: 20 } put api("/projects/#{project3.id}", user4), project_param - response.status.should == 403 + expect(response.status).to eq(403) end end @@ -741,7 +741,7 @@ describe API::API, api: true do merge_requests_enabled: true, description: 'new description' } put api("/projects/#{project.id}", user3), project_param - response.status.should == 403 + expect(response.status).to eq(403) end end end @@ -755,36 +755,36 @@ describe API::API, api: true do ).twice delete api("/projects/#{project.id}", user) - response.status.should == 200 + expect(response.status).to eq(200) end it 'should not remove a project if not an owner' do user3 = create(:user) project.team << [user3, :developer] delete api("/projects/#{project.id}", user3) - response.status.should == 403 + expect(response.status).to eq(403) end it 'should not remove a non existing project' do delete api('/projects/1328', user) - response.status.should == 404 + expect(response.status).to eq(404) end it 'should not remove a project not attached to user' do delete api("/projects/#{project.id}", user2) - response.status.should == 404 + expect(response.status).to eq(404) end end context 'when authenticated as admin' do it 'should remove any existing project' do delete api("/projects/#{project.id}", admin) - response.status.should == 200 + expect(response.status).to eq(200) end it 'should not remove a non existing project' do delete api('/projects/1328', admin) - response.status.should == 404 + expect(response.status).to eq(404) end end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 5518d2df566..729970153d1 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -16,9 +16,9 @@ describe API::API, api: true do describe "GET /projects/:id/repository/tags" do it "should return an array of project tags" do get api("/projects/#{project.id}/repository/tags", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first['name'].should == project.repo.tags.sort_by(&:name).reverse.first.name + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project.repo.tags.sort_by(&:name).reverse.first.name) end end @@ -29,8 +29,8 @@ describe API::API, api: true do tag_name: 'v7.0.1', ref: 'master' - response.status.should == 201 - json_response['name'].should == 'v7.0.1' + expect(response.status).to eq(201) + expect(json_response['name']).to eq('v7.0.1') end end @@ -46,9 +46,9 @@ describe API::API, api: true do ref: 'master', message: 'Release 7.1.0' - response.status.should == 201 - json_response['name'].should == 'v7.1.0' - json_response['message'].should == 'Release 7.1.0' + expect(response.status).to eq(201) + expect(json_response['name']).to eq('v7.1.0') + expect(json_response['message']).to eq('Release 7.1.0') end end @@ -56,35 +56,35 @@ describe API::API, api: true do post api("/projects/#{project.id}/repository/tags", user2), tag_name: 'v1.9.0', ref: '621491c677087aa243f165eab467bfdfbee00be1' - response.status.should == 403 + expect(response.status).to eq(403) end it 'should return 400 if tag name is invalid' do post api("/projects/#{project.id}/repository/tags", user), tag_name: 'v 1.0.0', ref: 'master' - response.status.should == 400 - json_response['message'].should == 'Tag name invalid' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Tag name invalid') end it 'should return 400 if tag already exists' do post api("/projects/#{project.id}/repository/tags", user), tag_name: 'v8.0.0', ref: 'master' - response.status.should == 201 + expect(response.status).to eq(201) post api("/projects/#{project.id}/repository/tags", user), tag_name: 'v8.0.0', ref: 'master' - response.status.should == 400 - json_response['message'].should == 'Tag already exists' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Tag already exists') end it 'should return 400 if ref name is invalid' do post api("/projects/#{project.id}/repository/tags", user), tag_name: 'mytag', ref: 'foo' - response.status.should == 400 - json_response['message'].should == 'Invalid reference name' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('Invalid reference name') end end @@ -94,19 +94,19 @@ describe API::API, api: true do it "should return project commits" do get api("/projects/#{project.id}/repository/tree", user) - response.status.should == 200 + expect(response.status).to eq(200) - json_response.should be_an Array - json_response.first['name'].should == 'encoding' - json_response.first['type'].should == 'tree' - json_response.first['mode'].should == '040000' + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq('encoding') + expect(json_response.first['type']).to eq('tree') + expect(json_response.first['mode']).to eq('040000') end it 'should return a 404 for unknown ref' do get api("/projects/#{project.id}/repository/tree?ref_name=foo", user) - response.status.should == 404 + expect(response.status).to eq(404) - json_response.should be_an Object + expect(json_response).to be_an Object json_response['message'] == '404 Tree Not Found' end end @@ -114,7 +114,7 @@ describe API::API, api: true do context "unauthorized user" do it "should not return project commits" do get api("/projects/#{project.id}/repository/tree") - response.status.should == 401 + expect(response.status).to eq(401) end end end @@ -122,43 +122,43 @@ describe API::API, api: true do describe "GET /projects/:id/repository/blobs/:sha" do it "should get the raw file contents" do get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return 404 for invalid branch_name" do get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should return 404 for invalid file" do get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user) - response.status.should == 404 + expect(response.status).to eq(404) end it "should return a 400 error if filepath is missing" do get api("/projects/#{project.id}/repository/blobs/master", user) - response.status.should == 400 + expect(response.status).to eq(400) end end describe "GET /projects/:id/repository/commits/:sha/blob" do it "should get the raw file contents" do get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user) - response.status.should == 200 + expect(response.status).to eq(200) end end describe "GET /projects/:id/repository/raw_blobs/:sha" do it "should get the raw file contents" do get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user) - response.status.should == 200 + expect(response.status).to eq(200) end it 'should return a 404 for unknown blob' do get api("/projects/#{project.id}/repository/raw_blobs/123456", user) - response.status.should == 404 + expect(response.status).to eq(404) - json_response.should be_an Object + expect(json_response).to be_an Object json_response['message'] == '404 Blob Not Found' end end @@ -167,83 +167,83 @@ describe API::API, api: true do it "should get the archive" do get api("/projects/#{project.id}/repository/archive", user) repo_name = project.repository.name.gsub("\.git", "") - response.status.should == 200 - response.headers['Content-Disposition'].should =~ /filename\=\"#{repo_name}\-[^\.]+\.tar.gz\"/ - response.content_type.should == MIME::Types.type_for('file.tar.gz').first.content_type + expect(response.status).to eq(200) + expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.tar.gz\"/) + expect(response.content_type).to eq(MIME::Types.type_for('file.tar.gz').first.content_type) end it "should get the archive.zip" do get api("/projects/#{project.id}/repository/archive.zip", user) repo_name = project.repository.name.gsub("\.git", "") - response.status.should == 200 - response.headers['Content-Disposition'].should =~ /filename\=\"#{repo_name}\-[^\.]+\.zip\"/ - response.content_type.should == MIME::Types.type_for('file.zip').first.content_type + expect(response.status).to eq(200) + expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.zip\"/) + expect(response.content_type).to eq(MIME::Types.type_for('file.zip').first.content_type) end it "should get the archive.tar.bz2" do get api("/projects/#{project.id}/repository/archive.tar.bz2", user) repo_name = project.repository.name.gsub("\.git", "") - response.status.should == 200 - response.headers['Content-Disposition'].should =~ /filename\=\"#{repo_name}\-[^\.]+\.tar.bz2\"/ - response.content_type.should == MIME::Types.type_for('file.tar.bz2').first.content_type + expect(response.status).to eq(200) + expect(response.headers['Content-Disposition']).to match(/filename\=\"#{repo_name}\-[^\.]+\.tar.bz2\"/) + expect(response.content_type).to eq(MIME::Types.type_for('file.tar.bz2').first.content_type) end it "should return 404 for invalid sha" do get api("/projects/#{project.id}/repository/archive/?sha=xxx", user) - response.status.should == 404 + expect(response.status).to eq(404) end end describe 'GET /projects/:id/repository/compare' do it "should compare branches" do get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature' - response.status.should == 200 - json_response['commits'].should be_present - json_response['diffs'].should be_present + expect(response.status).to eq(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present end it "should compare tags" do get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0' - response.status.should == 200 - json_response['commits'].should be_present - json_response['diffs'].should be_present + expect(response.status).to eq(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present end it "should compare commits" do get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id - response.status.should == 200 - json_response['commits'].should be_empty - json_response['diffs'].should be_empty - json_response['compare_same_ref'].should be_false + expect(response.status).to eq(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_falsey end it "should compare commits in reverse order" do get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id - response.status.should == 200 - json_response['commits'].should be_present - json_response['diffs'].should be_present + expect(response.status).to eq(200) + expect(json_response['commits']).to be_present + expect(json_response['diffs']).to be_present end it "should compare same refs" do get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master' - response.status.should == 200 - json_response['commits'].should be_empty - json_response['diffs'].should be_empty - json_response['compare_same_ref'].should be_true + expect(response.status).to eq(200) + expect(json_response['commits']).to be_empty + expect(json_response['diffs']).to be_empty + expect(json_response['compare_same_ref']).to be_truthy end end describe 'GET /projects/:id/repository/contributors' do it 'should return valid data' do get api("/projects/#{project.id}/repository/contributors", user) - response.status.should == 200 - json_response.should be_an Array + expect(response.status).to eq(200) + expect(json_response).to be_an Array contributor = json_response.first - contributor['email'].should == 'dmitriy.zaporozhets@gmail.com' - contributor['name'].should == 'Dmitriy Zaporozhets' - contributor['commits'].should == 13 - contributor['additions'].should == 0 - contributor['deletions'].should == 0 + expect(contributor['email']).to eq('dmitriy.zaporozhets@gmail.com') + expect(contributor['name']).to eq('Dmitriy Zaporozhets') + expect(contributor['commits']).to eq(13) + expect(contributor['additions']).to eq(0) + expect(contributor['deletions']).to eq(0) end end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index d8282d0696b..51c543578df 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -9,13 +9,13 @@ describe API::API, api: true do it "should update gitlab-ci settings" do put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'secret-token', project_url: "http://ci.example.com/projects/1" - response.status.should == 200 + expect(response.status).to eq(200) end it "should return if required fields missing" do put api("/projects/#{project.id}/services/gitlab-ci", user), project_url: "http://ci.example.com/projects/1", active: true - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -23,8 +23,8 @@ describe API::API, api: true do it "should update gitlab-ci settings" do delete api("/projects/#{project.id}/services/gitlab-ci", user) - response.status.should == 200 - project.gitlab_ci_service.should be_nil + expect(response.status).to eq(200) + expect(project.gitlab_ci_service).to be_nil end end @@ -33,15 +33,15 @@ describe API::API, api: true do put api("/projects/#{project.id}/services/hipchat", user), token: 'secret-token', room: 'test' - response.status.should == 200 - project.hipchat_service.should_not be_nil + expect(response.status).to eq(200) + expect(project.hipchat_service).not_to be_nil end it 'should return if required fields missing' do put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'secret-token', active: true - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -49,8 +49,8 @@ describe API::API, api: true do it 'should delete hipchat settings' do delete api("/projects/#{project.id}/services/hipchat", user) - response.status.should == 200 - project.hipchat_service.should be_nil + expect(response.status).to eq(200) + expect(project.hipchat_service).to be_nil end end end diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb index 57b2e6cbd6a..fbd57b34a58 100644 --- a/spec/requests/api/session_spec.rb +++ b/spec/requests/api/session_spec.rb @@ -9,13 +9,13 @@ describe API::API, api: true do context "when valid password" do it "should return private token" do post api("/session"), email: user.email, password: '12345678' - response.status.should == 201 + expect(response.status).to eq(201) - json_response['email'].should == user.email - json_response['private_token'].should == user.private_token - json_response['is_admin'].should == user.is_admin? - json_response['can_create_project'].should == user.can_create_project? - json_response['can_create_group'].should == user.can_create_group? + expect(json_response['email']).to eq(user.email) + expect(json_response['private_token']).to eq(user.private_token) + expect(json_response['is_admin']).to eq(user.is_admin?) + expect(json_response['can_create_project']).to eq(user.can_create_project?) + expect(json_response['can_create_group']).to eq(user.can_create_group?) end end @@ -48,30 +48,30 @@ describe API::API, api: true do context "when invalid password" do it "should return authentication error" do post api("/session"), email: user.email, password: '123' - response.status.should == 401 + expect(response.status).to eq(401) - json_response['email'].should be_nil - json_response['private_token'].should be_nil + expect(json_response['email']).to be_nil + expect(json_response['private_token']).to be_nil end end context "when empty password" do it "should return authentication error" do post api("/session"), email: user.email - response.status.should == 401 + expect(response.status).to eq(401) - json_response['email'].should be_nil - json_response['private_token'].should be_nil + expect(json_response['email']).to be_nil + expect(json_response['private_token']).to be_nil end end context "when empty name" do it "should return authentication error" do post api("/session"), password: user.password - response.status.should == 401 + expect(response.status).to eq(401) - json_response['email'].should be_nil - json_response['private_token'].should be_nil + expect(json_response['email']).to be_nil + expect(json_response['private_token']).to be_nil end end end diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index 5784ae8c23a..a9d86bbce6c 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -13,23 +13,23 @@ describe API::API, api: true do context "when no user" do it "should return authentication error" do get api("/hooks") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when not an admin" do it "should return forbidden error" do get api("/hooks", user) - response.status.should == 403 + expect(response.status).to eq(403) end end context "when authenticated as admin" do it "should return an array of hooks" do get api("/hooks", admin) - response.status.should == 200 - json_response.should be_an Array - json_response.first['url'].should == hook.url + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['url']).to eq(hook.url) end end end @@ -43,7 +43,7 @@ describe API::API, api: true do it "should respond with 400 if url not given" do post api("/hooks", admin) - response.status.should == 400 + expect(response.status).to eq(400) end it "should not create new hook without url" do @@ -56,13 +56,13 @@ describe API::API, api: true do describe "GET /hooks/:id" do it "should return hook by id" do get api("/hooks/#{hook.id}", admin) - response.status.should == 200 - json_response['event_name'].should == 'project_create' + expect(response.status).to eq(200) + expect(json_response['event_name']).to eq('project_create') end it "should return 404 on failure" do get api("/hooks/404", admin) - response.status.should == 404 + expect(response.status).to eq(404) end end @@ -75,7 +75,7 @@ describe API::API, api: true do it "should return success if hook id not found" do delete api("/hooks/12345", admin) - response.status.should == 200 + expect(response.status).to eq(200) end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 12dfcacec23..081400cdedd 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -11,30 +11,30 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/users") - response.status.should == 401 + expect(response.status).to eq(401) end end context "when authenticated" do it "should return an array of users" do get api("/users", user) - response.status.should == 200 - json_response.should be_an Array + expect(response.status).to eq(200) + expect(json_response).to be_an Array username = user.username - json_response.detect { + expect(json_response.detect { |user| user['username'] == username - }['username'].should == username + }['username']).to eq(username) end end context "when admin" do it "should return an array of users" do get api("/users", admin) - response.status.should == 200 - json_response.should be_an Array - json_response.first.keys.should include 'email' - json_response.first.keys.should include 'identities' - json_response.first.keys.should include 'can_create_project' + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first.keys).to include 'email' + expect(json_response.first.keys).to include 'identities' + expect(json_response.first.keys).to include 'can_create_project' end end end @@ -42,19 +42,19 @@ describe API::API, api: true do describe "GET /users/:id" do it "should return a user by id" do get api("/users/#{user.id}", user) - response.status.should == 200 - json_response['username'].should == user.username + expect(response.status).to eq(200) + expect(json_response['username']).to eq(user.username) end it "should return a 401 if unauthenticated" do get api("/users/9998") - response.status.should == 401 + expect(response.status).to eq(401) end it "should return a 404 error if user id not found" do get api("/users/9999", user) - response.status.should == 404 - json_response['message'].should == '404 Not found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') end end @@ -69,36 +69,36 @@ describe API::API, api: true do it "should create user with correct attributes" do post api('/users', admin), attributes_for(:user, admin: true, can_create_group: true) - response.status.should == 201 + expect(response.status).to eq(201) user_id = json_response['id'] new_user = User.find(user_id) - new_user.should_not == nil - new_user.admin.should == true - new_user.can_create_group.should == true + expect(new_user).not_to eq(nil) + expect(new_user.admin).to eq(true) + expect(new_user.can_create_group).to eq(true) end it "should create non-admin user" do post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false) - response.status.should == 201 + expect(response.status).to eq(201) user_id = json_response['id'] new_user = User.find(user_id) - new_user.should_not == nil - new_user.admin.should == false - new_user.can_create_group.should == false + expect(new_user).not_to eq(nil) + expect(new_user.admin).to eq(false) + expect(new_user.can_create_group).to eq(false) end it "should create non-admin users by default" do post api('/users', admin), attributes_for(:user) - response.status.should == 201 + expect(response.status).to eq(201) user_id = json_response['id'] new_user = User.find(user_id) - new_user.should_not == nil - new_user.admin.should == false + expect(new_user).not_to eq(nil) + expect(new_user.admin).to eq(false) end it "should return 201 Created on success" do post api("/users", admin), attributes_for(:user, projects_limit: 3) - response.status.should == 201 + expect(response.status).to eq(201) end it "should not create user with invalid email" do @@ -106,22 +106,22 @@ describe API::API, api: true do email: 'invalid email', password: 'password', name: 'test' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 error if name not given' do post api('/users', admin), email: 'test@example.com', password: 'pass1234' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 error if password not given' do post api('/users', admin), email: 'test@example.com', name: 'test' - response.status.should == 400 + expect(response.status).to eq(400) end it "should return 400 error if email not given" do post api('/users', admin), password: 'pass1234', name: 'test' - response.status.should == 400 + expect(response.status).to eq(400) end it 'should return 400 error if user does not validate' do @@ -132,20 +132,20 @@ describe API::API, api: true do name: 'test', bio: 'g' * 256, projects_limit: -1 - response.status.should == 400 - json_response['message']['password']. - should == ['is too short (minimum is 8 characters)'] - json_response['message']['bio']. - should == ['is too long (maximum is 255 characters)'] - json_response['message']['projects_limit']. - should == ['must be greater than or equal to 0'] - json_response['message']['username']. - should == [Gitlab::Regex.send(:default_regex_message)] + expect(response.status).to eq(400) + expect(json_response['message']['password']). + to eq(['is too short (minimum is 8 characters)']) + expect(json_response['message']['bio']). + to eq(['is too long (maximum is 255 characters)']) + expect(json_response['message']['projects_limit']). + to eq(['must be greater than or equal to 0']) + expect(json_response['message']['username']). + to eq([Gitlab::Regex.send(:default_regex_message)]) end it "shouldn't available for non admin users" do post api("/users", user), attributes_for(:user) - response.status.should == 403 + expect(response.status).to eq(403) end context 'with existing user' do @@ -165,8 +165,8 @@ describe API::API, api: true do password: 'password', username: 'foo' }.to change { User.count }.by(0) - response.status.should == 409 - json_response['message'].should == 'Email has already been taken' + expect(response.status).to eq(409) + expect(json_response['message']).to eq('Email has already been taken') end it 'should return 409 conflict error if same username exists' do @@ -177,8 +177,8 @@ describe API::API, api: true do password: 'password', username: 'test' end.to change { User.count }.by(0) - response.status.should == 409 - json_response['message'].should == 'Username has already been taken' + expect(response.status).to eq(409) + expect(json_response['message']).to eq('Username has already been taken') end end end @@ -187,8 +187,8 @@ describe API::API, api: true do it "should redirect to sign in page" do get "/users/sign_up" - response.status.should == 302 - response.should redirect_to(new_user_session_path) + expect(response.status).to eq(302) + expect(response).to redirect_to(new_user_session_path) end end @@ -199,55 +199,55 @@ describe API::API, api: true do it "should update user with new bio" do put api("/users/#{user.id}", admin), {bio: 'new test bio'} - response.status.should == 200 - json_response['bio'].should == 'new test bio' - user.reload.bio.should == 'new test bio' + expect(response.status).to eq(200) + expect(json_response['bio']).to eq('new test bio') + expect(user.reload.bio).to eq('new test bio') end it 'should update user with his own email' do put api("/users/#{user.id}", admin), email: user.email - response.status.should == 200 - json_response['email'].should == user.email - user.reload.email.should == user.email + expect(response.status).to eq(200) + expect(json_response['email']).to eq(user.email) + expect(user.reload.email).to eq(user.email) end it 'should update user with his own username' do put api("/users/#{user.id}", admin), username: user.username - response.status.should == 200 - json_response['username'].should == user.username - user.reload.username.should == user.username + expect(response.status).to eq(200) + expect(json_response['username']).to eq(user.username) + expect(user.reload.username).to eq(user.username) end it "should update admin status" do put api("/users/#{user.id}", admin), {admin: true} - response.status.should == 200 - json_response['is_admin'].should == true - user.reload.admin.should == true + expect(response.status).to eq(200) + expect(json_response['is_admin']).to eq(true) + expect(user.reload.admin).to eq(true) end it "should not update admin status" do put api("/users/#{admin_user.id}", admin), {can_create_group: false} - response.status.should == 200 - json_response['is_admin'].should == true - admin_user.reload.admin.should == true - admin_user.can_create_group.should == false + expect(response.status).to eq(200) + expect(json_response['is_admin']).to eq(true) + expect(admin_user.reload.admin).to eq(true) + expect(admin_user.can_create_group).to eq(false) end it "should not allow invalid update" do put api("/users/#{user.id}", admin), {email: 'invalid email'} - response.status.should == 400 - user.reload.email.should_not == 'invalid email' + expect(response.status).to eq(400) + expect(user.reload.email).not_to eq('invalid email') end it "shouldn't available for non admin users" do put api("/users/#{user.id}", user), attributes_for(:user) - response.status.should == 403 + expect(response.status).to eq(403) end it "should return 404 for non-existing user" do put api("/users/999999", admin), {bio: 'update should fail'} - response.status.should == 404 - json_response['message'].should == '404 Not found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') end it 'should return 400 error if user does not validate' do @@ -258,15 +258,15 @@ describe API::API, api: true do name: 'test', bio: 'g' * 256, projects_limit: -1 - response.status.should == 400 - json_response['message']['password']. - should == ['is too short (minimum is 8 characters)'] - json_response['message']['bio']. - should == ['is too long (maximum is 255 characters)'] - json_response['message']['projects_limit']. - should == ['must be greater than or equal to 0'] - json_response['message']['username']. - should == [Gitlab::Regex.send(:default_regex_message)] + expect(response.status).to eq(400) + expect(json_response['message']['password']). + to eq(['is too short (minimum is 8 characters)']) + expect(json_response['message']['bio']). + to eq(['is too long (maximum is 255 characters)']) + expect(json_response['message']['projects_limit']). + to eq(['must be greater than or equal to 0']) + expect(json_response['message']['username']). + to eq([Gitlab::Regex.send(:default_regex_message)]) end context "with existing user" do @@ -278,15 +278,15 @@ describe API::API, api: true do it 'should return 409 conflict error if email address exists' do put api("/users/#{@user.id}", admin), email: 'test@example.com' - response.status.should == 409 - @user.reload.email.should == @user.email + expect(response.status).to eq(409) + expect(@user.reload.email).to eq(@user.email) end it 'should return 409 conflict error if username taken' do @user_id = User.all.last.id put api("/users/#{@user.id}", admin), username: 'test' - response.status.should == 409 - @user.reload.username.should == @user.username + expect(response.status).to eq(409) + expect(@user.reload.username).to eq(@user.username) end end end @@ -296,14 +296,14 @@ describe API::API, api: true do it "should not create invalid ssh key" do post api("/users/#{user.id}/keys", admin), { title: "invalid key" } - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "key" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "key" not given') end it 'should not create key without title' do post api("/users/#{user.id}/keys", admin), key: 'some key' - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "title" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "title" not given') end it "should create ssh key" do @@ -320,24 +320,24 @@ describe API::API, api: true do context 'when unauthenticated' do it 'should return authentication error' do get api("/users/#{user.id}/keys") - response.status.should == 401 + expect(response.status).to eq(401) end end context 'when authenticated' do it 'should return 404 for non-existing user' do get api('/users/999999/keys', admin) - response.status.should == 404 - json_response['message'].should == '404 User Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') end it 'should return array of ssh keys' do user.keys << key user.save get api("/users/#{user.id}/keys", admin) - response.status.should == 200 - json_response.should be_an Array - json_response.first['title'].should == key.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(key.title) end end end @@ -348,7 +348,7 @@ describe API::API, api: true do context 'when unauthenticated' do it 'should return authentication error' do delete api("/users/#{user.id}/keys/42") - response.status.should == 401 + expect(response.status).to eq(401) end end @@ -359,21 +359,21 @@ describe API::API, api: true do expect { delete api("/users/#{user.id}/keys/#{key.id}", admin) }.to change { user.keys.count }.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it 'should return 404 error if user not found' do user.keys << key user.save delete api("/users/999999/keys/#{key.id}", admin) - response.status.should == 404 - json_response['message'].should == '404 User Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') end it 'should return 404 error if key not foud' do delete api("/users/#{user.id}/keys/42", admin) - response.status.should == 404 - json_response['message'].should == '404 Key Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Key Not Found') end end end @@ -383,42 +383,42 @@ describe API::API, api: true do it "should delete user" do delete api("/users/#{user.id}", admin) - response.status.should == 200 + expect(response.status).to eq(200) expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound - json_response['email'].should == user.email + expect(json_response['email']).to eq(user.email) end it "should not delete for unauthenticated user" do delete api("/users/#{user.id}") - response.status.should == 401 + expect(response.status).to eq(401) end it "shouldn't available for non admin users" do delete api("/users/#{user.id}", user) - response.status.should == 403 + expect(response.status).to eq(403) end it "should return 404 for non-existing user" do delete api("/users/999999", admin) - response.status.should == 404 - json_response['message'].should == '404 User Not Found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 User Not Found') end end describe "GET /user" do it "should return current user" do get api("/user", user) - response.status.should == 200 - json_response['email'].should == user.email - json_response['is_admin'].should == user.is_admin? - json_response['can_create_project'].should == user.can_create_project? - json_response['can_create_group'].should == user.can_create_group? - json_response['projects_limit'].should == user.projects_limit + expect(response.status).to eq(200) + expect(json_response['email']).to eq(user.email) + expect(json_response['is_admin']).to eq(user.is_admin?) + expect(json_response['can_create_project']).to eq(user.can_create_project?) + expect(json_response['can_create_group']).to eq(user.can_create_group?) + expect(json_response['projects_limit']).to eq(user.projects_limit) end it "should return 401 error if user is unauthenticated" do get api("/user") - response.status.should == 401 + expect(response.status).to eq(401) end end @@ -426,7 +426,7 @@ describe API::API, api: true do context "when unauthenticated" do it "should return authentication error" do get api("/user/keys") - response.status.should == 401 + expect(response.status).to eq(401) end end @@ -435,9 +435,9 @@ describe API::API, api: true do user.keys << key user.save get api("/user/keys", user) - response.status.should == 200 - json_response.should be_an Array - json_response.first["title"].should == key.title + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first["title"]).to eq(key.title) end end end @@ -447,14 +447,14 @@ describe API::API, api: true do user.keys << key user.save get api("/user/keys/#{key.id}", user) - response.status.should == 200 - json_response["title"].should == key.title + expect(response.status).to eq(200) + expect(json_response["title"]).to eq(key.title) end it "should return 404 Not Found within invalid ID" do get api("/user/keys/42", user) - response.status.should == 404 - json_response['message'].should == '404 Not found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') end it "should return 404 error if admin accesses user's ssh key" do @@ -462,8 +462,8 @@ describe API::API, api: true do user.save admin get api("/user/keys/#{key.id}", admin) - response.status.should == 404 - json_response['message'].should == '404 Not found' + expect(response.status).to eq(404) + expect(json_response['message']).to eq('404 Not found') end end @@ -473,29 +473,29 @@ describe API::API, api: true do expect { post api("/user/keys", user), key_attrs }.to change{ user.keys.count }.by(1) - response.status.should == 201 + expect(response.status).to eq(201) end it "should return a 401 error if unauthorized" do post api("/user/keys"), title: 'some title', key: 'some key' - response.status.should == 401 + expect(response.status).to eq(401) end it "should not create ssh key without key" do post api("/user/keys", user), title: 'title' - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "key" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "key" not given') end it 'should not create ssh key without title' do post api('/user/keys', user), key: 'some key' - response.status.should == 400 - json_response['message'].should == '400 (Bad request) "title" not given' + expect(response.status).to eq(400) + expect(json_response['message']).to eq('400 (Bad request) "title" not given') end it "should not create ssh key without title" do post api("/user/keys", user), key: "somekey" - response.status.should == 400 + expect(response.status).to eq(400) end end @@ -506,19 +506,19 @@ describe API::API, api: true do expect { delete api("/user/keys/#{key.id}", user) }.to change{user.keys.count}.by(-1) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return success if key ID not found" do delete api("/user/keys/42", user) - response.status.should == 200 + expect(response.status).to eq(200) end it "should return 401 error if unauthorized" do user.keys << key user.save delete api("/user/keys/#{key.id}") - response.status.should == 401 + expect(response.status).to eq(401) end end end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 7fe18ff47c3..92542df52fd 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -12,47 +12,47 @@ require 'spec_helper' # DELETE /admin/users/:id(.:format) admin/users#destroy describe Admin::UsersController, "routing" do it "to #team_update" do - put("/admin/users/1/team_update").should route_to('admin/users#team_update', id: '1') + expect(put("/admin/users/1/team_update")).to route_to('admin/users#team_update', id: '1') end it "to #block" do - put("/admin/users/1/block").should route_to('admin/users#block', id: '1') + expect(put("/admin/users/1/block")).to route_to('admin/users#block', id: '1') end it "to #unblock" do - put("/admin/users/1/unblock").should route_to('admin/users#unblock', id: '1') + expect(put("/admin/users/1/unblock")).to route_to('admin/users#unblock', id: '1') end it "to #index" do - get("/admin/users").should route_to('admin/users#index') + expect(get("/admin/users")).to route_to('admin/users#index') end it "to #show" do - get("/admin/users/1").should route_to('admin/users#show', id: '1') + expect(get("/admin/users/1")).to route_to('admin/users#show', id: '1') end it "to #create" do - post("/admin/users").should route_to('admin/users#create') + expect(post("/admin/users")).to route_to('admin/users#create') end it "to #new" do - get("/admin/users/new").should route_to('admin/users#new') + expect(get("/admin/users/new")).to route_to('admin/users#new') end it "to #edit" do - get("/admin/users/1/edit").should route_to('admin/users#edit', id: '1') + expect(get("/admin/users/1/edit")).to route_to('admin/users#edit', id: '1') end it "to #show" do - get("/admin/users/1").should route_to('admin/users#show', id: '1') + expect(get("/admin/users/1")).to route_to('admin/users#show', id: '1') end it "to #update" do - put("/admin/users/1").should route_to('admin/users#update', id: '1') + expect(put("/admin/users/1")).to route_to('admin/users#update', id: '1') end it "to #destroy" do - delete("/admin/users/1").should route_to('admin/users#destroy', id: '1') + expect(delete("/admin/users/1")).to route_to('admin/users#destroy', id: '1') end end @@ -67,11 +67,11 @@ end # DELETE /admin/projects/:id(.:format) admin/projects#destroy {id: /[^\/]+/} describe Admin::ProjectsController, "routing" do it "to #index" do - get("/admin/projects").should route_to('admin/projects#index') + expect(get("/admin/projects")).to route_to('admin/projects#index') end it "to #show" do - get("/admin/projects/gitlab").should route_to('admin/projects#show', id: 'gitlab') + expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', id: 'gitlab') end end @@ -81,19 +81,19 @@ end # admin_hook DELETE /admin/hooks/:id(.:format) admin/hooks#destroy describe Admin::HooksController, "routing" do it "to #test" do - get("/admin/hooks/1/test").should route_to('admin/hooks#test', hook_id: '1') + expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', hook_id: '1') end it "to #index" do - get("/admin/hooks").should route_to('admin/hooks#index') + expect(get("/admin/hooks")).to route_to('admin/hooks#index') end it "to #create" do - post("/admin/hooks").should route_to('admin/hooks#create') + expect(post("/admin/hooks")).to route_to('admin/hooks#create') end it "to #destroy" do - delete("/admin/hooks/1").should route_to('admin/hooks#destroy', id: '1') + expect(delete("/admin/hooks/1")).to route_to('admin/hooks#destroy', id: '1') end end @@ -101,21 +101,21 @@ end # admin_logs GET /admin/logs(.:format) admin/logs#show describe Admin::LogsController, "routing" do it "to #show" do - get("/admin/logs").should route_to('admin/logs#show') + expect(get("/admin/logs")).to route_to('admin/logs#show') end end # admin_background_jobs GET /admin/background_jobs(.:format) admin/background_jobs#show describe Admin::BackgroundJobsController, "routing" do it "to #show" do - get("/admin/background_jobs").should route_to('admin/background_jobs#show') + expect(get("/admin/background_jobs")).to route_to('admin/background_jobs#show') end end # admin_root /admin(.:format) admin/dashboard#index describe Admin::DashboardController, "routing" do it "to #index" do - get("/admin").should route_to('admin/dashboard#index') + expect(get("/admin")).to route_to('admin/dashboard#index') end end diff --git a/spec/routing/notifications_routing_spec.rb b/spec/routing/notifications_routing_spec.rb index 112b825e023..24592942a96 100644 --- a/spec/routing/notifications_routing_spec.rb +++ b/spec/routing/notifications_routing_spec.rb @@ -3,11 +3,11 @@ require "spec_helper" describe Profiles::NotificationsController do describe "routing" do it "routes to #show" do - get("/profile/notifications").should route_to("profiles/notifications#show") + expect(get("/profile/notifications")).to route_to("profiles/notifications#show") end it "routes to #update" do - put("/profile/notifications").should route_to("profiles/notifications#update") + expect(put("/profile/notifications")).to route_to("profiles/notifications#update") end end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index b8f9d2bf20a..6b587345597 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -25,31 +25,31 @@ shared_examples 'RESTful project resources' do let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } it 'to #index' do - get("/gitlab/gitlabhq/#{controller}").should route_to("projects/#{controller}#index", project_id: 'gitlab/gitlabhq') if actions.include?(:index) + expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", project_id: 'gitlab/gitlabhq') if actions.include?(:index) end it 'to #create' do - post("/gitlab/gitlabhq/#{controller}").should route_to("projects/#{controller}#create", project_id: 'gitlab/gitlabhq') if actions.include?(:create) + expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", project_id: 'gitlab/gitlabhq') if actions.include?(:create) end it 'to #new' do - get("/gitlab/gitlabhq/#{controller}/new").should route_to("projects/#{controller}#new", project_id: 'gitlab/gitlabhq') if actions.include?(:new) + expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", project_id: 'gitlab/gitlabhq') if actions.include?(:new) end it 'to #edit' do - get("/gitlab/gitlabhq/#{controller}/1/edit").should route_to("projects/#{controller}#edit", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:edit) + expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:edit) end it 'to #show' do - get("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#show", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:show) + expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:show) end it 'to #update' do - put("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#update", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:update) + expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:update) end it 'to #destroy' do - delete("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#destroy", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:destroy) + expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:destroy) end end @@ -63,35 +63,35 @@ end # markdown_preview_project POST /:id/markdown_preview(.:format) projects#markdown_preview describe ProjectsController, 'routing' do it 'to #create' do - post('/projects').should route_to('projects#create') + expect(post('/projects')).to route_to('projects#create') end it 'to #new' do - get('/projects/new').should route_to('projects#new') + expect(get('/projects/new')).to route_to('projects#new') end it 'to #edit' do - get('/gitlab/gitlabhq/edit').should route_to('projects#edit', id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', id: 'gitlab/gitlabhq') end it 'to #autocomplete_sources' do - get('/gitlab/gitlabhq/autocomplete_sources').should route_to('projects#autocomplete_sources', id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', id: 'gitlab/gitlabhq') end it 'to #show' do - get('/gitlab/gitlabhq').should route_to('projects#show', id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq')).to route_to('projects#show', id: 'gitlab/gitlabhq') end it 'to #update' do - put('/gitlab/gitlabhq').should route_to('projects#update', id: 'gitlab/gitlabhq') + expect(put('/gitlab/gitlabhq')).to route_to('projects#update', id: 'gitlab/gitlabhq') end it 'to #destroy' do - delete('/gitlab/gitlabhq').should route_to('projects#destroy', id: 'gitlab/gitlabhq') + expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', id: 'gitlab/gitlabhq') end it 'to #markdown_preview' do - post('/gitlab/gitlabhq/markdown_preview').should( + expect(post('/gitlab/gitlabhq/markdown_preview')).to( route_to('projects#markdown_preview', id: 'gitlab/gitlabhq') ) end @@ -105,11 +105,11 @@ end # DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy describe Projects::WikisController, 'routing' do it 'to #pages' do - get('/gitlab/gitlabhq/wikis/pages').should route_to('projects/wikis#pages', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', project_id: 'gitlab/gitlabhq') end it 'to #history' do - get('/gitlab/gitlabhq/wikis/1/history').should route_to('projects/wikis#history', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', project_id: 'gitlab/gitlabhq', id: '1') end it_behaves_like 'RESTful project resources' do @@ -124,43 +124,43 @@ end # edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit describe Projects::RepositoriesController, 'routing' do it 'to #archive' do - get('/gitlab/gitlabhq/repository/archive').should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq') end it 'to #archive format:zip' do - get('/gitlab/gitlabhq/repository/archive.zip').should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'zip') + expect(get('/gitlab/gitlabhq/repository/archive.zip')).to route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'zip') end it 'to #archive format:tar.bz2' do - get('/gitlab/gitlabhq/repository/archive.tar.bz2').should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'tar.bz2') + expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'tar.bz2') end it 'to #show' do - get('/gitlab/gitlabhq/repository').should route_to('projects/repositories#show', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/repository')).to route_to('projects/repositories#show', project_id: 'gitlab/gitlabhq') end end describe Projects::BranchesController, 'routing' do it 'to #branches' do - get('/gitlab/gitlabhq/branches').should route_to('projects/branches#index', project_id: 'gitlab/gitlabhq') - delete('/gitlab/gitlabhq/branches/feature%2345').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') - delete('/gitlab/gitlabhq/branches/feature%2B45').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') - delete('/gitlab/gitlabhq/branches/feature@45').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') - delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') - delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') - delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') + expect(get('/gitlab/gitlabhq/branches')).to route_to('projects/branches#index', project_id: 'gitlab/gitlabhq') + expect(delete('/gitlab/gitlabhq/branches/feature%2345')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') + expect(delete('/gitlab/gitlabhq/branches/feature%2B45')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') + expect(delete('/gitlab/gitlabhq/branches/feature@45')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') + expect(delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') end end describe Projects::TagsController, 'routing' do it 'to #tags' do - get('/gitlab/gitlabhq/tags').should route_to('projects/tags#index', project_id: 'gitlab/gitlabhq') - delete('/gitlab/gitlabhq/tags/feature%2345').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') - delete('/gitlab/gitlabhq/tags/feature%2B45').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') - delete('/gitlab/gitlabhq/tags/feature@45').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') - delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') - delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') - delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') + expect(get('/gitlab/gitlabhq/tags')).to route_to('projects/tags#index', project_id: 'gitlab/gitlabhq') + expect(delete('/gitlab/gitlabhq/tags/feature%2345')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') + expect(delete('/gitlab/gitlabhq/tags/feature%2B45')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') + expect(delete('/gitlab/gitlabhq/tags/feature@45')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') + expect(delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') end end @@ -193,19 +193,19 @@ end # logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree describe Projects::RefsController, 'routing' do it 'to #switch' do - get('/gitlab/gitlabhq/refs/switch').should route_to('projects/refs#switch', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/refs/switch')).to route_to('projects/refs#switch', project_id: 'gitlab/gitlabhq') end it 'to #logs_tree' do - get('/gitlab/gitlabhq/refs/stable/logs_tree').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable') - get('/gitlab/gitlabhq/refs/feature%2345/logs_tree').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45') - get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45') - get('/gitlab/gitlabhq/refs/feature@45/logs_tree').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45') - get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'foo/bar/baz') - get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45', path: 'foo/bar/baz') - get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45', path: 'foo/bar/baz') - get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45', path: 'foo/bar/baz') - get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'files.scss') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable') + expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45') + expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45') + expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'files.scss') end end @@ -223,31 +223,31 @@ end # DELETE /:project_id/merge_requests/:id(.:format) projects/merge_requests#destroy describe Projects::MergeRequestsController, 'routing' do it 'to #diffs' do - get('/gitlab/gitlabhq/merge_requests/1/diffs').should route_to('projects/merge_requests#diffs', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', project_id: 'gitlab/gitlabhq', id: '1') end it 'to #automerge' do - post('/gitlab/gitlabhq/merge_requests/1/automerge').should route_to( + expect(post('/gitlab/gitlabhq/merge_requests/1/automerge')).to route_to( 'projects/merge_requests#automerge', project_id: 'gitlab/gitlabhq', id: '1' ) end it 'to #automerge_check' do - get('/gitlab/gitlabhq/merge_requests/1/automerge_check').should route_to('projects/merge_requests#automerge_check', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/merge_requests/1/automerge_check')).to route_to('projects/merge_requests#automerge_check', project_id: 'gitlab/gitlabhq', id: '1') end it 'to #branch_from' do - get('/gitlab/gitlabhq/merge_requests/branch_from').should route_to('projects/merge_requests#branch_from', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', project_id: 'gitlab/gitlabhq') end it 'to #branch_to' do - get('/gitlab/gitlabhq/merge_requests/branch_to').should route_to('projects/merge_requests#branch_to', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', project_id: 'gitlab/gitlabhq') end it 'to #show' do - get('/gitlab/gitlabhq/merge_requests/1.diff').should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'diff') - get('/gitlab/gitlabhq/merge_requests/1.patch').should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'patch') + expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'diff') + expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'patch') end it_behaves_like 'RESTful project resources' do @@ -266,35 +266,35 @@ end # DELETE /:project_id/snippets/:id(.:format) snippets#destroy describe SnippetsController, 'routing' do it 'to #raw' do - get('/gitlab/gitlabhq/snippets/1/raw').should route_to('projects/snippets#raw', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/snippets/1/raw')).to route_to('projects/snippets#raw', project_id: 'gitlab/gitlabhq', id: '1') end it 'to #index' do - get('/gitlab/gitlabhq/snippets').should route_to('projects/snippets#index', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#index', project_id: 'gitlab/gitlabhq') end it 'to #create' do - post('/gitlab/gitlabhq/snippets').should route_to('projects/snippets#create', project_id: 'gitlab/gitlabhq') + expect(post('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#create', project_id: 'gitlab/gitlabhq') end it 'to #new' do - get('/gitlab/gitlabhq/snippets/new').should route_to('projects/snippets#new', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/snippets/new')).to route_to('projects/snippets#new', project_id: 'gitlab/gitlabhq') end it 'to #edit' do - get('/gitlab/gitlabhq/snippets/1/edit').should route_to('projects/snippets#edit', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/snippets/1/edit')).to route_to('projects/snippets#edit', project_id: 'gitlab/gitlabhq', id: '1') end it 'to #show' do - get('/gitlab/gitlabhq/snippets/1').should route_to('projects/snippets#show', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', project_id: 'gitlab/gitlabhq', id: '1') end it 'to #update' do - put('/gitlab/gitlabhq/snippets/1').should route_to('projects/snippets#update', project_id: 'gitlab/gitlabhq', id: '1') + expect(put('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#update', project_id: 'gitlab/gitlabhq', id: '1') end it 'to #destroy' do - delete('/gitlab/gitlabhq/snippets/1').should route_to('projects/snippets#destroy', project_id: 'gitlab/gitlabhq', id: '1') + expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', project_id: 'gitlab/gitlabhq', id: '1') end end @@ -304,7 +304,7 @@ end # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy describe Projects::HooksController, 'routing' do it 'to #test' do - get('/gitlab/gitlabhq/hooks/1/test').should route_to('projects/hooks#test', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', project_id: 'gitlab/gitlabhq', id: '1') end it_behaves_like 'RESTful project resources' do @@ -316,10 +316,10 @@ end # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /[[:alnum:]]{6,40}/, project_id: /[^\/]+/} describe Projects::CommitController, 'routing' do it 'to #show' do - get('/gitlab/gitlabhq/commit/4246fb').should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb') - get('/gitlab/gitlabhq/commit/4246fb.diff').should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'diff') - get('/gitlab/gitlabhq/commit/4246fb.patch').should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'patch') - get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5').should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') + expect(get('/gitlab/gitlabhq/commit/4246fb')).to route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb') + expect(get('/gitlab/gitlabhq/commit/4246fb.diff')).to route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'diff') + expect(get('/gitlab/gitlabhq/commit/4246fb.patch')).to route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'patch') + expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') end end @@ -334,7 +334,7 @@ describe Projects::CommitsController, 'routing' do end it 'to #show' do - get('/gitlab/gitlabhq/commits/master.atom').should route_to('projects/commits#show', project_id: 'gitlab/gitlabhq', id: 'master', format: 'atom') + expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', project_id: 'gitlab/gitlabhq', id: 'master', format: 'atom') end end @@ -369,7 +369,7 @@ end # project_labels GET /:project_id/labels(.:format) labels#index describe Projects::LabelsController, 'routing' do it 'to #index' do - get('/gitlab/gitlabhq/labels').should route_to('projects/labels#index', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', project_id: 'gitlab/gitlabhq') end end @@ -385,7 +385,7 @@ end # DELETE /:project_id/issues/:id(.:format) issues#destroy describe Projects::IssuesController, 'routing' do it 'to #bulk_update' do - post('/gitlab/gitlabhq/issues/bulk_update').should route_to('projects/issues#bulk_update', project_id: 'gitlab/gitlabhq') + expect(post('/gitlab/gitlabhq/issues/bulk_update')).to route_to('projects/issues#bulk_update', project_id: 'gitlab/gitlabhq') end it_behaves_like 'RESTful project resources' do @@ -407,39 +407,39 @@ end # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/} describe Projects::BlameController, 'routing' do it 'to #show' do - get('/gitlab/gitlabhq/blame/master/app/models/project.rb').should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - get('/gitlab/gitlabhq/blame/master/files.scss').should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') + expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/} describe Projects::BlobController, 'routing' do it 'to #show' do - get('/gitlab/gitlabhq/blob/master/app/models/project.rb').should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - get('/gitlab/gitlabhq/blob/master/app/models/compare.rb').should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/compare.rb') - get('/gitlab/gitlabhq/blob/master/app/models/diff.js').should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/diff.js') - get('/gitlab/gitlabhq/blob/master/files.scss').should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') + expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/blob/master/app/models/compare.rb')).to route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/compare.rb') + expect(get('/gitlab/gitlabhq/blob/master/app/models/diff.js')).to route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/diff.js') + expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/} describe Projects::TreeController, 'routing' do it 'to #show' do - get('/gitlab/gitlabhq/tree/master/app/models/project.rb').should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - get('/gitlab/gitlabhq/tree/master/files.scss').should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') + expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end describe Projects::BlobController, 'routing' do it 'to #edit' do - get('/gitlab/gitlabhq/edit/master/app/models/project.rb').should( + expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to( route_to('projects/blob#edit', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb')) end it 'to #preview' do - post('/gitlab/gitlabhq/preview/master/app/models/project.rb').should( + expect(post('/gitlab/gitlabhq/preview/master/app/models/project.rb')).to( route_to('projects/blob#preview', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb')) @@ -451,46 +451,46 @@ end # project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/} describe Projects::CompareController, 'routing' do it 'to #index' do - get('/gitlab/gitlabhq/compare').should route_to('projects/compare#index', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/compare')).to route_to('projects/compare#index', project_id: 'gitlab/gitlabhq') end it 'to #compare' do - post('/gitlab/gitlabhq/compare').should route_to('projects/compare#create', project_id: 'gitlab/gitlabhq') + expect(post('/gitlab/gitlabhq/compare')).to route_to('projects/compare#create', project_id: 'gitlab/gitlabhq') end it 'to #show' do - get('/gitlab/gitlabhq/compare/master...stable').should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'master', to: 'stable') - get('/gitlab/gitlabhq/compare/issue/1234...stable').should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'issue/1234', to: 'stable') + expect(get('/gitlab/gitlabhq/compare/master...stable')).to route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'master', to: 'stable') + expect(get('/gitlab/gitlabhq/compare/issue/1234...stable')).to route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'issue/1234', to: 'stable') end end describe Projects::NetworkController, 'routing' do it 'to #show' do - get('/gitlab/gitlabhq/network/master').should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master') - get('/gitlab/gitlabhq/network/master.json').should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master', format: 'json') + expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master') + expect(get('/gitlab/gitlabhq/network/master.json')).to route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master', format: 'json') end end describe Projects::GraphsController, 'routing' do it 'to #show' do - get('/gitlab/gitlabhq/graphs/master').should route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master') + expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master') end end describe Projects::ForksController, 'routing' do it 'to #new' do - get('/gitlab/gitlabhq/fork/new').should route_to('projects/forks#new', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/fork/new')).to route_to('projects/forks#new', project_id: 'gitlab/gitlabhq') end it 'to #create' do - post('/gitlab/gitlabhq/fork').should route_to('projects/forks#create', project_id: 'gitlab/gitlabhq') + expect(post('/gitlab/gitlabhq/fork')).to route_to('projects/forks#create', project_id: 'gitlab/gitlabhq') end end # project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy describe Projects::AvatarsController, 'routing' do it 'to #destroy' do - delete('/gitlab/gitlabhq/avatar').should route_to( + expect(delete('/gitlab/gitlabhq/avatar')).to route_to( 'projects/avatars#destroy', project_id: 'gitlab/gitlabhq') end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 1e92cf62dd5..d4915b51952 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' # search GET /search(.:format) search#show describe SearchController, "routing" do it "to #show" do - get("/search").should route_to('search#show') + expect(get("/search")).to route_to('search#show') end end @@ -11,11 +11,11 @@ end # /:path Grack describe "Mounted Apps", "routing" do it "to API" do - get("/api/issues").should be_routable + expect(get("/api/issues")).to be_routable end it "to Grack" do - get("/gitlab/gitlabhq.git").should be_routable + expect(get("/gitlab/gitlabhq.git")).to be_routable end end @@ -28,39 +28,39 @@ end # DELETE /snippets/:id(.:format) snippets#destroy describe SnippetsController, "routing" do it "to #user_index" do - get("/s/User").should route_to('snippets#user_index', username: 'User') + expect(get("/s/User")).to route_to('snippets#user_index', username: 'User') end it "to #raw" do - get("/snippets/1/raw").should route_to('snippets#raw', id: '1') + expect(get("/snippets/1/raw")).to route_to('snippets#raw', id: '1') end it "to #index" do - get("/snippets").should route_to('snippets#index') + expect(get("/snippets")).to route_to('snippets#index') end it "to #create" do - post("/snippets").should route_to('snippets#create') + expect(post("/snippets")).to route_to('snippets#create') end it "to #new" do - get("/snippets/new").should route_to('snippets#new') + expect(get("/snippets/new")).to route_to('snippets#new') end it "to #edit" do - get("/snippets/1/edit").should route_to('snippets#edit', id: '1') + expect(get("/snippets/1/edit")).to route_to('snippets#edit', id: '1') end it "to #show" do - get("/snippets/1").should route_to('snippets#show', id: '1') + expect(get("/snippets/1")).to route_to('snippets#show', id: '1') end it "to #update" do - put("/snippets/1").should route_to('snippets#update', id: '1') + expect(put("/snippets/1")).to route_to('snippets#update', id: '1') end it "to #destroy" do - delete("/snippets/1").should route_to('snippets#destroy', id: '1') + expect(delete("/snippets/1")).to route_to('snippets#destroy', id: '1') end end @@ -75,39 +75,39 @@ end # help_raketasks GET /help/raketasks(.:format) help#raketasks describe HelpController, "routing" do it "to #index" do - get("/help").should route_to('help#index') + expect(get("/help")).to route_to('help#index') end it "to #permissions" do - get("/help/permissions/permissions").should route_to('help#show', category: "permissions", file: "permissions") + expect(get("/help/permissions/permissions")).to route_to('help#show', category: "permissions", file: "permissions") end it "to #workflow" do - get("/help/workflow/README").should route_to('help#show', category: "workflow", file: "README") + expect(get("/help/workflow/README")).to route_to('help#show', category: "workflow", file: "README") end it "to #api" do - get("/help/api/README").should route_to('help#show', category: "api", file: "README") + expect(get("/help/api/README")).to route_to('help#show', category: "api", file: "README") end it "to #web_hooks" do - get("/help/web_hooks/web_hooks").should route_to('help#show', category: "web_hooks", file: "web_hooks") + expect(get("/help/web_hooks/web_hooks")).to route_to('help#show', category: "web_hooks", file: "web_hooks") end it "to #system_hooks" do - get("/help/system_hooks/system_hooks").should route_to('help#show', category: "system_hooks", file: "system_hooks") + expect(get("/help/system_hooks/system_hooks")).to route_to('help#show', category: "system_hooks", file: "system_hooks") end it "to #markdown" do - get("/help/markdown/markdown").should route_to('help#show',category: "markdown", file: "markdown") + expect(get("/help/markdown/markdown")).to route_to('help#show',category: "markdown", file: "markdown") end it "to #ssh" do - get("/help/ssh/README").should route_to('help#show', category: "ssh", file: "README") + expect(get("/help/ssh/README")).to route_to('help#show', category: "ssh", file: "README") end it "to #raketasks" do - get("/help/raketasks/README").should route_to('help#show', category: "raketasks", file: "README") + expect(get("/help/raketasks/README")).to route_to('help#show', category: "raketasks", file: "README") end end @@ -121,23 +121,23 @@ end # profile_update PUT /profile/update(.:format) profile#update describe ProfilesController, "routing" do it "to #account" do - get("/profile/account").should route_to('profiles/accounts#show') + expect(get("/profile/account")).to route_to('profiles/accounts#show') end it "to #history" do - get("/profile/history").should route_to('profiles#history') + expect(get("/profile/history")).to route_to('profiles#history') end it "to #reset_private_token" do - put("/profile/reset_private_token").should route_to('profiles#reset_private_token') + expect(put("/profile/reset_private_token")).to route_to('profiles#reset_private_token') end it "to #show" do - get("/profile").should route_to('profiles#show') + expect(get("/profile")).to route_to('profiles#show') end it "to #design" do - get("/profile/design").should route_to('profiles#design') + expect(get("/profile/design")).to route_to('profiles#design') end end @@ -150,36 +150,36 @@ end # DELETE /keys/:id(.:format) keys#destroy describe Profiles::KeysController, "routing" do it "to #index" do - get("/profile/keys").should route_to('profiles/keys#index') + expect(get("/profile/keys")).to route_to('profiles/keys#index') end it "to #create" do - post("/profile/keys").should route_to('profiles/keys#create') + expect(post("/profile/keys")).to route_to('profiles/keys#create') end it "to #new" do - get("/profile/keys/new").should route_to('profiles/keys#new') + expect(get("/profile/keys/new")).to route_to('profiles/keys#new') end it "to #edit" do - get("/profile/keys/1/edit").should route_to('profiles/keys#edit', id: '1') + expect(get("/profile/keys/1/edit")).to route_to('profiles/keys#edit', id: '1') end it "to #show" do - get("/profile/keys/1").should route_to('profiles/keys#show', id: '1') + expect(get("/profile/keys/1")).to route_to('profiles/keys#show', id: '1') end it "to #update" do - put("/profile/keys/1").should route_to('profiles/keys#update', id: '1') + expect(put("/profile/keys/1")).to route_to('profiles/keys#update', id: '1') end it "to #destroy" do - delete("/profile/keys/1").should route_to('profiles/keys#destroy', id: '1') + expect(delete("/profile/keys/1")).to route_to('profiles/keys#destroy', id: '1') end # get all the ssh-keys of a user it "to #get_keys" do - get("/foo.keys").should route_to('profiles/keys#get_keys', username: 'foo') + expect(get("/foo.keys")).to route_to('profiles/keys#get_keys', username: 'foo') end end @@ -188,22 +188,22 @@ end # DELETE /keys/:id(.:format) keys#destroy describe Profiles::EmailsController, "routing" do it "to #index" do - get("/profile/emails").should route_to('profiles/emails#index') + expect(get("/profile/emails")).to route_to('profiles/emails#index') end it "to #create" do - post("/profile/emails").should route_to('profiles/emails#create') + expect(post("/profile/emails")).to route_to('profiles/emails#create') end it "to #destroy" do - delete("/profile/emails/1").should route_to('profiles/emails#destroy', id: '1') + expect(delete("/profile/emails/1")).to route_to('profiles/emails#destroy', id: '1') end end # profile_avatar DELETE /profile/avatar(.:format) profiles/avatars#destroy describe Profiles::AvatarsController, "routing" do it "to #destroy" do - delete("/profile/avatar").should route_to('profiles/avatars#destroy') + expect(delete("/profile/avatar")).to route_to('profiles/avatars#destroy') end end @@ -213,16 +213,16 @@ end # root / dashboard#show describe DashboardController, "routing" do it "to #index" do - get("/dashboard").should route_to('dashboard#show') - get("/").should route_to('dashboard#show') + expect(get("/dashboard")).to route_to('dashboard#show') + expect(get("/")).to route_to('dashboard#show') end it "to #issues" do - get("/dashboard/issues").should route_to('dashboard#issues') + expect(get("/dashboard/issues")).to route_to('dashboard#issues') end it "to #merge_requests" do - get("/dashboard/merge_requests").should route_to('dashboard#merge_requests') + expect(get("/dashboard/merge_requests")).to route_to('dashboard#merge_requests') end end @@ -241,11 +241,11 @@ end describe "Groups", "routing" do it "to #show" do - get("/groups/1").should route_to('groups#show', id: '1') + expect(get("/groups/1")).to route_to('groups#show', id: '1') end it "also display group#show on the short path" do - get('/1').should route_to('namespaces#show', id: '1') + expect(get('/1')).to route_to('namespaces#show', id: '1') end end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 713aa3e7e74..007a9eed192 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -7,7 +7,7 @@ describe EventCreateService do describe :open_issue do let(:issue) { create(:issue) } - it { service.open_issue(issue, issue.author).should be_true } + it { expect(service.open_issue(issue, issue.author)).to be_truthy } it "should create new event" do expect { service.open_issue(issue, issue.author) }.to change { Event.count } @@ -17,7 +17,7 @@ describe EventCreateService do describe :close_issue do let(:issue) { create(:issue) } - it { service.close_issue(issue, issue.author).should be_true } + it { expect(service.close_issue(issue, issue.author)).to be_truthy } it "should create new event" do expect { service.close_issue(issue, issue.author) }.to change { Event.count } @@ -27,7 +27,7 @@ describe EventCreateService do describe :reopen_issue do let(:issue) { create(:issue) } - it { service.reopen_issue(issue, issue.author).should be_true } + it { expect(service.reopen_issue(issue, issue.author)).to be_truthy } it "should create new event" do expect { service.reopen_issue(issue, issue.author) }.to change { Event.count } @@ -39,7 +39,7 @@ describe EventCreateService do describe :open_mr do let(:merge_request) { create(:merge_request) } - it { service.open_mr(merge_request, merge_request.author).should be_true } + it { expect(service.open_mr(merge_request, merge_request.author)).to be_truthy } it "should create new event" do expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count } @@ -49,7 +49,7 @@ describe EventCreateService do describe :close_mr do let(:merge_request) { create(:merge_request) } - it { service.close_mr(merge_request, merge_request.author).should be_true } + it { expect(service.close_mr(merge_request, merge_request.author)).to be_truthy } it "should create new event" do expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count } @@ -59,7 +59,7 @@ describe EventCreateService do describe :merge_mr do let(:merge_request) { create(:merge_request) } - it { service.merge_mr(merge_request, merge_request.author).should be_true } + it { expect(service.merge_mr(merge_request, merge_request.author)).to be_truthy } it "should create new event" do expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count } @@ -69,7 +69,7 @@ describe EventCreateService do describe :reopen_mr do let(:merge_request) { create(:merge_request) } - it { service.reopen_mr(merge_request, merge_request.author).should be_true } + it { expect(service.reopen_mr(merge_request, merge_request.author)).to be_truthy } it "should create new event" do expect { service.reopen_mr(merge_request, merge_request.author) }.to change { Event.count } @@ -83,7 +83,7 @@ describe EventCreateService do describe :open_milestone do let(:milestone) { create(:milestone) } - it { service.open_milestone(milestone, user).should be_true } + it { expect(service.open_milestone(milestone, user)).to be_truthy } it "should create new event" do expect { service.open_milestone(milestone, user) }.to change { Event.count } @@ -93,7 +93,7 @@ describe EventCreateService do describe :close_mr do let(:milestone) { create(:milestone) } - it { service.close_milestone(milestone, user).should be_true } + it { expect(service.close_milestone(milestone, user)).to be_truthy } it "should create new event" do expect { service.close_milestone(milestone, user) }.to change { Event.count } diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 3a75d65b5bc..9d0e41e4e8a 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -20,7 +20,7 @@ describe GitPushService do service.execute(project, user, @blankrev, @newrev, @ref) end - it { should be_true } + it { is_expected.to be_truthy } end context 'existing branch' do @@ -28,7 +28,7 @@ describe GitPushService do service.execute(project, user, @oldrev, @newrev, @ref) end - it { should be_true } + it { is_expected.to be_truthy } end context 'rm branch' do @@ -36,7 +36,7 @@ describe GitPushService do service.execute(project, user, @oldrev, @blankrev, @ref) end - it { should be_true } + it { is_expected.to be_truthy } end end @@ -49,41 +49,43 @@ describe GitPushService do subject { @push_data } - it { should include(before: @oldrev) } - it { should include(after: @newrev) } - it { should include(ref: @ref) } - it { should include(user_id: user.id) } - it { should include(user_name: user.name) } - it { should include(project_id: project.id) } + it { is_expected.to include(before: @oldrev) } + it { is_expected.to include(after: @newrev) } + it { is_expected.to include(ref: @ref) } + it { is_expected.to include(user_id: user.id) } + it { is_expected.to include(user_name: user.name) } + it { is_expected.to include(project_id: project.id) } context "with repository data" do subject { @push_data[:repository] } - it { should include(name: project.name) } - it { should include(url: project.url_to_repo) } - it { should include(description: project.description) } - it { should include(homepage: project.web_url) } + it { is_expected.to include(name: project.name) } + it { is_expected.to include(url: project.url_to_repo) } + it { is_expected.to include(description: project.description) } + it { is_expected.to include(homepage: project.web_url) } end context "with commits" do subject { @push_data[:commits] } - it { should be_an(Array) } - it { should have(1).element } + it { is_expected.to be_an(Array) } + it 'has 1 element' do + expect(subject.size).to eq(1) + end context "the commit" do subject { @push_data[:commits].first } - it { should include(id: @commit.id) } - it { should include(message: @commit.safe_message) } - it { should include(timestamp: @commit.date.xmlschema) } - it { should include(url: "#{Gitlab.config.gitlab.url}/#{project.to_param}/commit/#{@commit.id}") } + it { is_expected.to include(id: @commit.id) } + it { is_expected.to include(message: @commit.safe_message) } + it { is_expected.to include(timestamp: @commit.date.xmlschema) } + it { is_expected.to include(url: "#{Gitlab.config.gitlab.url}/#{project.to_param}/commit/#{@commit.id}") } context "with a author" do subject { @push_data[:commits].first[:author] } - it { should include(name: @commit.author_name) } - it { should include(email: @commit.author_email) } + it { is_expected.to include(name: @commit.author_name) } + it { is_expected.to include(email: @commit.author_email) } end end end @@ -95,46 +97,46 @@ describe GitPushService do @event = Event.last end - it { @event.should_not be_nil } - it { @event.project.should == project } - it { @event.action.should == Event::PUSHED } - it { @event.data.should == service.push_data } + it { expect(@event).not_to be_nil } + it { expect(@event.project).to eq(project) } + it { expect(@event.action).to eq(Event::PUSHED) } + it { expect(@event.data).to eq(service.push_data) } end describe "Web Hooks" do context "execute web hooks" do it "when pushing a branch for the first time" do - project.should_receive(:execute_hooks) - project.default_branch.should == "master" - project.protected_branches.should_receive(:create).with({ name: "master", developers_can_push: false }) + expect(project).to receive(:execute_hooks) + expect(project.default_branch).to eq("master") + expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false }) service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') end it "when pushing a branch for the first time with default branch protection disabled" do ApplicationSetting.any_instance.stub(default_branch_protection: 0) - project.should_receive(:execute_hooks) - project.default_branch.should == "master" - project.protected_branches.should_not_receive(:create) + expect(project).to receive(:execute_hooks) + expect(project.default_branch).to eq("master") + expect(project.protected_branches).not_to receive(:create) service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') end it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do ApplicationSetting.any_instance.stub(default_branch_protection: 1) - project.should_receive(:execute_hooks) - project.default_branch.should == "master" - project.protected_branches.should_receive(:create).with({ name: "master", developers_can_push: true }) + expect(project).to receive(:execute_hooks) + expect(project.default_branch).to eq("master") + expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true }) service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') end it "when pushing new commits to existing branch" do - project.should_receive(:execute_hooks) + expect(project).to receive(:execute_hooks) service.execute(project, user, 'oldrev', 'newrev', 'refs/heads/master') end it "when pushing tags" do - project.should_not_receive(:execute_hooks) + expect(project).not_to receive(:execute_hooks) service.execute(project, user, 'newrev', 'newrev', 'refs/tags/v1.0.0') end end @@ -156,7 +158,7 @@ describe GitPushService do end it "creates a note if a pushed commit mentions an issue" do - Note.should_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + expect(Note).to receive(:create_cross_reference_note).with(issue, commit, commit_author, project) service.execute(project, user, @oldrev, @newrev, @ref) end @@ -164,32 +166,32 @@ describe GitPushService do it "only creates a cross-reference note if one doesn't already exist" do Note.create_cross_reference_note(issue, commit, user, project) - Note.should_not_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + expect(Note).not_to receive(:create_cross_reference_note).with(issue, commit, commit_author, project) service.execute(project, user, @oldrev, @newrev, @ref) end it "defaults to the pushing user if the commit's author is not known" do commit.stub(author_name: 'unknown name', author_email: 'unknown@email.com') - Note.should_receive(:create_cross_reference_note).with(issue, commit, user, project) + expect(Note).to receive(:create_cross_reference_note).with(issue, commit, user, project) service.execute(project, user, @oldrev, @newrev, @ref) end it "finds references in the first push to a non-default branch" do - project.repository.stub(:commits_between).with(@blankrev, @newrev).and_return([]) - project.repository.stub(:commits_between).with("master", @newrev).and_return([commit]) + allow(project.repository).to receive(:commits_between).with(@blankrev, @newrev).and_return([]) + allow(project.repository).to receive(:commits_between).with("master", @newrev).and_return([commit]) - Note.should_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + expect(Note).to receive(:create_cross_reference_note).with(issue, commit, commit_author, project) service.execute(project, user, @blankrev, @newrev, 'refs/heads/other') end it "finds references in the first push to a default branch" do - project.repository.stub(:commits_between).with(@blankrev, @newrev).and_return([]) - project.repository.stub(:commits).with(@newrev).and_return([commit]) + allow(project.repository).to receive(:commits_between).with(@blankrev, @newrev).and_return([]) + allow(project.repository).to receive(:commits).with(@newrev).and_return([commit]) - Note.should_receive(:create_cross_reference_note).with(issue, commit, commit_author, project) + expect(Note).to receive(:create_cross_reference_note).with(issue, commit, commit_author, project) service.execute(project, user, @blankrev, @newrev, 'refs/heads/master') end @@ -215,7 +217,7 @@ describe GitPushService do it "closes issues with commit messages" do service.execute(project, user, @oldrev, @newrev, @ref) - Issue.find(issue.id).should be_closed + expect(Issue.find(issue.id)).to be_closed end it "doesn't create cross-reference notes for a closing reference" do @@ -232,7 +234,7 @@ describe GitPushService do service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf') }.not_to change { Note.where(project_id: project.id, system: true).count } - Issue.find(issue.id).should be_opened + expect(Issue.find(issue.id)).to be_opened end end end diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index e65a8204c54..fcf462edbfc 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -19,27 +19,27 @@ describe GitTagPushService do subject { @push_data } - it { should include(ref: @ref) } - it { should include(before: @oldrev) } - it { should include(after: @newrev) } - it { should include(user_id: user.id) } - it { should include(user_name: user.name) } - it { should include(project_id: project.id) } + it { is_expected.to include(ref: @ref) } + it { is_expected.to include(before: @oldrev) } + it { is_expected.to include(after: @newrev) } + it { is_expected.to include(user_id: user.id) } + it { is_expected.to include(user_name: user.name) } + it { is_expected.to include(project_id: project.id) } context 'With repository data' do subject { @push_data[:repository] } - it { should include(name: project.name) } - it { should include(url: project.url_to_repo) } - it { should include(description: project.description) } - it { should include(homepage: project.web_url) } + it { is_expected.to include(name: project.name) } + it { is_expected.to include(url: project.url_to_repo) } + it { is_expected.to include(description: project.description) } + it { is_expected.to include(homepage: project.web_url) } end end describe "Web Hooks" do context "execute web hooks" do it "when pushing tags" do - project.should_receive(:execute_hooks) + expect(project).to receive(:execute_hooks) service.execute(project, user, 'oldrev', 'newrev', 'refs/tags/v1.0.0') end end diff --git a/spec/services/issues/bulk_update_context_spec.rb b/spec/services/issues/bulk_update_context_spec.rb index f4c9148f1a3..eb867f78c5c 100644 --- a/spec/services/issues/bulk_update_context_spec.rb +++ b/spec/services/issues/bulk_update_context_spec.rb @@ -30,11 +30,11 @@ describe Issues::BulkUpdateService do it { result = Issues::BulkUpdateService.new(@project, @user, @params).execute - result[:success].should be_true - result[:count].should == @issues.count + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(@issues.count) - @project.issues.opened.should be_empty - @project.issues.closed.should_not be_empty + expect(@project.issues.opened).to be_empty + expect(@project.issues.closed).not_to be_empty } end @@ -55,11 +55,11 @@ describe Issues::BulkUpdateService do it { result = Issues::BulkUpdateService.new(@project, @user, @params).execute - result[:success].should be_true - result[:count].should == @issues.count + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(@issues.count) - @project.issues.closed.should be_empty - @project.issues.opened.should_not be_empty + expect(@project.issues.closed).to be_empty + expect(@project.issues.opened).not_to be_empty } end @@ -78,10 +78,10 @@ describe Issues::BulkUpdateService do it { result = Issues::BulkUpdateService.new(@project, @user, @params).execute - result[:success].should be_true - result[:count].should == 1 + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) - @project.issues.first.assignee.should == @new_assignee + expect(@project.issues.first.assignee).to eq(@new_assignee) } end @@ -100,10 +100,10 @@ describe Issues::BulkUpdateService do it { result = Issues::BulkUpdateService.new(@project, @user, @params).execute - result[:success].should be_true - result[:count].should == 1 + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) - @project.issues.first.milestone.should == @milestone + expect(@project.issues.first.milestone).to eq(@milestone) } end diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index d4f2cc1339b..d15dff1b52b 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -17,18 +17,18 @@ describe Issues::CloseService do @issue = Issues::CloseService.new(project, user, {}).execute(issue) end - it { @issue.should be_valid } - it { @issue.should be_closed } + it { expect(@issue).to be_valid } + it { expect(@issue).to be_closed } it 'should send email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(issue.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) end it 'should create system note about issue reassign' do note = @issue.notes.last - note.note.should include "Status changed to closed" + expect(note.note).to include "Status changed to closed" end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 90720be5ded..7f1ebcb3198 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -16,8 +16,8 @@ describe Issues::CreateService do @issue = Issues::CreateService.new(project, user, opts).execute end - it { @issue.should be_valid } - it { @issue.title.should == 'Awesome issue' } + it { expect(@issue).to be_valid } + it { expect(@issue.title).to eq('Awesome issue') } end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 964b3a707e4..22b89bec96d 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -27,27 +27,27 @@ describe Issues::UpdateService do @issue.reload end - it { @issue.should be_valid } - it { @issue.title.should == 'New title' } - it { @issue.assignee.should == user2 } - it { @issue.should be_closed } - it { @issue.labels.count.should == 1 } - it { @issue.labels.first.title.should == 'Bug' } + it { expect(@issue).to be_valid } + it { expect(@issue.title).to eq('New title') } + it { expect(@issue.assignee).to eq(user2) } + it { expect(@issue).to be_closed } + it { expect(@issue.labels.count).to eq(1) } + it { expect(@issue.labels.first.title).to eq('Bug') } it 'should send email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(issue.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) end it 'should create system note about issue reassign' do note = @issue.notes.last - note.note.should include "Reassigned to \@#{user2.username}" + expect(note.note).to include "Reassigned to \@#{user2.username}" end it 'should create system note about issue label edit' do note = @issue.notes[1] - note.note.should include "Added ~#{label.id} label" + expect(note.note).to include "Added ~#{label.id} label" end end end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 5060a67bebf..b3cbfd4b5b8 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -16,13 +16,13 @@ describe MergeRequests::CloseService do let(:service) { MergeRequests::CloseService.new(project, user, {}) } before do - service.stub(:execute_hooks) + allow(service).to receive(:execute_hooks) @merge_request = service.execute(merge_request) end - it { @merge_request.should be_valid } - it { @merge_request.should be_closed } + it { expect(@merge_request).to be_valid } + it { expect(@merge_request).to be_closed } it 'should execute hooks with close action' do expect(service).to have_received(:execute_hooks). @@ -31,13 +31,13 @@ describe MergeRequests::CloseService do it 'should send email to user2 about assign of new merge_request' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(merge_request.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(merge_request.title) end it 'should create system note about merge_request reassign' do note = @merge_request.notes.last - note.note.should include 'Status changed to closed' + expect(note.note).to include 'Status changed to closed' end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index dbd21143690..d9bfdf64308 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -18,13 +18,13 @@ describe MergeRequests::CreateService do before do project.team << [user, :master] - service.stub(:execute_hooks) + allow(service).to receive(:execute_hooks) @merge_request = service.execute end - it { @merge_request.should be_valid } - it { @merge_request.title.should == 'Awesome merge_request' } + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.title).to eq('Awesome merge_request') } it 'should execute hooks with default action' do expect(service).to have_received(:execute_hooks).with(@merge_request) diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 5f61fd3187b..0a25fb12f4e 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -16,13 +16,13 @@ describe MergeRequests::MergeService do let(:service) { MergeRequests::MergeService.new(project, user, {}) } before do - service.stub(:execute_hooks) + allow(service).to receive(:execute_hooks) service.execute(merge_request, 'Awesome message') end - it { merge_request.should be_valid } - it { merge_request.should be_merged } + it { expect(merge_request).to be_valid } + it { expect(merge_request).to be_merged } it 'should execute hooks with merge action' do expect(service).to have_received(:execute_hooks). @@ -31,13 +31,13 @@ describe MergeRequests::MergeService do it 'should send email to user2 about merge of new merge_request' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(merge_request.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(merge_request.title) end it 'should create system note about merge_request merge' do note = merge_request.notes.last - note.note.should include 'Status changed to merged' + expect(note.note).to include 'Status changed to merged' end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 35c7aac94df..2830da87814 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -35,10 +35,10 @@ describe MergeRequests::RefreshService do reload_mrs end - it { @merge_request.notes.should_not be_empty } - it { @merge_request.should be_open } - it { @fork_merge_request.should be_open } - it { @fork_merge_request.notes.should be_empty } + it { expect(@merge_request.notes).not_to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } end context 'push to origin repo target branch' do @@ -47,10 +47,10 @@ describe MergeRequests::RefreshService do reload_mrs end - it { @merge_request.notes.last.note.should include('changed to merged') } - it { @merge_request.should be_merged } - it { @fork_merge_request.should be_merged } - it { @fork_merge_request.notes.last.note.should include('changed to merged') } + it { expect(@merge_request.notes.last.note).to include('changed to merged') } + it { expect(@merge_request).to be_merged } + it { expect(@fork_merge_request).to be_merged } + it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } end context 'push to fork repo source branch' do @@ -59,10 +59,10 @@ describe MergeRequests::RefreshService do reload_mrs end - it { @merge_request.notes.should be_empty } - it { @merge_request.should be_open } - it { @fork_merge_request.notes.last.note.should include('new commit') } - it { @fork_merge_request.should be_open } + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes.last.note).to include('new commit') } + it { expect(@fork_merge_request).to be_open } end context 'push to fork repo target branch' do @@ -71,10 +71,10 @@ describe MergeRequests::RefreshService do reload_mrs end - it { @merge_request.notes.should be_empty } - it { @merge_request.should be_open } - it { @fork_merge_request.notes.should be_empty } - it { @fork_merge_request.should be_open } + it { expect(@merge_request.notes).to be_empty } + it { expect(@merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } + it { expect(@fork_merge_request).to be_open } end context 'push to origin repo target branch after fork project was removed' do @@ -84,10 +84,10 @@ describe MergeRequests::RefreshService do reload_mrs end - it { @merge_request.notes.last.note.should include('changed to merged') } - it { @merge_request.should be_merged } - it { @fork_merge_request.should be_open } - it { @fork_merge_request.notes.should be_empty } + it { expect(@merge_request.notes.last.note).to include('changed to merged') } + it { expect(@merge_request).to be_merged } + it { expect(@fork_merge_request).to be_open } + it { expect(@fork_merge_request.notes).to be_empty } end def reload_mrs diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index 2a7066124dc..9401bc3b558 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -16,14 +16,14 @@ describe MergeRequests::ReopenService do let(:service) { MergeRequests::ReopenService.new(project, user, {}) } before do - service.stub(:execute_hooks) + allow(service).to receive(:execute_hooks) merge_request.state = :closed service.execute(merge_request) end - it { merge_request.should be_valid } - it { merge_request.should be_reopened } + it { expect(merge_request).to be_valid } + it { expect(merge_request).to be_reopened } it 'should execute hooks with reopen action' do expect(service).to have_received(:execute_hooks). @@ -32,13 +32,13 @@ describe MergeRequests::ReopenService do it 'should send email to user2 about reopen of merge_request' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(merge_request.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(merge_request.title) end it 'should create system note about merge_request reopen' do note = merge_request.notes.last - note.note.should include 'Status changed to reopened' + expect(note.note).to include 'Status changed to reopened' end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index b27acb47711..916b01e1c45 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -27,18 +27,18 @@ describe MergeRequests::UpdateService do let(:service) { MergeRequests::UpdateService.new(project, user, opts) } before do - service.stub(:execute_hooks) + allow(service).to receive(:execute_hooks) @merge_request = service.execute(merge_request) @merge_request.reload end - it { @merge_request.should be_valid } - it { @merge_request.title.should == 'New title' } - it { @merge_request.assignee.should == user2 } - it { @merge_request.should be_closed } - it { @merge_request.labels.count.should == 1 } - it { @merge_request.labels.first.title.should == 'Bug' } + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.title).to eq('New title') } + it { expect(@merge_request.assignee).to eq(user2) } + it { expect(@merge_request).to be_closed } + it { expect(@merge_request.labels.count).to eq(1) } + it { expect(@merge_request.labels.first.title).to eq('Bug') } it 'should execute hooks with update action' do expect(service).to have_received(:execute_hooks). @@ -47,18 +47,18 @@ describe MergeRequests::UpdateService do it 'should send email to user2 about assign of new merge_request' do email = ActionMailer::Base.deliveries.last - email.to.first.should == user2.email - email.subject.should include(merge_request.title) + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(merge_request.title) end it 'should create system note about merge_request reassign' do note = @merge_request.notes.last - note.note.should include "Reassigned to \@#{user2.username}" + expect(note.note).to include "Reassigned to \@#{user2.username}" end it 'should create system note about merge_request label edit' do note = @merge_request.notes[1] - note.note.should include "Added ~#{label.id} label" + expect(note.note).to include "Added ~#{label.id} label" end end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index f59786efcf9..1a02299bf19 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -18,8 +18,8 @@ describe Notes::CreateService do @note = Notes::CreateService.new(project, user, opts).execute end - it { @note.should be_valid } - it { @note.note.should == 'Awesome comment' } + it { expect(@note).to be_valid } + it { expect(@note.note).to eq('Awesome comment') } end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 2ba1e3372b9..2074f8e7f78 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -7,10 +7,10 @@ describe NotificationService do describe :new_key do let!(:key) { create(:personal_key) } - it { notification.new_key(key).should be_true } + it { expect(notification.new_key(key)).to be_truthy } it 'should sent email to key owner' do - Notify.should_receive(:new_ssh_key_email).with(key.id) + expect(Notify).to receive(:new_ssh_key_email).with(key.id) notification.new_key(key) end end @@ -20,10 +20,10 @@ describe NotificationService do describe :new_email do let!(:email) { create(:email) } - it { notification.new_email(email).should be_true } + it { expect(notification.new_email(email)).to be_truthy } it 'should send email to email owner' do - Notify.should_receive(:new_email_email).with(email.id) + expect(Notify).to receive(:new_email_email).with(email.id) notification.new_email(email) end end @@ -54,7 +54,7 @@ describe NotificationService do it 'filters out "mentioned in" notes' do mentioned_note = Note.create_cross_reference_note(mentioned_issue, issue, issue.author, issue.project) - Notify.should_not_receive(:note_issue_email) + expect(Notify).not_to receive(:note_issue_email) notification.new_note(mentioned_note) end end @@ -87,11 +87,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:note_issue_email).with(user_id, note.id) + expect(Notify).to receive(:note_issue_email).with(user_id, note.id) end def should_not_email(user_id) - Notify.should_not_receive(:note_issue_email).with(user_id, note.id) + expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id) end end @@ -125,17 +125,17 @@ describe NotificationService do it 'filters out "mentioned in" notes' do mentioned_note = Note.create_cross_reference_note(mentioned_issue, issue, issue.author, issue.project) - Notify.should_not_receive(:note_issue_email) + expect(Notify).not_to receive(:note_issue_email) notification.new_note(mentioned_note) end end def should_email(user_id) - Notify.should_receive(:note_issue_email).with(user_id, note.id) + expect(Notify).to receive(:note_issue_email).with(user_id, note.id) end def should_not_email(user_id) - Notify.should_not_receive(:note_issue_email).with(user_id, note.id) + expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id) end end @@ -176,11 +176,11 @@ describe NotificationService do end def should_email(user_id, n) - Notify.should_receive(:note_commit_email).with(user_id, n.id) + expect(Notify).to receive(:note_commit_email).with(user_id, n.id) end def should_not_email(user_id, n) - Notify.should_not_receive(:note_commit_email).with(user_id, n.id) + expect(Notify).not_to receive(:note_commit_email).with(user_id, n.id) end end end @@ -211,11 +211,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:new_issue_email).with(user_id, issue.id) + expect(Notify).to receive(:new_issue_email).with(user_id, issue.id) end def should_not_email(user_id) - Notify.should_not_receive(:new_issue_email).with(user_id, issue.id) + expect(Notify).not_to receive(:new_issue_email).with(user_id, issue.id) end end @@ -231,11 +231,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:reassigned_issue_email).with(user_id, issue.id, nil, @u_disabled.id) + expect(Notify).to receive(:reassigned_issue_email).with(user_id, issue.id, nil, @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:reassigned_issue_email).with(user_id, issue.id, issue.assignee_id, @u_disabled.id) + expect(Notify).not_to receive(:reassigned_issue_email).with(user_id, issue.id, issue.assignee_id, @u_disabled.id) end end @@ -252,11 +252,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) + expect(Notify).to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) + expect(Notify).not_to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id) end end @@ -273,11 +273,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) + expect(Notify).to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) + expect(Notify).not_to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id) end end end @@ -299,11 +299,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:new_merge_request_email).with(user_id, merge_request.id) + expect(Notify).to receive(:new_merge_request_email).with(user_id, merge_request.id) end def should_not_email(user_id) - Notify.should_not_receive(:new_merge_request_email).with(user_id, merge_request.id) + expect(Notify).not_to receive(:new_merge_request_email).with(user_id, merge_request.id) end end @@ -317,11 +317,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:reassigned_merge_request_email).with(user_id, merge_request.id, nil, merge_request.author_id) + expect(Notify).to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, nil, merge_request.author_id) end def should_not_email(user_id) - Notify.should_not_receive(:reassigned_merge_request_email).with(user_id, merge_request.id, merge_request.assignee_id, merge_request.author_id) + expect(Notify).not_to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, merge_request.assignee_id, merge_request.author_id) end end @@ -335,11 +335,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + expect(Notify).to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + expect(Notify).not_to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) end end @@ -353,11 +353,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + expect(Notify).to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) + expect(Notify).not_to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id) end end @@ -371,11 +371,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) + expect(Notify).to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) end def should_not_email(user_id) - Notify.should_not_receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) + expect(Notify).not_to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id) end end end @@ -396,11 +396,11 @@ describe NotificationService do end def should_email(user_id) - Notify.should_receive(:project_was_moved_email).with(project.id, user_id) + expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id) end def should_not_email(user_id) - Notify.should_not_receive(:project_was_moved_email).with(project.id, user_id) + expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id) end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 9c97dad2ff0..8bb48346202 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -16,9 +16,9 @@ describe Projects::CreateService do @project = create_project(@user, @opts) end - it { @project.should be_valid } - it { @project.owner.should == @user } - it { @project.namespace.should == @user.namespace } + it { expect(@project).to be_valid } + it { expect(@project.owner).to eq(@user) } + it { expect(@project.namespace).to eq(@user.namespace) } end context 'group namespace' do @@ -30,9 +30,9 @@ describe Projects::CreateService do @project = create_project(@user, @opts) end - it { @project.should be_valid } - it { @project.owner.should == @group } - it { @project.namespace.should == @group } + it { expect(@project).to be_valid } + it { expect(@project.owner).to eq(@group) } + it { expect(@project.namespace).to eq(@group) } end context 'wiki_enabled creates repository directory' do @@ -42,7 +42,7 @@ describe Projects::CreateService do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { File.exists?(@path).should be_true } + it { expect(File.exists?(@path)).to be_truthy } end context 'wiki_enabled false does not create wiki repository directory' do @@ -52,7 +52,7 @@ describe Projects::CreateService do @path = ProjectWiki.new(@project, @user).send(:path_to_repo) end - it { File.exists?(@path).should be_false } + it { expect(File.exists?(@path)).to be_falsey } end end end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 5c80345c2b3..e55a2e3f8a0 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -16,18 +16,18 @@ describe Projects::ForkService do describe "successfully creates project in the user namespace" do let(:to_project) { fork_project(@from_project, @to_user) } - it { to_project.owner.should == @to_user } - it { to_project.namespace.should == @to_user.namespace } - it { to_project.star_count.should be_zero } - it { to_project.description.should == @from_project.description } + it { expect(to_project.owner).to eq(@to_user) } + it { expect(to_project.namespace).to eq(@to_user.namespace) } + it { expect(to_project.star_count).to be_zero } + it { expect(to_project.description).to eq(@from_project.description) } end end context 'fork project failure' do it "fails due to transaction failure" do @to_project = fork_project(@from_project, @to_user, false) - @to_project.errors.should_not be_empty - @to_project.errors[:base].should include("Fork transaction failed.") + expect(@to_project.errors).not_to be_empty + expect(@to_project.errors[:base]).to include("Fork transaction failed.") end end @@ -35,9 +35,9 @@ describe Projects::ForkService do it "should fail due to validation, not transaction failure" do @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @to_project = fork_project(@from_project, @to_user) - @existing_project.persisted?.should be_true - @to_project.errors[:base].should include("Invalid fork destination") - @to_project.errors[:base].should_not include("Fork transaction failed.") + expect(@existing_project.persisted?).to be_truthy + expect(@to_project.errors[:base]).to include("Invalid fork destination") + expect(@to_project.errors[:base]).not_to include("Fork transaction failed.") end end end @@ -58,19 +58,19 @@ describe Projects::ForkService do context 'fork project for group' do it 'group owner successfully forks project into the group' do to_project = fork_project(@project, @group_owner, true, @opts) - to_project.owner.should == @group - to_project.namespace.should == @group - to_project.name.should == @project.name - to_project.path.should == @project.path - to_project.description.should == @project.description - to_project.star_count.should be_zero + expect(to_project.owner).to eq(@group) + expect(to_project.namespace).to eq(@group) + expect(to_project.name).to eq(@project.name) + expect(to_project.path).to eq(@project.path) + expect(to_project.description).to eq(@project.description) + expect(to_project.star_count).to be_zero end end context 'fork project for group when user not owner' do it 'group developer should fail to fork project into the group' do to_project = fork_project(@project, @developer, true, @opts) - to_project.errors[:namespace].should == ['insufficient access rights'] + expect(to_project.errors[:namespace]).to eq(['insufficient access rights']) end end @@ -79,10 +79,10 @@ describe Projects::ForkService do existing_project = create(:project, name: @project.name, namespace: @group) to_project = fork_project(@project, @group_owner, true, @opts) - existing_project.persisted?.should be_true - to_project.errors[:base].should == ['Invalid fork destination'] - to_project.errors[:name].should == ['has already been taken'] - to_project.errors[:path].should == ['has already been taken'] + expect(existing_project.persisted?).to be_truthy + expect(to_project.errors[:base]).to eq(['Invalid fork destination']) + expect(to_project.errors[:name]).to eq(['has already been taken']) + expect(to_project.errors[:path]).to eq(['has already been taken']) end end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 79d0526ff89..46fb5f5fae5 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -11,8 +11,8 @@ describe Projects::TransferService do @result = transfer_project(project, user, namespace_id: group.id) end - it { @result.should be_true } - it { project.namespace.should == group } + it { expect(@result).to be_truthy } + it { expect(project.namespace).to eq(group) } end context 'namespace -> no namespace' do @@ -20,9 +20,9 @@ describe Projects::TransferService do @result = transfer_project(project, user, namespace_id: nil) end - it { @result.should_not be_nil } # { result.should be_false } passes on nil - it { @result.should be_false } - it { project.namespace.should == user.namespace } + it { expect(@result).not_to be_nil } # { result.should be_false } passes on nil + it { expect(@result).to be_falsey } + it { expect(project.namespace).to eq(user.namespace) } end context 'namespace -> not allowed namespace' do @@ -30,9 +30,9 @@ describe Projects::TransferService do @result = transfer_project(project, user, namespace_id: group.id) end - it { @result.should_not be_nil } # { result.should be_false } passes on nil - it { @result.should be_false } - it { project.namespace.should == user.namespace } + it { expect(@result).not_to be_nil } # { result.should be_false } passes on nil + it { expect(@result).to be_falsey } + it { expect(project.namespace).to eq(user.namespace) } end def transfer_project(project, user, params) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 5a10174eb36..10dbc548e86 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -17,8 +17,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.private?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.private?).to be_truthy } end context 'should be internal when updated to internal' do @@ -29,8 +29,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.internal?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.internal?).to be_truthy } end context 'should be public when updated to public' do @@ -41,14 +41,14 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.public?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.public?).to be_truthy } end context 'respect configured visibility restrictions setting' do before(:each) do @restrictions = double("restrictions") - @restrictions.stub(:restricted_visibility_levels) { [ "public" ] } + allow(@restrictions).to receive(:restricted_visibility_levels) { [ "public" ] } Settings.stub_chain(:gitlab).and_return(@restrictions) end @@ -60,8 +60,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.private?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.private?).to be_truthy } end context 'should be internal when updated to internal' do @@ -72,8 +72,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.internal?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.internal?).to be_truthy } end context 'should be private when updated to public' do @@ -84,8 +84,8 @@ describe Projects::UpdateService do update_project(@project, @user, @opts) end - it { @created_private.should be_true } - it { @project.private?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.private?).to be_truthy } end context 'should be public when updated to public by admin' do @@ -96,8 +96,8 @@ describe Projects::UpdateService do update_project(@project, @admin, @opts) end - it { @created_private.should be_true } - it { @project.public?.should be_true } + it { expect(@created_private).to be_truthy } + it { expect(@project.public?).to be_truthy } end end end diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 3217c571e67..f57bfaea879 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -19,7 +19,7 @@ describe 'Search::GlobalService' do it 'should return public projects only' do context = Search::GlobalService.new(nil, search: "searchable") results = context.execute - results.objects('projects').should match_array [public_project] + expect(results.objects('projects')).to match_array [public_project] end end @@ -27,19 +27,19 @@ describe 'Search::GlobalService' do it 'should return public, internal and private projects' do context = Search::GlobalService.new(user, search: "searchable") results = context.execute - results.objects('projects').should match_array [public_project, found_project, internal_project] + expect(results.objects('projects')).to match_array [public_project, found_project, internal_project] end it 'should return only public & internal projects' do context = Search::GlobalService.new(internal_user, search: "searchable") results = context.execute - results.objects('projects').should match_array [internal_project, public_project] + expect(results.objects('projects')).to match_array [internal_project, public_project] end it 'namespace name should be searchable' do context = Search::GlobalService.new(user, search: found_project.namespace.path) results = context.execute - results.objects('projects').should match_array [found_project] + expect(results.objects('projects')).to match_array [found_project] end end end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index a45e9d0575c..199ac996608 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -9,35 +9,35 @@ describe SystemHooksService do let (:group_member) { create(:group_member) } context 'event data' do - it { event_data(user, :create).should include(:event_name, :name, :created_at, :email, :user_id) } - it { event_data(user, :destroy).should include(:event_name, :name, :created_at, :email, :user_id) } - it { event_data(project, :create).should include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { event_data(project, :destroy).should include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { event_data(project_member, :create).should include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { event_data(project_member, :destroy).should include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { event_data(key, :create).should include(:username, :key, :id) } - it { event_data(key, :destroy).should include(:username, :key, :id) } + it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :email, :user_id) } + it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) } + it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } + it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } + it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(key, :create)).to include(:username, :key, :id) } + it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } it do - event_data(group, :create).should include( + expect(event_data(group, :create)).to include( :event_name, :name, :created_at, :path, :group_id, :owner_name, :owner_email ) end it do - event_data(group, :destroy).should include( + expect(event_data(group, :destroy)).to include( :event_name, :name, :created_at, :path, :group_id, :owner_name, :owner_email ) end it do - event_data(group_member, :create).should include( + expect(event_data(group_member, :create)).to include( :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, :user_name, :user_email, :group_access ) end it do - event_data(group_member, :destroy).should include( + expect(event_data(group_member, :destroy)).to include( :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, :user_name, :user_email, :group_access ) @@ -45,18 +45,18 @@ describe SystemHooksService do end context 'event names' do - it { event_name(user, :create).should eq "user_create" } - it { event_name(user, :destroy).should eq "user_destroy" } - it { event_name(project, :create).should eq "project_create" } - it { event_name(project, :destroy).should eq "project_destroy" } - it { event_name(project_member, :create).should eq "user_add_to_team" } - it { event_name(project_member, :destroy).should eq "user_remove_from_team" } - it { event_name(key, :create).should eq 'key_create' } - it { event_name(key, :destroy).should eq 'key_destroy' } - it { event_name(group, :create).should eq 'group_create' } - it { event_name(group, :destroy).should eq 'group_destroy' } - it { event_name(group_member, :create).should eq 'user_add_to_group' } - it { event_name(group_member, :destroy).should eq 'user_remove_from_group' } + it { expect(event_name(user, :create)).to eq "user_create" } + it { expect(event_name(user, :destroy)).to eq "user_destroy" } + it { expect(event_name(project, :create)).to eq "project_create" } + it { expect(event_name(project, :destroy)).to eq "project_destroy" } + it { expect(event_name(project_member, :create)).to eq "user_add_to_team" } + it { expect(event_name(project_member, :destroy)).to eq "user_remove_from_team" } + it { expect(event_name(key, :create)).to eq 'key_create' } + it { expect(event_name(key, :destroy)).to eq 'key_destroy' } + it { expect(event_name(group, :create)).to eq 'group_create' } + it { expect(event_name(group, :destroy)).to eq 'group_destroy' } + it { expect(event_name(group_member, :create)).to eq 'user_add_to_group' } + it { expect(event_name(group_member, :destroy)).to eq 'user_remove_from_group' } end def event_data(*args) diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb index 76af5bf7b88..d2b505f55a2 100644 --- a/spec/services/test_hook_service_spec.rb +++ b/spec/services/test_hook_service_spec.rb @@ -8,7 +8,7 @@ describe TestHookService do describe :execute do it "should execute successfully" do stub_request(:post, hook.url).to_return(status: 200) - TestHookService.new.execute(hook, user).should be_true + expect(TestHookService.new.execute(hook, user)).to be_truthy end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8352516a665..eaec2198dc8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -38,6 +38,7 @@ RSpec.configure do |config| config.include TestEnv config.infer_spec_type_from_file_location! + config.raise_errors_for_deprecations! config.before(:suite) do TestEnv.init diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index d2d532d9738..cca7652093a 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -36,4 +36,15 @@ RSpec.configure do |config| config.after(:each) do DatabaseCleaner.clean end + + # rspec-rails 3 will no longer automatically infer an example group's spec type + # from the file location. You can explicitly opt-in to the feature using this + # config option. + # To explicitly tag specs without using automatic inference, set the `:type` + # metadata manually: + # + # describe ThingsController, :type => :controller do + # # Equivalent to being in spec/controllers + # end + config.infer_spec_type_from_file_location! end diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index ebd74206699..305592fa5a6 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -39,7 +39,7 @@ def common_mentionable_setup # unrecognized commits. commitmap = { '1234567890a' => mentioned_commit } extra_commits.each { |c| commitmap[c.short_id] = c } - mproject.repository.stub(:commit) { |sha| commitmap[sha] } + allow(mproject.repository).to receive(:commit) { |sha| commitmap[sha] } set_mentionable_text.call(ref_string) end end @@ -48,19 +48,19 @@ shared_examples 'a mentionable' do common_mentionable_setup it 'generates a descriptive back-reference' do - subject.gfm_reference.should == backref_text + expect(subject.gfm_reference).to eq(backref_text) end it "extracts references from its reference property" do # De-duplicate and omit itself refs = subject.references(mproject) - refs.should have(6).items - refs.should include(mentioned_issue) - refs.should include(mentioned_mr) - refs.should include(mentioned_commit) - refs.should include(ext_issue) - refs.should include(ext_mr) - refs.should include(ext_commit) + expect(refs.size).to eq(6) + expect(refs).to include(mentioned_issue) + expect(refs).to include(mentioned_mr) + expect(refs).to include(mentioned_commit) + expect(refs).to include(ext_issue) + expect(refs).to include(ext_mr) + expect(refs).to include(ext_commit) end it 'creates cross-reference notes' do @@ -68,7 +68,7 @@ shared_examples 'a mentionable' do ext_issue, ext_mr, ext_commit] mentioned_objects.each do |referenced| - Note.should_receive(:create_cross_reference_note).with(referenced, subject.local_reference, mauthor, mproject) + expect(Note).to receive(:create_cross_reference_note).with(referenced, subject.local_reference, mauthor, mproject) end subject.create_cross_references!(mproject, mauthor) @@ -77,8 +77,8 @@ shared_examples 'a mentionable' do it 'detects existing cross-references' do Note.create_cross_reference_note(mentioned_issue, subject.local_reference, mauthor, mproject) - subject.has_mentioned?(mentioned_issue).should be_true - subject.has_mentioned?(mentioned_mr).should be_false + expect(subject.has_mentioned?(mentioned_issue)).to be_truthy + expect(subject.has_mentioned?(mentioned_mr)).to be_falsey end end @@ -95,12 +95,12 @@ shared_examples 'an editable mentionable' do "#{ext_proj.path_with_namespace}##{other_ext_issue.iid}" [mentioned_issue, mentioned_commit, ext_issue].each do |oldref| - Note.should_not_receive(:create_cross_reference_note).with(oldref, subject.local_reference, + expect(Note).not_to receive(:create_cross_reference_note).with(oldref, subject.local_reference, mauthor, mproject) end [other_issue, other_ext_issue].each do |newref| - Note.should_receive(:create_cross_reference_note).with( + expect(Note).to receive(:create_cross_reference_note).with( newref, subject.local_reference, mauthor, diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb index 42252675683..490f453d468 100644 --- a/spec/support/taskable_shared_examples.rb +++ b/spec/support/taskable_shared_examples.rb @@ -34,9 +34,9 @@ EOT end it 'knows if it has tasks' do - expect(subject.tasks?).to be_true + expect(subject.tasks?).to be_truthy subject.description = 'Now I have no tasks' - expect(subject.tasks?).to be_false + expect(subject.tasks?).to be_falsey end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 24fee7c0379..1c150cbfe25 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -19,8 +19,6 @@ module TestEnv # See gitlab.yml.example test section for paths # def init(opts = {}) - RSpec::Mocks::setup(self) - # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false @@ -49,7 +47,7 @@ module TestEnv end def enable_mailer - NotificationService.any_instance.unstub(:mailer) + allow_any_instance_of(NotificationService).to receive(:mailer).and_call_original end def setup_gitlab_shell diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 71a45eb2fa6..60942cc95fc 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -13,7 +13,7 @@ describe 'gitlab:app namespace rake task' do describe 'backup_restore' do before do # avoid writing task output to spec progress - $stdout.stub :write + allow($stdout).to receive :write end let :run_rake_task do @@ -24,7 +24,7 @@ describe 'gitlab:app namespace rake task' do context 'gitlab version' do before do Dir.stub glob: [] - Dir.stub :chdir + allow(Dir).to receive :chdir File.stub exists?: true Kernel.stub system: true FileUtils.stub cp_r: true @@ -41,9 +41,9 @@ describe 'gitlab:app namespace rake task' do it 'should invoke restoration on mach' do YAML.stub load_file: {gitlab_version: gitlab_version} - Rake::Task["gitlab:backup:db:restore"].should_receive :invoke - Rake::Task["gitlab:backup:repo:restore"].should_receive :invoke - Rake::Task["gitlab:shell:setup"].should_receive :invoke + expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke + expect(Rake::Task["gitlab:backup:repo:restore"]).to receive :invoke + expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke expect { run_rake_task }.to_not raise_error end end diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb index 45aaf0fc90b..22e746870dc 100644 --- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb +++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb @@ -12,7 +12,7 @@ describe 'gitlab:mail_google_schema_whitelisting rake task' do describe 'call' do before do # avoid writing task output to spec progress - $stdout.stub :write + allow($stdout).to receive :write end let :run_rake_task do diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 4273fd1019a..8eabc46112b 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe PostReceive do context "as a resque worker" do it "reponds to #perform" do - PostReceive.new.should respond_to(:perform) + expect(PostReceive.new).to respond_to(:perform) end end @@ -13,23 +13,23 @@ describe PostReceive do let(:key_id) { key.shell_id } it "fetches the correct project" do - Project.should_receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) + expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) PostReceive.new.perform(pwd(project), key_id, changes) end it "does not run if the author is not in the project" do - Key.stub(:find_by).with(hash_including(id: anything())) { nil } + allow(Key).to receive(:find_by).with(hash_including(id: anything())) { nil } - project.should_not_receive(:execute_hooks) + expect(project).not_to receive(:execute_hooks) - PostReceive.new.perform(pwd(project), key_id, changes).should be_false + expect(PostReceive.new.perform(pwd(project), key_id, changes)).to be_falsey end it "asks the project to trigger all hooks" do Project.stub(find_with_namespace: project) - project.should_receive(:execute_hooks) - project.should_receive(:execute_services) - project.should_receive(:update_merge_requests) + expect(project).to receive(:execute_hooks) + expect(project).to receive(:execute_services) + expect(project).to receive(:update_merge_requests) PostReceive.new.perform(pwd(project), key_id, changes) end -- GitLab From 940a402b6e015cde1b3513808cb9ac7f0a3bec8c Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Thu, 12 Feb 2015 19:32:58 +0100 Subject: [PATCH 0862/1609] Fixed hound warnings Signed-off-by: Jeroen van Baarsen --- spec/controllers/blob_controller_spec.rb | 5 ++++- spec/controllers/branches_controller_spec.rb | 14 ++++++++++---- spec/controllers/commit_controller_spec.rb | 3 ++- spec/controllers/import/github_controller_spec.rb | 12 ++++++++---- spec/controllers/import/gitlab_controller_spec.rb | 9 +++++---- spec/controllers/merge_requests_controller_spec.rb | 3 ++- 6 files changed, 31 insertions(+), 15 deletions(-) diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb index 02f418053fa..02a9db61255 100644 --- a/spec/controllers/blob_controller_spec.rb +++ b/spec/controllers/blob_controller_spec.rb @@ -45,7 +45,10 @@ describe Projects::BlobController do context 'redirect to tree' do let(:id) { 'markdown/doc' } - it { is_expected.to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc") } + it "redirects" do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc") + end end end end diff --git a/spec/controllers/branches_controller_spec.rb b/spec/controllers/branches_controller_spec.rb index d31870058ca..0c39d016440 100644 --- a/spec/controllers/branches_controller_spec.rb +++ b/spec/controllers/branches_controller_spec.rb @@ -27,25 +27,31 @@ describe Projects::BranchesController do context "valid branch name, valid source" do let(:branch) { "merge_branch" } let(:ref) { "master" } - it { is_expected.to redirect_to("/#{project.path_with_namespace}/tree/merge_branch") } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/merge_branch") + end end context "invalid branch name, valid ref" do let(:branch) { "" } let(:ref) { "master" } - it { is_expected.to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") + end end context "valid branch name, invalid ref" do let(:branch) { "merge_branch" } let(:ref) { "" } - it { is_expected.to render_template("new") } + it { is_expected.to render_template('new') } end context "invalid branch name, invalid ref" do let(:branch) { "" } let(:ref) { "" } - it { is_expected.to render_template("new") } + it { is_expected.to render_template('new') } end end end diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index 507fd4e6ba7..8f0d0261e6d 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -31,7 +31,8 @@ describe Projects::CommitController do end it "should not escape Html" do - allow_any_instance_of(Commit).to receive(:"to_#{format}").and_return('HTML entities &<>" ') + allow_any_instance_of(Commit).to receive(:"to_#{format}") + .and_return('HTML entities &<>" ') get :show, project_id: project.to_param, id: commit.id, format: format diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 30bf54a908c..69469e5d8f3 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -10,11 +10,14 @@ describe Import::GithubController do describe "GET callback" do it "updates access token" do token = "asdasd12345" - allow_any_instance_of(Gitlab::GithubImport::Client).to receive(:get_token).and_return(token) - Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "github") + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:get_token).and_return(token) + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", + app_secret: "asd123", + name: "github") get :callback - + expect(user.reload.github_access_token).to eq(token) expect(controller).to redirect_to(status_import_github_url) end @@ -55,7 +58,8 @@ describe Import::GithubController do it "takes already existing namespace" do namespace = create(:namespace, name: "john", owner: user) - expect(Gitlab::GithubImport::ProjectCreator).to receive(:new).with(@repo, namespace, user). + expect(Gitlab::GithubImport::ProjectCreator). + to receive(:new).with(@repo, namespace, user). and_return(double(execute: true)) controller.stub_chain(:client, :repo).and_return(@repo) diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 322dec04a1e..287aa315db5 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -14,7 +14,7 @@ describe Import::GitlabController do Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "gitlab") get :callback - + expect(user.reload.gitlab_access_token).to eq(token) expect(controller).to redirect_to(status_import_gitlab_url) end @@ -28,7 +28,7 @@ describe Import::GitlabController do it "assigns variables" do @project = create(:project, import_type: 'gitlab', creator_id: user.id) controller.stub_chain(:client, :projects).and_return([@repo]) - + get :status expect(assigns(:already_added_projects)).to eq([@project]) @@ -38,7 +38,7 @@ describe Import::GitlabController do it "does not show already added project" do @project = create(:project, import_type: 'gitlab', creator_id: user.id, import_source: 'asd/vim') controller.stub_chain(:client, :projects).and_return([@repo]) - + get :status expect(assigns(:already_added_projects)).to eq([@project]) @@ -58,7 +58,8 @@ describe Import::GitlabController do it "takes already existing namespace" do namespace = create(:namespace, name: "john", owner: user) - expect(Gitlab::GitlabImport::ProjectCreator).to receive(:new).with(@repo, namespace, user). + expect(Gitlab::GitlabImport::ProjectCreator). + to receive(:new).with(@repo, namespace, user). and_return(double(execute: true)) controller.stub_chain(:client, :project).and_return(@repo) diff --git a/spec/controllers/merge_requests_controller_spec.rb b/spec/controllers/merge_requests_controller_spec.rb index fde34e480bf..eedaf17941a 100644 --- a/spec/controllers/merge_requests_controller_spec.rb +++ b/spec/controllers/merge_requests_controller_spec.rb @@ -31,7 +31,8 @@ describe Projects::MergeRequestsController do end it "should not escape Html" do - allow_any_instance_of(MergeRequest).to receive(:"to_#{format}").and_return('HTML entities &<>" ') + allow_any_instance_of(MergeRequest).to receive(:"to_#{format}"). + and_return('HTML entities &<>" ') get :show, project_id: project.to_param, id: merge_request.iid, format: format -- GitLab From 5bb743efec0043d79ac508503c9e28bee5fae48f Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Thu, 12 Feb 2015 19:48:42 +0100 Subject: [PATCH 0863/1609] Fixed tests for spinach Signed-off-by: Jeroen van Baarsen --- features/support/env.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/support/env.rb b/features/support/env.rb index 67660777842..be17065ccfd 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -47,8 +47,8 @@ Spinach.hooks.after_scenario do end Spinach.hooks.before_run do + include RSpec::Mocks::ExampleMethods TestEnv.init(mailer: false) - RSpec::Mocks::setup self include FactoryGirl::Syntax::Methods end -- GitLab From 378520bd8be0a23510c9beea5987e10343194fb5 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 12 Feb 2015 10:53:01 -0800 Subject: [PATCH 0864/1609] Add a test for service template. --- spec/models/service_spec.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 10cbafebd95..1df34f56cf1 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -60,4 +60,29 @@ describe Service do end end end + + describe "Template" do + describe "for pushover service" do + let(:service_template) { + PushoverService.create(template: true, properties: {device: 'MyDevice', sound: 'mic', priority: 4, api_key: '123456789'}) + } + let(:project) { create(:project) } + + describe 'should be prefilled for projects pushover service' do + before do + service_template + project.build_missing_services + end + + it "should have all fields prefilled" do + service = project.pushover_service + expect(service.template).to eq(false) + expect(service.device).to eq('MyDevice') + expect(service.sound).to eq('mic') + expect(service.priority).to eq(4) + expect(service.api_key).to eq('123456789') + end + end + end + end end -- GitLab From e8271226b1a474f097909b8006d78dd60bbca7be Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 12 Feb 2015 10:57:08 -0800 Subject: [PATCH 0865/1609] Use the service_name. --- app/controllers/admin/services_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 554a7d83d9f..e80cabd6e18 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -26,8 +26,8 @@ class Admin::ServicesController < Admin::ApplicationController def services_templates templates = [] - Service.available_services_names.each do |service| - service_template = service.concat("_service").camelize.constantize + Service.available_services_names.each do |service_name| + service_template = service_name.concat("_service").camelize.constantize templates << service_template.where(template: true).first_or_create end -- GitLab From 4377ba1c360cf6f4d15e3b5ad2a7ed7bc41f795e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 12 Feb 2015 20:34:34 +0100 Subject: [PATCH 0866/1609] Use gitattribute merge=union to reduce CHANGELOG merge conflicts. --- .gitattributes | 1 + CHANGELOG | 27 --------------------------- doc/release/monthly.md | 9 +-------- 3 files changed, 2 insertions(+), 35 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..7e800609e6c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG merge=union \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG index 52a41c7df3d..9bb75fdf884 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,3 @@ -Note: The upcoming release below contains empty lines. -This helps to reduce the number of merge conflicts. -Scroll down to see the released versions of GitLab. -Please pick a random empty line to add new content. - v 7.8.0 (unreleased) - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) @@ -19,58 +14,36 @@ v 7.8.0 (unreleased) - Add notes for label changes in issue and merge requests - Show tags in commit view (Hannes Rosenögger) - Only count a user's vote once on a merge request or issue (Michael Clarke) - - - Increate font size when browse source files and diffs - Create new file in empty repository using GitLab UI - - - Ability to clone project using oauth2 token - - - Upgrade Sidekiq gem to version 3.3.0 - Stop git zombie creation during force push check - Show success/error messages for test setting button in services - Added Rubocop for code style checks - Fix commits pagination - - - Async load a branch information at the commit page - Disable blacklist validation for project names - Allow configuring protection of the default branch upon first push (Marco Wessel) - - - Add gitlab.com importer - Add an ability to login with gitlab.com - - - Add a commit calendar to the user profile (Hannes Rosenögger) - - - Submit comment on command-enter - Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`. - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" - - - Fix long broadcast message cut-off on left sidebar (Visay Keo) - Add Project Avatars (Steven Thonus and Hannes Rosenögger) - - - - - Password reset token validity increased from 2 hours to 2 days since it is also send on account creation. - - - Edit group members via API - Enable raw image paste from clipboard, currently Chrome only (Marco Cyriacks) - - - - - Add action property to merge request hook (Julien Bianchi) - - - Remove duplicates from group milestone participants list. - - - - - Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger) - - - - - API: Access groups with their path (Julien Bianchi) - Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard) - - - Allow notification email to be set separately from primary email. - - - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) - - - Don't have Markdown preview fail for long comments/wiki pages. - - - When test web hook - show error message instead of 500 error page if connection to hook url was reset - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) - Added persistent collapse button for left side nav bar (Jason Blanchard) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index 12376d36a9a..c9e6d3426bc 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -87,20 +87,13 @@ asked if there is anything missing. There are three changelogs that need to be updated: CE, EE and CI. -Remove the Note text in the stable branches. - ## Create RC1 (CE, EE, CI) [Follow this How-to guide](howto_rc1.md) to create RC1. ## Prepare CHANGELOG for next release -Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version and add 70 empty -lines to it. We do this in order to avoid merge conflicts when merging the CHANGELOG. - -Make sure that the CHANGELOG im master contains the following disclaimer message: - -> Note: The upcoming release contains empty lines to reduce the number of merge conflicts, scroll down to see past releases. +Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version. ## QA -- GitLab From 026e988544f282c87afec9a85ff21a23877f6226 Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Thu, 12 Feb 2015 19:53:23 +0100 Subject: [PATCH 0867/1609] Even more hound fixes Signed-off-by: Jeroen van Baarsen --- spec/controllers/blob_controller_spec.rb | 2 +- spec/controllers/commit_controller_spec.rb | 4 +- .../import/github_controller_spec.rb | 6 +- spec/controllers/tree_controller_spec.rb | 6 +- spec/features/admin/admin_users_spec.rb | 22 +-- spec/features/atom/dashboard_issues_spec.rb | 13 +- spec/features/atom/dashboard_spec.rb | 5 +- spec/features/atom/issues_spec.rb | 33 +++-- spec/features/atom/users_spec.rb | 18 ++- spec/features/issues_spec.rb | 93 +++++++------ spec/features/notes_on_merge_requests_spec.rb | 131 ++++++++++-------- spec/features/profile_spec.rb | 16 +-- spec/helpers/application_helper_spec.rb | 30 ++-- .../helpers/broadcast_messages_helper_spec.rb | 3 +- spec/helpers/diff_helper_spec.rb | 17 ++- spec/helpers/gitlab_markdown_helper_spec.rb | 58 +++++--- 16 files changed, 266 insertions(+), 191 deletions(-) diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb index 02a9db61255..ea52e4d212a 100644 --- a/spec/controllers/blob_controller_spec.rb +++ b/spec/controllers/blob_controller_spec.rb @@ -45,7 +45,7 @@ describe Projects::BlobController do context 'redirect to tree' do let(:id) { 'markdown/doc' } - it "redirects" do + it 'redirects' do expect(subject). to redirect_to("/#{project.path_with_namespace}/tree/markdown/doc") end diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index 8f0d0261e6d..f0e39e674fd 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -31,8 +31,8 @@ describe Projects::CommitController do end it "should not escape Html" do - allow_any_instance_of(Commit).to receive(:"to_#{format}") - .and_return('HTML entities &<>" ') + allow_any_instance_of(Commit).to receive(:"to_#{format}"). + and_return('HTML entities &<>" ') get :show, project_id: project.to_param, id: commit.id, format: format diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 69469e5d8f3..3b779855d3f 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -12,9 +12,9 @@ describe Import::GithubController do token = "asdasd12345" allow_any_instance_of(Gitlab::GithubImport::Client). to receive(:get_token).and_return(token) - Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", - app_secret: "asd123", - name: "github") + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: 'asd123', + app_secret: 'asd123', + name: 'github') get :callback diff --git a/spec/controllers/tree_controller_spec.rb b/spec/controllers/tree_controller_spec.rb index c228584c886..805e0a8795b 100644 --- a/spec/controllers/tree_controller_spec.rb +++ b/spec/controllers/tree_controller_spec.rb @@ -50,7 +50,11 @@ describe Projects::TreeController do context 'redirect to blob' do let(:id) { 'master/README.md' } - it { is_expected.to redirect_to("/#{project.path_with_namespace}/blob/master/README.md") } + it 'redirects' do + redirect_url = "/#{project.path_with_namespace}/blob/master/README.md" + expect(subject). + to redirect_to(redirect_url) + end end end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index c6c9f1f33c1..f97b69713ce 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -33,15 +33,17 @@ describe "Admin::Users", feature: true do it "should apply defaults to user" do click_button "Create user" user = User.find_by(username: 'bang') - expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) - expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) + expect(user.projects_limit). + to eq(Gitlab.config.gitlab.default_projects_limit) + expect(user.can_create_group). + to eq(Gitlab.config.gitlab.default_can_create_group) end it "should create user with valid data" do click_button "Create user" user = User.find_by(username: 'bang') - expect(user.name).to eq("Big Bang") - expect(user.email).to eq("bigbang@mail.com") + expect(user.name).to eq('Big Bang') + expect(user.email).to eq('bigbang@mail.com') end it "should call send mail" do @@ -54,7 +56,7 @@ describe "Admin::Users", feature: true do click_button "Create user" user = User.find_by(username: 'bang') email = ActionMailer::Base.deliveries.last - expect(email.subject).to have_content("Account was created") + expect(email.subject).to have_content('Account was created') expect(email.text_part.body).to have_content(user.email) expect(email.text_part.body).to have_content('password') end @@ -80,8 +82,8 @@ describe "Admin::Users", feature: true do end it "should have user edit page" do - expect(page).to have_content("Name") - expect(page).to have_content("Password") + expect(page).to have_content('Name') + expect(page).to have_content('Password') end describe "Update user" do @@ -93,13 +95,13 @@ describe "Admin::Users", feature: true do end it "should show page with new data" do - expect(page).to have_content("bigbang@mail.com") - expect(page).to have_content("Big Bang") + expect(page).to have_content('bigbang@mail.com') + expect(page).to have_content('Big Bang') end it "should change user entry" do @simple_user.reload - expect(@simple_user.name).to eq("Big Bang") + expect(@simple_user.name).to eq('Big Bang') expect(@simple_user.is_admin?).to be_truthy end end diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index ceeb3e6c5aa..b710cb3c72f 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -17,12 +17,13 @@ describe "Dashboard Issues Feed", feature: true do it "should render atom feed via private token" do visit issues_dashboard_path(:atom, private_token: user.private_token) - expect(response_headers['Content-Type']).to have_content("application/atom+xml") - expect(body).to have_selector("title", text: "#{user.name} issues") - expect(body).to have_selector("author email", text: issue1.author_email) - expect(body).to have_selector("entry summary", text: issue1.title) - expect(body).to have_selector("author email", text: issue2.author_email) - expect(body).to have_selector("entry summary", text: issue2.title) + expect(response_headers['Content-Type']). + to have_content('application/atom+xml') + expect(body).to have_selector('title', text: "#{user.name} issues") + expect(body).to have_selector('author email', text: issue1.author_email) + expect(body).to have_selector('entry summary', text: issue1.title) + expect(body).to have_selector('author email', text: issue2.author_email) + expect(body).to have_selector('entry summary', text: issue2.title) end end end diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index 8e723b5c2ac..ad157d742ff 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -7,7 +7,7 @@ describe "Dashboard Feed", feature: true do context "projects atom feed via private token" do it "should render projects atom feed" do visit dashboard_path(:atom, private_token: user.private_token) - expect(body).to have_selector("feed title") + expect(body).to have_selector('feed title') end end @@ -28,7 +28,8 @@ describe "Dashboard Feed", feature: true do end it "should have issue comment event" do - expect(body).to have_content("#{user.name} commented on issue ##{issue.iid}") + expect(body). + to have_content("#{user.name} commented on issue ##{issue.iid}") end end end diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 26422c8fdc0..43163e4113e 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -1,33 +1,36 @@ require 'spec_helper' -describe "Issues Feed", feature: true do - describe "GET /issues" do +describe 'Issues Feed', feature: true do + describe 'GET /issues' do let!(:user) { create(:user) } let!(:project) { create(:project) } let!(:issue) { create(:issue, author: user, project: project) } before { project.team << [user, :developer] } - context "when authenticated" do - it "should render atom feed" do + context 'when authenticated' do + it 'should render atom feed' do login_with user visit project_issues_path(project, :atom) - expect(response_headers['Content-Type']).to have_content("application/atom+xml") - expect(body).to have_selector("title", text: "#{project.name} issues") - expect(body).to have_selector("author email", text: issue.author_email) - expect(body).to have_selector("entry summary", text: issue.title) + expect(response_headers['Content-Type']). + to have_content('application/atom+xml') + expect(body).to have_selector('title', text: "#{project.name} issues") + expect(body).to have_selector('author email', text: issue.author_email) + expect(body).to have_selector('entry summary', text: issue.title) end end - context "when authenticated via private token" do - it "should render atom feed" do - visit project_issues_path(project, :atom, private_token: user.private_token) + context 'when authenticated via private token' do + it 'should render atom feed' do + visit project_issues_path(project, :atom, + private_token: user.private_token) - expect(response_headers['Content-Type']).to have_content("application/atom+xml") - expect(body).to have_selector("title", text: "#{project.name} issues") - expect(body).to have_selector("author email", text: issue.author_email) - expect(body).to have_selector("entry summary", text: issue.title) + expect(response_headers['Content-Type']). + to have_content('application/atom+xml') + expect(body).to have_selector('title', text: "#{project.name} issues") + expect(body).to have_selector('author email', text: issue.author_email) + expect(body).to have_selector('entry summary', text: issue.title) end end end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 37af48282db..c0316b073ad 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -4,17 +4,23 @@ describe "User Feed", feature: true do describe "GET /" do let!(:user) { create(:user) } - context "user atom feed via private token" do + context 'user atom feed via private token' do it "should render user atom feed" do visit user_path(user, :atom, private_token: user.private_token) - expect(body).to have_selector("feed title") + expect(body).to have_selector('feed title') end end context 'feed content' do let(:project) { create(:project) } - let(:issue) { create(:issue, project: project, author: user, description: '') } - let(:note) { create(:note, noteable: issue, author: user, note: 'Bug confirmed', project: project) } + let(:issue) do + create(:issue, project: project, + author: user, description: '') + end + let(:note) do + create(:note, noteable: issue, author: user, + note: 'Bug confirmed', project: project) + end before do project.team << [user, :master] @@ -23,11 +29,11 @@ describe "User Feed", feature: true do visit user_path(user, :atom, private_token: user.private_token) end - it "should have issue opened event" do + it 'should have issue opened event' do expect(body).to have_content("#{safe_name} opened issue ##{issue.iid}") end - it "should have issue comment event" do + it 'should have issue comment event' do expect(body). to have_content("#{safe_name} commented on issue ##{issue.iid}") end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 78e5adebc5c..f54155439cb 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Issues", feature: true do +describe 'Issues', feature: true do include SortingHelper let(:project) { create(:project) } @@ -12,7 +12,7 @@ describe "Issues", feature: true do project.team << [[@user, user2], :developer] end - describe "Edit issue" do + describe 'Edit issue' do let!(:issue) do create(:issue, author: @user, @@ -25,30 +25,34 @@ describe "Issues", feature: true do click_link "Edit" end - it "should open new issue popup" do + it 'should open new issue popup' do expect(page).to have_content("Issue ##{issue.iid}") end - describe "fill in" do + describe 'fill in' do before do - fill_in "issue_title", with: "bug 345" - fill_in "issue_description", with: "bug description" + fill_in 'issue_title', with: 'bug 345' + fill_in 'issue_description', with: 'bug description' end - it { expect { click_button "Save changes" }.to_not change {Issue.count} } + it 'does not change issue count' do + expect { + click_button 'Save changes' + }.to_not change { Issue.count } + end - it "should update issue fields" do - click_button "Save changes" + it 'should update issue fields' do + click_button 'Save changes' expect(page).to have_content @user.name - expect(page).to have_content "bug 345" + expect(page).to have_content 'bug 345' expect(page).to have_content project.name end end end - describe "Editing issue assignee" do + describe 'Editing issue assignee' do let!(:issue) do create(:issue, author: @user, @@ -56,7 +60,7 @@ describe "Issues", feature: true do project: project) end - it 'allows user to select unasigned', :js => true do + it 'allows user to select unasigned', js: true do visit edit_project_issue_path(project, issue) expect(page).to have_content "Assign to #{@user.name}" @@ -65,14 +69,14 @@ describe "Issues", feature: true do sleep 2 # wait for ajax stuff to complete first('.user-result').click - click_button "Save changes" + click_button 'Save changes' expect(page).to have_content 'Assignee: none' expect(issue.reload.assignee).to be_nil end end - describe "Filter issue" do + describe 'Filter issue' do before do ['foobar', 'barbaz', 'gitlab'].each do |title| create(:issue, @@ -90,7 +94,7 @@ describe "Issues", feature: true do let(:issue) { @issue } - it "should allow filtering by issues with no specified milestone" do + it 'should allow filtering by issues with no specified milestone' do visit project_issues_path(project, milestone_id: '0') expect(page).not_to have_content 'foobar' @@ -98,7 +102,7 @@ describe "Issues", feature: true do expect(page).to have_content 'gitlab' end - it "should allow filtering by a specified milestone" do + it 'should allow filtering by a specified milestone' do visit project_issues_path(project, milestone_id: issue.milestone.id) expect(page).to have_content 'foobar' @@ -106,7 +110,7 @@ describe "Issues", feature: true do expect(page).not_to have_content 'gitlab' end - it "should allow filtering by issues with no specified assignee" do + it 'should allow filtering by issues with no specified assignee' do visit project_issues_path(project, assignee_id: '0') expect(page).to have_content 'foobar' @@ -114,7 +118,7 @@ describe "Issues", feature: true do expect(page).not_to have_content 'gitlab' end - it "should allow filtering by a specified assignee" do + it 'should allow filtering by a specified assignee' do visit project_issues_path(project, assignee_id: @user.id) expect(page).not_to have_content 'foobar' @@ -126,7 +130,11 @@ describe "Issues", feature: true do describe 'filter issue' do titles = ['foo','bar','baz'] titles.each_with_index do |title, index| - let!(title.to_sym) { create(:issue, title: title, project: project, created_at: Time.now - (index * 60)) } + let!(title.to_sym) do + create(:issue, title: title, + project: project, + created_at: Time.now - (index * 60)) + end end let(:newer_due_milestone) { create(:milestone, due_date: '2013-12-11') } let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') } @@ -134,15 +142,15 @@ describe "Issues", feature: true do it 'sorts by newest' do visit project_issues_path(project, sort: sort_value_recently_created) - expect(first_issue).to include("foo") - expect(last_issue).to include("baz") + expect(first_issue).to include('foo') + expect(last_issue).to include('baz') end it 'sorts by oldest' do visit project_issues_path(project, sort: sort_value_oldest_created) - expect(first_issue).to include("baz") - expect(last_issue).to include("foo") + expect(first_issue).to include('baz') + expect(last_issue).to include('foo') end it 'sorts by most recently updated' do @@ -150,7 +158,7 @@ describe "Issues", feature: true do baz.save visit project_issues_path(project, sort: sort_value_recently_updated) - expect(first_issue).to include("baz") + expect(first_issue).to include('baz') end it 'sorts by least recently updated' do @@ -158,7 +166,7 @@ describe "Issues", feature: true do baz.save visit project_issues_path(project, sort: sort_value_oldest_updated) - expect(first_issue).to include("baz") + expect(first_issue).to include('baz') end describe 'sorting by milestone' do @@ -172,13 +180,13 @@ describe "Issues", feature: true do it 'sorts by recently due milestone' do visit project_issues_path(project, sort: sort_value_milestone_soon) - expect(first_issue).to include("foo") + expect(first_issue).to include('foo') end it 'sorts by least recently due milestone' do visit project_issues_path(project, sort: sort_value_milestone_later) - expect(first_issue).to include("bar") + expect(first_issue).to include('bar') end end @@ -193,10 +201,12 @@ describe "Issues", feature: true do end it 'sorts with a filter applied' do - visit project_issues_path(project, sort: sort_value_oldest_created, assignee_id: user2.id) + visit project_issues_path(project, + sort: sort_value_oldest_created, + assignee_id: user2.id) - expect(first_issue).to include("bar") - expect(last_issue).to include("foo") + expect(first_issue).to include('bar') + expect(last_issue).to include('foo') expect(page).not_to have_content 'baz' end end @@ -210,11 +220,13 @@ describe "Issues", feature: true do it 'with dropdown menu' do visit project_issue_path(project, issue) - find('.edit-issue.inline-update #issue_assignee_id').set project.team.members.first.id + find('.edit-issue.inline-update #issue_assignee_id'). + set project.team.members.first.id click_button 'Update Issue' - expect(page).to have_content "Assignee:" - has_select?('issue_assignee_id', :selected => project.team.members.first.name) + expect(page).to have_content 'Assignee:' + has_select?('issue_assignee_id', + selected: project.team.members.first.name) end end @@ -228,7 +240,7 @@ describe "Issues", feature: true do issue.save end - it "shows assignee text", js: true do + it 'shows assignee text', js: true do logout login_with guest @@ -247,12 +259,13 @@ describe "Issues", feature: true do it 'with dropdown menu' do visit project_issue_path(project, issue) - find('.edit-issue.inline-update').select(milestone.title, from: 'issue_milestone_id') + find('.edit-issue.inline-update'). + select(milestone.title, from: 'issue_milestone_id') click_button 'Update Issue' expect(page).to have_content "Milestone changed to #{milestone.title}" expect(page).to have_content "Milestone: #{milestone.title}" - has_select?('issue_assignee_id', :selected => milestone.title) + has_select?('issue_assignee_id', selected: milestone.title) end end @@ -265,7 +278,7 @@ describe "Issues", feature: true do issue.save end - it "shows milestone text", js: true do + it 'shows milestone text', js: true do logout login_with guest @@ -282,7 +295,7 @@ describe "Issues", feature: true do issue.save end - it 'allows user to remove assignee', :js => true do + it 'allows user to remove assignee', js: true do visit project_issue_path(project, issue) expect(page).to have_content "Assignee: #{user2.name}" @@ -298,10 +311,10 @@ describe "Issues", feature: true do end def first_issue - all("ul.issues-list li").first.text + all('ul.issues-list li').first.text end def last_issue - all("ul.issues-list li").last.text + all('ul.issues-list li').last.text end end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 2884c560a7c..7790d0ecd73 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -3,10 +3,12 @@ require 'spec_helper' describe 'Comments' do include RepoHelpers - describe "On a merge request", js: true, feature: true do + describe 'On a merge request', js: true, feature: true do let!(:merge_request) { create(:merge_request) } let!(:project) { merge_request.source_project } - let!(:note) { create(:note_on_merge_request, :with_attachment, project: project) } + let!(:note) do + create(:note_on_merge_request, :with_attachment, project: project) + end before do login_as :admin @@ -15,19 +17,20 @@ describe 'Comments' do subject { page } - describe "the note form" do + describe 'the note form' do it 'should be valid' do - is_expected.to have_css(".js-main-target-form", visible: true, count: 1) - expect(find(".js-main-target-form input[type=submit]").value).to eq("Add Comment") + is_expected.to have_css('.js-main-target-form', visible: true, count: 1) + expect(find('.js-main-target-form input[type=submit]').value). + to eq('Add Comment') within('.js-main-target-form') do expect(page).not_to have_link('Cancel') end end - describe "with text" do + describe 'with text' do before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awesome" + within('.js-main-target-form') do + fill_in 'note[note]', with: 'This is awesome' end end @@ -40,41 +43,45 @@ describe 'Comments' do end end - describe "when posting a note" do + describe 'when posting a note' do before do - within(".js-main-target-form") do - fill_in "note[note]", with: "This is awsome!" + within('.js-main-target-form') do + fill_in 'note[note]', with: 'This is awsome!' find('.js-md-preview-button').click - click_button "Add Comment" + click_button 'Add Comment' end end it 'should be added and form reset' do - is_expected.to have_content("This is awsome!") + is_expected.to have_content('This is awsome!') within('.js-main-target-form') do expect(page).to have_no_field('note[note]', with: 'This is awesome!') expect(page).to have_css('.js-md-preview', visible: :hidden) end - within(".js-main-target-form") { is_expected.to have_css(".js-note-text", visible: true) } + within('.js-main-target-form') do + is_expected.to have_css('.js-note-text', visible: true) + end end end - describe "when editing a note", js: true do - it "should contain the hidden edit form" do - within("#note_#{note.id}") { is_expected.to have_css(".note-edit-form", visible: false) } + describe 'when editing a note', js: true do + it 'should contain the hidden edit form' do + within("#note_#{note.id}") do + is_expected.to have_css('.note-edit-form', visible: false) + end end - describe "editing the note" do + describe 'editing the note' do before do find('.note').hover find(".js-note-edit").click end - it "should show the note edit form and hide the note body" do + it 'should show the note edit form and hide the note body' do within("#note_#{note.id}") do - expect(find(".current-note-edit-form", visible: true)).to be_visible - expect(find(".note-edit-form", visible: true)).to be_visible - expect(find(:css, ".note-text", visible: false)).not_to be_visible + expect(find('.current-note-edit-form', visible: true)).to be_visible + expect(find('.note-edit-form', visible: true)).to be_visible + expect(find(:css, '.note-text', visible: false)).not_to be_visible end end @@ -87,41 +94,43 @@ describe 'Comments' do #end #end - it "appends the edited at time to the note" do - within(".current-note-edit-form") do - fill_in "note[note]", with: "Some new content" - find(".btn-save").click + it 'appends the edited at time to the note' do + within('.current-note-edit-form') do + fill_in 'note[note]', with: 'Some new content' + find('.btn-save').click end within("#note_#{note.id}") do - is_expected.to have_css(".note_edited_ago") - expect(find(".note_edited_ago").text).to match(/less than a minute ago/) + is_expected.to have_css('.note_edited_ago') + expect(find('.note_edited_ago').text). + to match(/less than a minute ago/) end end end - describe "deleting an attachment" do + describe 'deleting an attachment' do before do find('.note').hover - find(".js-note-edit").click + find('.js-note-edit').click end - it "shows the delete link" do - within(".note-attachment") do - is_expected.to have_css(".js-note-attachment-delete") + it 'shows the delete link' do + within('.note-attachment') do + is_expected.to have_css('.js-note-attachment-delete') end end - it "removes the attachment div and resets the edit form" do - find(".js-note-attachment-delete").click - is_expected.not_to have_css(".note-attachment") - expect(find(".current-note-edit-form", visible: false)).not_to be_visible + it 'removes the attachment div and resets the edit form' do + find('.js-note-attachment-delete').click + is_expected.not_to have_css('.note-attachment') + expect(find('.current-note-edit-form', visible: false)). + not_to be_visible end end end end - describe "On a merge request diff", js: true, feature: true do + describe 'On a merge request diff', js: true, feature: true do let(:merge_request) { create(:merge_request) } let(:project) { merge_request.source_project } @@ -132,68 +141,74 @@ describe 'Comments' do subject { page } - describe "when adding a note" do + describe 'when adding a note' do before do click_diff_line end - describe "the notes holder" do - it { is_expected.to have_css(".js-temp-notes-holder") } + describe 'the notes holder' do + it { is_expected.to have_css('.js-temp-notes-holder') } - it { within(".js-temp-notes-holder") { is_expected.to have_css(".new_note") } } + it 'has .new_note css class' do + within('.js-temp-notes-holder') do + expect(subject).to have_css('.new_note') + end + end end - describe "the note form" do + describe 'the note form' do it "shouldn't add a second form for same row" do click_diff_line - is_expected.to have_css("tr[id='#{line_code}'] + .js-temp-notes-holder form", count: 1) + is_expected. + to have_css("tr[id='#{line_code}'] + .js-temp-notes-holder form", + count: 1) end - it "should be removed when canceled" do + it 'should be removed when canceled' do within(".diff-file form[rel$='#{line_code}']") do - find(".js-close-discussion-note-form").trigger("click") + find('.js-close-discussion-note-form').trigger('click') end - is_expected.to have_no_css(".js-temp-notes-holder") + is_expected.to have_no_css('.js-temp-notes-holder') end end end - describe "with muliple note forms" do + describe 'with muliple note forms' do before do click_diff_line click_diff_line(line_code_2) end - it { is_expected.to have_css(".js-temp-notes-holder", count: 2) } + it { is_expected.to have_css('.js-temp-notes-holder', count: 2) } - describe "previewing them separately" do + describe 'previewing them separately' do before do # add two separate texts and trigger previews on both within("tr[id='#{line_code}'] + .js-temp-notes-holder") do - fill_in "note[note]", with: "One comment on line 7" + fill_in 'note[note]', with: 'One comment on line 7' find('.js-md-preview-button').click end within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do - fill_in "note[note]", with: "Another comment on line 10" + fill_in 'note[note]', with: 'Another comment on line 10' find('.js-md-preview-button').click end end end - describe "posting a note" do + describe 'posting a note' do before do within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do - fill_in "note[note]", with: "Another comment on line 10" - click_button("Add Comment") + fill_in 'note[note]', with: 'Another comment on line 10' + click_button('Add Comment') end end it 'should be added as discussion' do - is_expected.to have_content("Another comment on line 10") - is_expected.to have_css(".notes_holder") - is_expected.to have_css(".notes_holder .note", count: 1) + is_expected.to have_content('Another comment on line 10') + is_expected.to have_css('.notes_holder') + is_expected.to have_css('.notes_holder .note', count: 1) is_expected.to have_button('Reply') end end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index dfbe65cee9d..3d36a3c02d0 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -1,34 +1,34 @@ require 'spec_helper' -describe "Profile account page", feature: true do +describe 'Profile account page', feature: true do let(:user) { create(:user) } before do login_as :user end - describe "when signup is enabled" do + describe 'when signup is enabled' do before do ApplicationSetting.any_instance.stub(signup_enabled?: true) visit profile_account_path end - it { expect(page).to have_content("Remove account") } + it { expect(page).to have_content('Remove account') } - it "should delete the account" do - expect { click_link "Delete account" }.to change {User.count}.by(-1) + it 'should delete the account' do + expect { click_link 'Delete account' }.to change { User.count }.by(-1) expect(current_path).to eq(new_user_session_path) end end - describe "when signup is disabled" do + describe 'when signup is disabled' do before do ApplicationSetting.any_instance.stub(signup_enabled?: false) visit profile_account_path end - it "should not have option to remove account" do - expect(page).not_to have_content("Remove account") + it 'should not have option to remove account' do + expect(page).not_to have_content('Remove account') expect(current_path).to eq(profile_account_path) end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 9c8c8ab4b0f..61d6c906ad0 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -46,7 +46,8 @@ describe ApplicationHelper do group = create(:group) group.avatar = File.open(avatar_file_path) group.save! - expect(group_icon(group.path).to_s).to match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png") + expect(group_icon(group.path).to_s). + to match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png") end it 'should give default avatar_icon when no avatar is present' do @@ -86,7 +87,8 @@ describe ApplicationHelper do user = create(:user) user.avatar = File.open(avatar_file_path) user.save! - expect(avatar_icon(user.email).to_s).to match("/uploads/user/avatar/#{ user.id }/gitlab_logo.png") + expect(avatar_icon(user.email).to_s). + to match("/uploads/user/avatar/#{ user.id }/gitlab_logo.png") end it 'should return an url for the avatar with relative url' do @@ -96,7 +98,8 @@ describe ApplicationHelper do user = create(:user) user.avatar = File.open(avatar_file_path) user.save! - expect(avatar_icon(user.email).to_s).to match("/gitlab/uploads/user/avatar/#{ user.id }/gitlab_logo.png") + expect(avatar_icon(user.email).to_s). + to match("/gitlab/uploads/user/avatar/#{ user.id }/gitlab_logo.png") end it 'should call gravatar_icon when no avatar is present' do @@ -120,7 +123,8 @@ describe ApplicationHelper do it 'should return default gravatar url' do Gitlab.config.gitlab.stub(https: false) - expect(gravatar_icon(user_email)).to match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') + url = 'http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118' + expect(gravatar_icon(user_email)).to match(url) end it 'should use SSL when appropriate' do @@ -130,8 +134,11 @@ describe ApplicationHelper do it 'should return custom gravatar path when gravatar_url is set' do allow(self).to receive(:request).and_return(double(:ssl? => false)) - allow(Gitlab.config.gravatar).to receive(:plain_url).and_return('http://example.local/?s=%{size}&hash=%{hash}') - expect(gravatar_icon(user_email, 20)).to eq('http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118') + allow(Gitlab.config.gravatar). + to receive(:plain_url). + and_return('http://example.local/?s=%{size}&hash=%{hash}') + url = 'http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118' + expect(gravatar_icon(user_email, 20)).to eq(url) end it 'should accept a custom size' do @@ -146,7 +153,8 @@ describe ApplicationHelper do it 'should be case insensitive' do allow(self).to receive(:request).and_return(double(:ssl? => false)) - expect(gravatar_icon(user_email)).to eq(gravatar_icon(user_email.upcase + ' ')) + expect(gravatar_icon(user_email)). + to eq(gravatar_icon(user_email.upcase + ' ')) end end @@ -170,7 +178,7 @@ describe ApplicationHelper do it 'includes a list of tag names' do expect(options[1][0]).to eq('Tags') - expect(options[1][1]).to include('v1.0.0','v1.1.0') + expect(options[1][1]).to include('v1.0.0', 'v1.1.0') end it 'includes a specific commit ref if defined' do @@ -183,9 +191,11 @@ describe ApplicationHelper do it 'sorts tags in a natural order' do # Stub repository.tag_names to make sure we get some valid testing data - expect(@project.repository).to receive(:tag_names).and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v1.0.9a']) + expect(@project.repository).to receive(:tag_names). + and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v1.0.9a']) - expect(options[1][1]).to eq(['v3.1.4.2', 'v2.0', 'v1.0.10', 'v1.0.9a', 'v1.0.9']) + expect(options[1][1]). + to eq(['v3.1.4.2', 'v2.0', 'v1.0.10', 'v1.0.9a', 'v1.0.9']) end end diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb index cf310b893e0..f6df12662bb 100644 --- a/spec/helpers/broadcast_messages_helper_spec.rb +++ b/spec/helpers/broadcast_messages_helper_spec.rb @@ -14,7 +14,8 @@ describe BroadcastMessagesHelper do before { broadcast_message.stub(color: "#f2dede", font: "#b94a48") } it "should have a customized style" do - expect(broadcast_styling(broadcast_message)).to match('background-color:#f2dede;color:#b94a48') + expect(broadcast_styling(broadcast_message)). + to match('background-color:#f2dede;color:#b94a48') end end end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 75da43a68a6..5bd09793b11 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -10,7 +10,7 @@ describe DiffHelper do describe 'diff_hard_limit_enabled?' do it 'should return true if param is provided' do - allow(controller).to receive(:params) { { :force_show_diff => true } } + allow(controller).to receive(:params) { { force_show_diff: true } } expect(diff_hard_limit_enabled?).to be_truthy end @@ -21,7 +21,7 @@ describe DiffHelper do describe 'allowed_diff_size' do it 'should return hard limit for a diff if force diff is true' do - allow(controller).to receive(:params) { { :force_show_diff => true } } + allow(controller).to receive(:params) { { force_show_diff: true } } expect(allowed_diff_size).to eq(1000) end @@ -32,13 +32,15 @@ describe DiffHelper do describe 'parallel_diff' do it 'should return an array of arrays containing the parsed diff' do - expect(parallel_diff(diff_file, 0)).to match_array(parallel_diff_result_array) + expect(parallel_diff(diff_file, 0)). + to match_array(parallel_diff_result_array) end end describe 'generate_line_code' do it 'should generate correct line code' do - expect(generate_line_code(diff_file.file_path, diff_file.diff_lines.first)).to eq('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6') + expect(generate_line_code(diff_file.file_path, diff_file.diff_lines.first)). + to eq('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6') end end @@ -55,12 +57,13 @@ describe DiffHelper do describe 'diff_line_content' do it 'should return non breaking space when line is empty' do - expect(diff_line_content(nil)).to eq("  ") + expect(diff_line_content(nil)).to eq('  ') end it 'should return the line itself' do - expect(diff_line_content(diff_file.diff_lines.first.text)).to eq("@@ -6,12 +6,18 @@ module Popen") - expect(diff_line_content(diff_file.diff_lines.first.type)).to eq("match") + expect(diff_line_content(diff_file.diff_lines.first.text)). + to eq('@@ -6,12 +6,18 @@ module Popen') + expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match') expect(diff_line_content(diff_file.diff_lines.first.new_pos)).to eq(6) end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 87d45faa207..317a559f83c 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -1,4 +1,4 @@ -require "spec_helper" +require 'spec_helper' describe GitlabMarkdownHelper do include ApplicationHelper @@ -42,7 +42,8 @@ describe GitlabMarkdownHelper do end it "should not touch HTML entities" do - allow(@project.issues).to receive(:where).with(id: '39').and_return([issue]) + allow(@project.issues).to receive(:where). + with(id: '39').and_return([issue]) actual = 'We'll accept good pull requests.' expect(gfm(actual)).to eq("We'll accept good pull requests.") end @@ -156,7 +157,8 @@ describe GitlabMarkdownHelper do expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected) # Append some text to the end of the reference - expect(gfm(actual.gsub(reference, "#{reference}, right?"))).to match(expected) + expect(gfm(actual.gsub(reference, "#{reference}, right?"))). + to match(expected) end it "should keep whitespace intact" do @@ -216,9 +218,8 @@ describe GitlabMarkdownHelper do ) # Append some text to the end of the reference - expect(gfm(actual.gsub(full_reference, "#{full_reference}, right?"))).to( - match(expected) - ) + expect(gfm(actual.gsub(full_reference, "#{full_reference}, right?"))). + to(match(expected)) end it 'should keep whitespace intact' do @@ -315,7 +316,8 @@ describe GitlabMarkdownHelper do expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected) # Append some text to the end of the reference - expect(gfm(actual.gsub(reference, "#{reference}, right?"))).to match(expected) + expect(gfm(actual.gsub(reference, "#{reference}, right?"))). + to match(expected) end it "should keep whitespace intact" do @@ -471,7 +473,8 @@ describe GitlabMarkdownHelper do expect(groups[0]).to match(/This should finally fix $/) # First issue link - expect(groups[1]).to match(/href="#{project_issue_url(project, issues[0])}"/) + expect(groups[1]). + to match(/href="#{project_issue_url(project, issues[0])}"/) expect(groups[1]).to match(/##{issues[0].iid}$/) # Internal commit link @@ -479,7 +482,8 @@ describe GitlabMarkdownHelper do expect(groups[2]).to match(/ and /) # Second issue link - expect(groups[3]).to match(/href="#{project_issue_url(project, issues[1])}"/) + expect(groups[3]). + to match(/href="#{project_issue_url(project, issues[1])}"/) expect(groups[3]).to match(/##{issues[1].iid}$/) # Trailing commit link @@ -494,7 +498,8 @@ describe GitlabMarkdownHelper do it "escapes HTML passed in as the body" do actual = "This is a

    test

    - see ##{issues[0].iid}" - expect(link_to_gfm(actual, commit_path)).to match('<h1>test</h1>') + expect(link_to_gfm(actual, commit_path)). + to match('<h1>test</h1>') end end @@ -508,16 +513,20 @@ describe GitlabMarkdownHelper do it "should handle references in headers" do actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}" - expect(markdown(actual, {no_header_anchors:true})).to match(%r{Working around ##{issue.iid}}) - expect(markdown(actual, {no_header_anchors:true})).to match(%r{Apply !#{merge_request.iid}}) + expect(markdown(actual, no_header_anchors: true)). + to match(%r{Working around ##{issue.iid}}) + expect(markdown(actual, no_header_anchors: true)). + to match(%r{Apply !#{merge_request.iid}}) end it "should add ids and links to headers" do # Test every rule except nested tags. text = '..Ab_c-d. e..' id = 'ab_c-d-e' - expect(markdown("# #{text}")).to match(%r{

    #{text}

    }) - expect(markdown("# #{text}", {no_header_anchors:true})).to eq("

    #{text}

    ") + expect(markdown("# #{text}")). + to match(%r{

    #{text}

    }) + expect(markdown("# #{text}", {no_header_anchors:true})). + to eq("

    #{text}

    ") id = 'link-text' expect(markdown("# [link text](url) ![img alt](url)")).to match( @@ -530,13 +539,16 @@ describe GitlabMarkdownHelper do actual = "\n* dark: ##{issue.iid}\n* light by @#{member.user.username}" - expect(markdown(actual)).to match(%r{
  • dark: ##{issue.iid}
  • }) - expect(markdown(actual)).to match(%r{
  • light by @#{member.user.username}
  • }) + expect(markdown(actual)). + to match(%r{
  • dark: ##{issue.iid}
  • }) + expect(markdown(actual)). + to match(%r{
  • light by @#{member.user.username}
  • }) end it "should not link the apostrophe to issue 39" do project.team << [user, :master] - allow(project.issues).to receive(:where).with(iid: '39').and_return([issue]) + allow(project.issues). + to receive(:where).with(iid: '39').and_return([issue]) actual = "Yes, it is @#{member.user.username}'s task." expected = /Yes, it is @#{member.user.username}<\/a>'s task/ @@ -545,7 +557,8 @@ describe GitlabMarkdownHelper do it "should not link the apostrophe to issue 39 in code blocks" do project.team << [user, :master] - allow(project.issues).to receive(:where).with(iid: '39').and_return([issue]) + allow(project.issues). + to receive(:where).with(iid: '39').and_return([issue]) actual = "Yes, `it is @#{member.user.username}'s task.`" expected = /Yes, it is @gfm\'s task.<\/code>/ @@ -555,7 +568,8 @@ describe GitlabMarkdownHelper do it "should handle references in " do actual = "Apply _!#{merge_request.iid}_ ASAP" - expect(markdown(actual)).to match(%r{Apply !#{merge_request.iid}}) + expect(markdown(actual)). + to match(%r{Apply !#{merge_request.iid}}) end it "should handle tables" do @@ -572,8 +586,10 @@ describe GitlabMarkdownHelper do target_html = "
    some code from $40\nhere too\n
    \n" - expect(helper.markdown("\n some code from $#{snippet.id}\n here too\n")).to eq(target_html) - expect(helper.markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n")).to eq(target_html) + expect(helper.markdown("\n some code from $#{snippet.id}\n here too\n")). + to eq(target_html) + expect(helper.markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n")). + to eq(target_html) end it "should leave inline code untouched" do -- GitLab From 6685661b549cdece3b93131af168b5174bc0403f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 11 Feb 2015 14:12:43 +0100 Subject: [PATCH 0868/1609] Clean username acquired from OAuth/LDAP. Fixes #1967. --- CHANGELOG | 1 + app/models/user.rb | 16 ++++++++++++++++ lib/gitlab/oauth/user.rb | 10 +++++----- spec/lib/gitlab/oauth/user_spec.rb | 2 +- spec/models/user_spec.rb | 10 ++++++++++ 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6a90320b8bc..0b369acf483 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -75,6 +75,7 @@ v 7.8.0 (unreleased) - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) - Added persistent collapse button for left side nav bar (Jason Blanchard) - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. + - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/app/models/user.rb b/app/models/user.rb index 3a7dfabeafe..d7f688ec138 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -243,6 +243,22 @@ class User < ActiveRecord::Base def build_user(attrs = {}) User.new(attrs) end + + def clean_username(username) + username.gsub!(/@.*\z/, "") + username.gsub!(/\.git\z/, "") + username.gsub!(/\A-/, "") + username.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + + counter = 0 + base = username + while by_login(username).present? + counter += 1 + username = "#{base}#{counter}" + end + + username + end end # diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index 6861427864e..9f55e8c4950 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -85,11 +85,11 @@ module Gitlab def user_attributes { - name: auth_hash.name, - username: auth_hash.username, - email: auth_hash.email, - password: auth_hash.password, - password_confirmation: auth_hash.password + name: auth_hash.name, + username: ::User.clean_username(auth_hash.username), + email: auth_hash.email, + password: auth_hash.password, + password_confirmation: auth_hash.password } end diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/oauth/user_spec.rb index 88307515789..2680794a747 100644 --- a/spec/lib/gitlab/oauth/user_spec.rb +++ b/spec/lib/gitlab/oauth/user_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::OAuth::User do let(:auth_hash) { double(uid: uid, provider: provider, info: double(info_hash)) } let(:info_hash) do { - nickname: 'john', + nickname: '-john+gitlab-ETC%.git@gmail.com', name: 'John', email: 'john@mail.com' } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 629d51b960d..7473054f481 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -301,6 +301,16 @@ describe User do end end + describe ".clean_username" do + + let!(:user1) { create(:user, username: "johngitlab-etc") } + let!(:user2) { create(:user, username: "JohnGitLab-etc1") } + + it "cleans a username and makes sure it's available" do + expect(User.clean_username("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2") + end + end + describe 'all_ssh_keys' do it { should have_many(:keys).dependent(:destroy) } -- GitLab From 686000446639fbf0756733c822a1ebb19e09e121 Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Thu, 12 Feb 2015 21:22:23 +0100 Subject: [PATCH 0869/1609] Fixed deprecation in spinach stubs Signed-off-by: Jeroen van Baarsen --- features/steps/project/redirects.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index e54637120ce..e2badccbcf4 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -17,7 +17,7 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps end step 'I should see project "Community" home page' do - Gitlab.config.gitlab.stub(:host).and_return("www.example.com") + Gitlab.config.gitlab.should_receive(:host).and_return("www.example.com") within '.navbar-gitlab .title' do page.should have_content 'Community' end -- GitLab From 1a89db5ffbca432c14eae9d364debc5b87b4635e Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 12 Feb 2015 13:02:58 -0800 Subject: [PATCH 0870/1609] Try to test settings added in the service. --- .../projects/services_controller.rb | 2 +- .../project_services/issue_tracker_service.rb | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index b3110eacc18..2b3e70f7bdb 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -29,7 +29,7 @@ class Projects::ServicesController < Projects::ApplicationController if @service.execute(data) message = { notice: 'We sent a request to the provided URL' } else - message = { alert: 'We tried to send a request to the provided URL but error occured' } + message = { alert: 'We tried to send a request to the provided URL but an error occured' } end redirect_to :back, message diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 51b2fb3dcc7..3d927bb50d4 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -65,6 +65,29 @@ class IssueTrackerService < Service end end + def execute(data) + message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again." + result = false + + begin + url = URI.parse(self.project_url) + + if url.host && url.port + http = Net::HTTP.start(url.host, url.port, {open_timeout: 5, read_timeout: 5}) + response = http.head("/") + + if response + message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}" + result = true + end + end + rescue Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED => error + message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}" + end + Rails.logger.info(message) + result + end + private def enabled_in_gitlab_config -- GitLab From 8a37435738423853654ec622d59fbb2048ad7a1e Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 12 Feb 2015 13:21:47 -0800 Subject: [PATCH 0871/1609] Fix rubocop error. --- app/models/project_services/issue_tracker_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 3d927bb50d4..c991a34ecdb 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -73,7 +73,7 @@ class IssueTrackerService < Service url = URI.parse(self.project_url) if url.host && url.port - http = Net::HTTP.start(url.host, url.port, {open_timeout: 5, read_timeout: 5}) + http = Net::HTTP.start(url.host, url.port, { open_timeout: 5, read_timeout: 5 }) response = http.head("/") if response -- GitLab From eccf695640680050127c830887631d241dc7c8be Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 12 Feb 2015 17:06:55 -0800 Subject: [PATCH 0872/1609] Explained in the integration documentation how to enable external issue tracker --- app/models/project_services/jira_service.rb | 14 ++++++++ app/views/admin/services/_form.html.haml | 4 +++ app/views/layouts/nav/_admin.html.haml | 2 +- doc/integration/external-issue-tracker.md | 35 ++++++++++++++++--- doc/integration/redmine_configuration.png | Bin 0 -> 118752 bytes doc/integration/redmine_service_template.png | Bin 0 -> 198077 bytes 6 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 doc/integration/redmine_configuration.png create mode 100644 doc/integration/redmine_service_template.png diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index a159c287485..4c056605ea8 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -14,9 +14,23 @@ # class JiraService < IssueTrackerService + include Rails.application.routes.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + def help + issue_tracker_link = help_page_path("integration", "external-issue-tracker") + + line1 = "Setting `project_url`, `issues_url` and `new_issue_url` will "\ + "allow a user to easily navigate to the Jira issue tracker. "\ + "See the [integration doc](#{issue_tracker_link}) for details." + + line2 = 'Support for referencing commits and automatic closing of Jira issues directly ' \ + 'from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)' + + [line1, line2].join("\n\n") + end + def title if self.properties && self.properties['title'].present? self.properties['title'] diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index d8242e37621..5df8849317b 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -9,6 +9,10 @@ .alert.alert-danger - @service.errors.full_messages.each do |msg| %p= msg + - if @service.help.present? + .bs-callout + = preserve do + = markdown @service.help - @service.fields.each do |field| - name = field[:name] diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 4f864926d08..74334b12e63 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -46,7 +46,7 @@ %span Applications - = nav_link(controller: :application_settings) do + = nav_link(controller: :services) do = link_to admin_application_settings_services_path, title: 'Service Templates' do %i.fa.fa-copy %span diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index ba4df9f8fe0..a4f67daa563 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,13 +1,38 @@ # External issue tracker -GitLab has a great issue tracker but you can also use an external issue tracker such as JIRA, Bugzilla or Redmine. This is something that you can turn on per GitLab project. If for example you configure JIRA it provides the following functionality: +GitLab has a great issue tracker but you can also use an external issue tracker such as Jira, Bugzilla or Redmine. This is something that you can turn on per GitLab project. If for example you configure Jira it provides the following functionality: -- the 'Issues' link on the GitLab project pages takes you to the appropriate JIRA issue index; -- clicking 'New issue' on the project dashboard creates a new JIRA issue; -- To reference JIRA issue PROJECT-1234 in comments, use syntax PROJECT-1234. Commit messages get turned into HTML links to the corresponding JIRA issue. +- the 'Issues' link on the GitLab project pages takes you to the appropriate Jira issue index; +- clicking 'New issue' on the project dashboard creates a new Jira issue; +- To reference Jira issue PROJECT-1234 in comments, use syntax PROJECT-1234. Commit messages get turned into HTML links to the corresponding Jira issue. ![Jira screenshot](jira-integration-points.png) -You can configure the integration in the gitlab.yml configuration file. +## Configuration + +### Project Service + +External issue tracker can be enabled per project basis. As an example, we will configure `Redmine` for project named gitlab-ci. + +Fill in the required details on the page: + +![redmine configuration](redmine_configuration.png) + +* `description` A name for the issue tracker (to differentiate between instances, for example). +* `project_url` The URL to the project in Redmine which is being linked to this GitLab project. +* `issues_url` The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the url. This id GitLab uses as a placeholder to replace the issue number. +* `new_issue_url` This is the URL to create a new issue in Redmine for the project linked to this GitLab project. + + +### Service Template + +Since external issue tracker needs some project specific details, it is required to enable issue tracker per project level. +GitLab makes this easier by allowing admin to add a service template which will allow GitLab project user with permissions to edit details for its project. + +In GitLab Admin section, navigate to `Service Templates` and choose the service template you want to create: + +![redmine service template](redmine_service_template.png) + +After the template is created, the template details will be pre-filled on the project service page. Support to add your commits to the Jira ticket automatically is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jira.html). diff --git a/doc/integration/redmine_configuration.png b/doc/integration/redmine_configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..6b1453632293f010e2349a98270e99d5ac60c39d GIT binary patch literal 118752 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�t+hIK+jeOw>s!-_mpm^+ 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∨}L5Ezlm)XMg*)339~j(Oht|@_0$? z#<@3+-~6^#PWotR^4uHo7IwQ*Y9-xmccZ+!o*5)=E(_NL3plIVHr{fq7Yd^lWu;79R!+v(}zGqoy(Z@ML0f?8`|%3gr( zy_3BsZfn)^!FRr}!i9N<45MxGqGK->xqT_B{kF0y((&}=ZNKIvmvyR6%h(#VZ{Z>> z<1k~TD*1Y+gaOhaWuVrom!o`_gIhQ)eaG``!{cPGxJ@ixj49abCyAFOMBeE9RqV9{iAuoG`=Otzj`{rF;v>bmJO zcLbg-dwq4bI^*4x=C}NkiLWIsYTj|UggxD9Z2o)^ytjz02MHE4*v|57cC>Mk9r}u| zFH;yA-+SxtEy?|u60=QXfkSd5Xa|B><8hzfL-;qrHtGpffDV}2+jX0D!?_?(qE5Ic zG~;XK3~8UT5NP|X)_1{f`KOmB*VQZ&-eZ$+pyAHjliPOWuvYCp&?9Ml>C8;yVq?(W z)h!2@>^fF&&o{ok9G`E_=z_xU&D0e;w?3VNwq4bspPNU@WX18@3Ctoh!dW+l5?bjS!ziNkbmi4Aw29?>J#HY9b8|Fjbh6#c*ZZQV{#UPCjP z5I0sY;Xu)J;oS^40{g-L&E;|*%0g4`#kBzn|OxjjR9rRq;~NY)wq(V>7vzyJiyBkijtei+W>4naue~zZd7Q zp8o52(+Dj*c@!E07rQ;ZwA_EbUBlK58Tf)7VYmbj%cGlxHs0_0tu!Ov@t>Neh~`)Z zPMw`kCUyG%1l9PN*VaTbM+kRMLl}S~VnGTT8&wV%C(o1p%ri6nzxwE97;;P+|gy6$c!I9M+sMPVPxRVdMQyU;17=Yh)jK zSn|j;ZcIL&w(!%LnZ}nv`*Nm5Wv$$D#_04CL1nik4-fxc{_w!p9#wB*QqK!(ruF;( zRn4-kE(7gj__oH!6SRHwkz`T*t=n-`FPGMTNpGA6jcth@mS=JN#aoZ{NE%D5PM-^n zt-Wp!UM`>S=Wq8jrRwup^J-mx<4K^fLJf5+u4z2zpKxPC;?2!P_1#ky7fY`_Xm7V> zA*y#9ays$M=ZQJH;7 z9oIB=mffvhs1A~_VEV&cvf!VfRl(YADMThT4W`3vs}fF6CCazhg5y~|6W6ro|LwN5 z;wnuD87*PJ#5IkbXD=&J(Tye9CHzx2i07Vm|J%0hXaCz~aI0b|(g-;MWUN64Cy)G* z((IjCmzVi^>+k8ZZlf;(T2!k4Bcb}M$RvMjgIOO^NiE|W?7YH%`(jn+a6_+b!Eku zZ`=3Vf;SR@P8dC@K0jv$-}}>w1z}t62uxPT;ZlMjcOjdFtv5kZ@nih|U)M`MpEX|| z9$&llHg8N)r|P9utJlqHJu&~+mF1VFhR4mESMjJ*?{e_GMNjLKs$MRgp0($LlkB2* zQ3gQqqt6c!;fvZ^G=)I&RV_p((e0z-}P

    3hZ5 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 a@<(fWYF5AS%w=F;VDNPHb6Mw<&;$U;+rxnX literal 0 HcmV?d00001 -- GitLab From 07d05d2df7d1b79406232e91a7354e0c7fd1877a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 11:57:56 +0100 Subject: [PATCH 0873/1609] Move all event creation to EventCreateService. --- app/controllers/projects/tags_controller.rb | 2 +- app/models/event.rb | 23 ------- app/models/members/project_member.rb | 17 ++---- app/services/create_branch_service.rb | 2 +- app/services/create_tag_service.rb | 2 +- app/services/delete_branch_service.rb | 2 +- app/services/event_create_service.rb | 68 ++++++++++++++++----- app/services/git_push_service.rb | 11 +--- app/services/git_tag_push_service.rb | 11 +--- 9 files changed, 64 insertions(+), 74 deletions(-) diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 64b820160d3..22eb8f67f9a 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController tag = @repository.find_tag(params[:id]) if tag && @repository.rm_tag(tag.name) - Event.create_ref_event(@project, current_user, tag, 'rm', 'refs/tags') + EventCreateService.new.push_ref(@project, current_user, tag, 'rm', 'refs/tags') end respond_to do |format| diff --git a/app/models/event.rb b/app/models/event.rb index 9a42d380f87..3ead45a4bb4 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -49,29 +49,6 @@ class Event < ActiveRecord::Base scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } class << self - def create_ref_event(project, user, ref, action = 'add', prefix = 'refs/heads') - commit = project.repository.commit(ref.target) - - if action.to_s == 'add' - before = '00000000' - after = commit.id - else - before = commit.id - after = '00000000' - end - - Event.create( - project: project, - action: Event::PUSHED, - data: { - ref: "#{prefix}/#{ref.name}", - before: before, - after: after - }, - author_id: user.id - ) - end - def reset_event_cache_for(target) Event.where(target_id: target.id, target_type: target.class.to_s). order('id DESC').limit(100). diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 30c09f768d7..ff05ab1590f 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -114,12 +114,8 @@ class ProjectMember < Member end def post_create_hook - Event.create( - project_id: self.project.id, - action: Event::JOINED, - author_id: self.user.id - ) + event_service.join_project(self.project, self.user) notification_service.new_team_member(self) unless owner? system_hook_service.execute_hooks_for(self, :create) end @@ -129,15 +125,14 @@ class ProjectMember < Member end def post_destroy_hook - Event.create( - project_id: self.project.id, - action: Event::LEFT, - author_id: self.user.id - ) - + event_service.leave_project(self.project, self.user) system_hook_service.execute_hooks_for(self, :destroy) end + def event_service + EventCreateService.new + end + def notification_service NotificationService.new end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 901f67bafb3..5e971c7891c 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -17,7 +17,7 @@ class CreateBranchService < BaseService new_branch = repository.find_branch(branch_name) if new_branch - Event.create_ref_event(project, current_user, new_branch, 'add') + EventCreateService.new.push_ref(project, current_user, new_branch, 'add') return success(new_branch) else return error('Invalid reference name') diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 041c2287c36..a735d3f7f20 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -26,7 +26,7 @@ class CreateTagService < BaseService project.gitlab_ci_service.async_execute(push_data) end - Event.create_ref_event(project, current_user, new_tag, 'add', 'refs/tags') + EventCreateService.new.push_ref(project, current_user, new_tag, 'add', 'refs/tags') success(new_tag) else error('Invalid reference name') diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index cae6327fe72..c26aee2b0aa 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -25,7 +25,7 @@ class DeleteBranchService < BaseService end if repository.rm_branch(branch_name) - Event.create_ref_event(project, current_user, branch, 'rm') + EventCreateService.new.push_ref(project, current_user, branch, 'rm') success('Branch was removed') else return error('Failed to remove branch') diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 8d8a5873e62..bb3c37023a0 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -7,58 +7,94 @@ # class EventCreateService def open_issue(issue, current_user) - create_event(issue, current_user, Event::CREATED) + create_record_event(issue, current_user, Event::CREATED) end def close_issue(issue, current_user) - create_event(issue, current_user, Event::CLOSED) + create_record_event(issue, current_user, Event::CLOSED) end def reopen_issue(issue, current_user) - create_event(issue, current_user, Event::REOPENED) + create_record_event(issue, current_user, Event::REOPENED) end def open_mr(merge_request, current_user) - create_event(merge_request, current_user, Event::CREATED) + create_record_event(merge_request, current_user, Event::CREATED) end def close_mr(merge_request, current_user) - create_event(merge_request, current_user, Event::CLOSED) + create_record_event(merge_request, current_user, Event::CLOSED) end def reopen_mr(merge_request, current_user) - create_event(merge_request, current_user, Event::REOPENED) + create_record_event(merge_request, current_user, Event::REOPENED) end def merge_mr(merge_request, current_user) - create_event(merge_request, current_user, Event::MERGED) + create_record_event(merge_request, current_user, Event::MERGED) end def open_milestone(milestone, current_user) - create_event(milestone, current_user, Event::CREATED) + create_record_event(milestone, current_user, Event::CREATED) end def close_milestone(milestone, current_user) - create_event(milestone, current_user, Event::CLOSED) + create_record_event(milestone, current_user, Event::CLOSED) end def reopen_milestone(milestone, current_user) - create_event(milestone, current_user, Event::REOPENED) + create_record_event(milestone, current_user, Event::REOPENED) end def leave_note(note, current_user) - create_event(note, current_user, Event::COMMENTED) + create_record_event(note, current_user, Event::COMMENTED) + end + + def join_project(project, current_user) + create_event(project, current_user, Event::JOINED) + end + + def leave_project(project, current_user) + create_event(project, current_user, Event::LEFT) + end + + def push_ref(project, current_user, ref, action = 'add', prefix = 'refs/heads') + commit = project.repository.commit(ref.target) + + if action.to_s == 'add' + before = '00000000' + after = commit.id + else + before = commit.id + after = '00000000' + end + + data = { + ref: "#{prefix}/#{ref.name}", + before: before, + after: after + } + + push(project, current_user, data) + end + + def push(project, current_user, push_data) + create_event(project, current_user, Event::PUSHED, data: push_data) end private - def create_event(record, current_user, status) - Event.create( - project: record.project, - target_id: record.id, - target_type: record.class.name, + def create_record_event(record, current_user, status) + create_event(record.project, current_user, status, target_id: record.id, target_type: record.class.name) + end + + def create_event(project, current_user, status, attributes = {}) + attributes.reverse_merge!( + project: project, action: status, author_id: current_user.id ) + + Event.create(attributes) end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index c775f79ec29..f21e6ac207d 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -52,7 +52,7 @@ class GitPushService end @push_data = post_receive_data(oldrev, newrev, ref) - create_push_event(@push_data) + EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup) end @@ -60,15 +60,6 @@ class GitPushService protected - def create_push_event(push_data) - Event.create!( - project: project, - action: Event::PUSHED, - data: push_data, - author_id: push_data[:user_id] - ) - end - # Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched, # close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables. def process_commit_messages(ref) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index c24809ad607..46d8987f12d 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -5,7 +5,7 @@ class GitTagPushService @project, @user = project, user @push_data = create_push_data(oldrev, newrev, ref) - create_push_event + EventCreateService.new.push(project, user, @push_data) project.repository.expire_cache project.execute_hooks(@push_data.dup, :tag_push_hooks) @@ -22,13 +22,4 @@ class GitTagPushService Gitlab::PushDataBuilder. build(project, user, oldrev, newrev, ref, []) end - - def create_push_event - Event.create!( - project: project, - action: Event::PUSHED, - data: push_data, - author_id: push_data[:user_id] - ) - end end -- GitLab From 522efa43fe9ff5828838a5d5ed49db23bfd88c95 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 12:00:12 +0100 Subject: [PATCH 0874/1609] Refactor event title generation for more consistent messages. Example: "User joined project Namespace / Project" rather than "User joined project at Namespace / Project" --- app/helpers/events_helper.rb | 53 ++++++++++------- app/models/event.rb | 72 ++++++++++++++---------- app/views/events/event/_common.html.haml | 10 ++-- app/views/events/event/_note.html.haml | 6 +- app/views/events/event/_push.html.haml | 2 +- 5 files changed, 85 insertions(+), 58 deletions(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index d05f6df5f9f..ca64499675c 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -10,11 +10,15 @@ module EventsHelper end def event_action_name(event) - target = if event.target_type - event.target_type.titleize.downcase - else - 'project' - end + target = if event.target_type + if event.note? + event.note_target_type + else + event.target_type.titleize.downcase + end + else + 'project' + end [event.action_name, target].join(" ") end @@ -42,21 +46,30 @@ module EventsHelper end def event_feed_title(event) - if event.issue? - "#{event.author_name} #{event.action_name} issue ##{event.target_iid}: #{event.issue_title} at #{event.project_name}" - elsif event.merge_request? - "#{event.author_name} #{event.action_name} MR ##{event.target_iid}: #{event.merge_request_title} at #{event.project_name}" - elsif event.push? - "#{event.author_name} #{event.push_action_name} #{event.ref_type} #{event.ref_name} at #{event.project_name}" - elsif event.membership_changed? - "#{event.author_name} #{event.action_name} #{event.project_name}" - elsif event.note? && event.note_commit? - "#{event.author_name} commented on #{event.note_target_type} #{event.note_short_commit_id} at #{event.project_name}" - elsif event.note? - "#{event.author_name} commented on #{event.note_target_type} ##{truncate event.note_target_iid} at #{event.project_name}" - else - "" + words = [] + words << event.author_name + words << event_action_name(event) + + if event.push? + words << event.ref_type + words << event.ref_name + words << "at" + elsif event.commented? + if event.note_commit? + words << event.note_short_commit_id + else + words << "##{truncate event.note_target_iid}" + end + words << "at" + elsif event.target + words << "##{event.target_iid}:" + words << event.target.title if event.target.respond_to?(:title) + words << "at" end + + words << event.project_name + + words.join(" ") end def event_feed_url(event) @@ -96,8 +109,6 @@ module EventsHelper render "events/event_push", event: event elsif event.merge_request? render "events/event_merge_request", merge_request: event.merge_request - elsif event.push? - render "events/event_push", event: event elsif event.note? render "events/event_note", note: event.note end diff --git a/app/models/event.rb b/app/models/event.rb index 3ead45a4bb4..87be24e31a8 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -75,25 +75,43 @@ class Event < ActiveRecord::Base end def target_title - if target && target.respond_to?(:title) - target.title - end + target.title if target && target.respond_to?(:title) + end + + def created? + action == CREATED end def push? - action == self.class::PUSHED && valid_push? + action == PUSHED && valid_push? end def merged? - action == self.class::MERGED + action == MERGED end def closed? - action == self.class::CLOSED + action == CLOSED end def reopened? - action == self.class::REOPENED + action == REOPENED + end + + def joined? + action == JOINED + end + + def left? + action == LEFT + end + + def commented? + action == COMMENTED + end + + def membership_changed? + joined? || left? end def milestone? @@ -112,32 +130,32 @@ class Event < ActiveRecord::Base target_type == "MergeRequest" end - def joined? - action == JOINED - end - - def left? - action == LEFT - end - - def membership_changed? - joined? || left? + def milestone + target if milestone? end def issue - target if target_type == "Issue" + target if issue? end def merge_request - target if target_type == "MergeRequest" + target if merge_request? end def note - target if target_type == "Note" + target if note? end def action_name - if closed? + if push? + if new_ref? + "pushed new" + elsif rm_ref? + "deleted" + else + "pushed to" + end + elsif closed? "closed" elsif merged? "accepted" @@ -145,6 +163,8 @@ class Event < ActiveRecord::Base 'joined' elsif left? 'left' + elsif commented? + "commented on" else "opened" end @@ -213,16 +233,6 @@ class Event < ActiveRecord::Base tag? ? "tag" : "branch" end - def push_action_name - if new_ref? - "pushed new" - elsif rm_ref? - "deleted" - else - "pushed to" - end - end - def push_with_commits? md_ref? && commits.any? && commit_from && commit_to end diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index a9d3adf41df..b0cfba0dea1 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,15 +1,17 @@ .event-title %span.author_name= link_to_author event - %span.event_label{class: event.action_name}= event_action_name(event) + %span.event_label{class: event.action_name} + = event_action_name(event) + - if event.target %strong= link_to "##{event.target_iid}", [event.project, event.target] - - else - %strong= gfm event.target_title - at + at + - if event.project = link_to_project event.project - else = event.project_name + - if event.target.respond_to?(:title) .event-body .event-note diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index 6ec8e54fba5..0acb8538778 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,6 +1,10 @@ .event-title %span.author_name= link_to_author event - %span.event_label commented on #{event_note_title_html(event)} at + %span.event_label + = event.action_name + = event_note_title_html(event) + at + - if event.project = link_to_project event.project - else diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index b912b5e092f..4b645550517 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -1,6 +1,6 @@ .event-title %span.author_name= link_to_author event - %span.event_label.pushed #{event.push_action_name} #{event.ref_type} + %span.event_label.pushed #{event.action_name} #{event.ref_type} - if event.rm_ref? %strong= event.ref_name - else -- GitLab From 9b917b4a73ddd7607cd19847e89381fda0ec65d5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 12:01:28 +0100 Subject: [PATCH 0875/1609] Add "User created project Namespace / Project" event --- app/models/event.rb | 12 ++++++++++++ app/models/members/project_member.rb | 8 +++++--- app/services/event_create_service.rb | 4 ++++ app/services/projects/create_service.rb | 12 +++++------- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/app/models/event.rb b/app/models/event.rb index 87be24e31a8..cae7f0be85b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -61,6 +61,8 @@ class Event < ActiveRecord::Base true elsif membership_changed? true + elsif created_project? + true else (issue? || merge_request? || note? || milestone?) && target end @@ -114,6 +116,14 @@ class Event < ActiveRecord::Base joined? || left? end + def created_project? + created? && !target + end + + def created_target? + created? && target + end + def milestone? target_type == "Milestone" end @@ -165,6 +175,8 @@ class Event < ActiveRecord::Base 'left' elsif commented? "commented on" + elsif created_project? + "created" else "opened" end diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index ff05ab1590f..e4791d0f0aa 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -114,9 +114,11 @@ class ProjectMember < Member end def post_create_hook - - event_service.join_project(self.project, self.user) - notification_service.new_team_member(self) unless owner? + unless owner? + event_service.join_project(self.project, self.user) + notification_service.new_team_member(self) + end + system_hook_service.execute_hooks_for(self, :create) end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index bb3c37023a0..ba9547b9242 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -58,6 +58,10 @@ class EventCreateService create_event(project, current_user, Event::LEFT) end + def create_project(project, current_user) + create_event(project, current_user, Event::CREATED) + end + def push_ref(project, current_user, ref, action = 'add', prefix = 'refs/heads') commit = project.repository.commit(ref.target) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 139de70114b..4fe790b98f1 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -52,13 +52,7 @@ module Projects end end - if @project.persisted? - if @project.wiki_enabled? - @project.create_wiki - end - - after_create_actions - end + after_create_actions if @project.persisted? @project rescue => ex @@ -79,6 +73,10 @@ module Projects def after_create_actions log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") + + @project.create_wiki if @project.wiki_enabled? + + event_service.create_project(@project, current_user) system_hook_service.execute_hooks_for(@project, :create) unless @project.group -- GitLab From ce08f919bfab73178b2f8c584f34fd8849834365 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 12:02:17 +0100 Subject: [PATCH 0876/1609] Add link to share via twitter to "created project" event. --- app/assets/stylesheets/sections/events.scss | 4 +++ .../admin/application_settings_controller.rb | 1 + app/helpers/application_settings_helper.rb | 4 +++ app/models/application_setting.rb | 2 ++ .../application_settings/_form.html.haml | 5 ++++ app/views/events/_event.html.haml | 6 +++-- .../events/event/_created_project.html.haml | 27 +++++++++++++++++++ config/initializers/1_settings.rb | 1 + ...sharing_enabled_to_application_settings.rb | 5 ++++ db/schema.rb | 7 ++--- 10 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 app/views/events/event/_created_project.html.haml create mode 100644 db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb diff --git a/app/assets/stylesheets/sections/events.scss b/app/assets/stylesheets/sections/events.scss index 9582c995980..b7614513216 100644 --- a/app/assets/stylesheets/sections/events.scss +++ b/app/assets/stylesheets/sections/events.scss @@ -64,6 +64,10 @@ .md { font-size: 13px; + + iframe.twitter-share-button { + vertical-align: bottom; + } } pre { diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 7458542fc73..2b0c500e97a 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -26,6 +26,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :signup_enabled, :signin_enabled, :gravatar_enabled, + :twitter_sharing_enabled, :sign_in_text, :home_page_url ) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 04299316102..1ee086da997 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -3,6 +3,10 @@ module ApplicationSettingsHelper current_application_settings.gravatar_enabled? end + def twitter_sharing_enabled? + current_application_settings.twitter_sharing_enabled? + end + def signup_enabled? current_application_settings.signup_enabled? end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 6d4e220b16c..f1d918e5457 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -8,6 +8,7 @@ # signup_enabled :boolean # signin_enabled :boolean # gravatar_enabled :boolean +# twitter_sharing_enabled :boolean # sign_in_text :text # created_at :datetime # updated_at :datetime @@ -30,6 +31,7 @@ class ApplicationSetting < ActiveRecord::Base default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], + twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], ) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index ae0c70a79c7..f528d69f431 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -19,6 +19,11 @@ = f.label :gravatar_enabled, class: 'control-label' .col-sm-10 = f.check_box :gravatar_enabled, class: 'checkbox' + .form-group + = f.label :twitter_sharing_enabled, "Twitter enabled", class: 'control-label' + .col-sm-10 + = f.check_box :twitter_sharing_enabled, class: 'checkbox' + %span.help-block Show users button to share their newly created public or internal projects on twitter %fieldset %legend Misc .form-group diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index c7976ba564f..02b1dec753c 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -3,12 +3,14 @@ .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} - = cache event do + = cache [event, current_user] do = image_tag avatar_icon(event.author_email, 24), class: "avatar s24", alt:'' - if event.push? = render "events/event/push", event: event - - elsif event.note? + - elsif event.commented? = render "events/event/note", event: event + - elsif event.created_project? + = render "events/event/created_project", event: event - else = render "events/event/common", event: event \ No newline at end of file diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml new file mode 100644 index 00000000000..0ebbb841cca --- /dev/null +++ b/app/views/events/event/_created_project.html.haml @@ -0,0 +1,27 @@ +.event-title + %span.author_name= link_to_author event + %span.event_label{class: event.action_name} + = event_action_name(event) + + - if event.project + = link_to_project event.project + - else + = event.project_name + +- if current_user == event.author && !event.project.private? && twitter_sharing_enabled? + .event-body + .event-note + .md + %p + Congratulations! Why not share your accomplishment with the world? + + %a.twitter-share-button{ | + href: "https://twitter.com/share", | + class: "twitter-share-button", | + "data-url" => event.project.web_url, | + "data-text" => "I just created a new project in GitLab! GitLab is version control on your server, like GitHub but better.", | + "data-size" => "medium", | + "data-related" => "gitlab", | + "data-count" => "none"} + Tweet + \ No newline at end of file diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d7c1a8428ac..6a8bbb80b9c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -112,6 +112,7 @@ end Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? +Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil? diff --git a/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb b/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb new file mode 100644 index 00000000000..a0439172391 --- /dev/null +++ b/db/migrate/20150213104043_add_twitter_sharing_enabled_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddTwitterSharingEnabledToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :twitter_sharing_enabled, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index f33766a1fe8..15c89a14c02 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150211174341) do +ActiveRecord::Schema.define(version: 20150213104043) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -26,6 +26,7 @@ ActiveRecord::Schema.define(version: 20150211174341) do t.datetime "updated_at" t.string "home_page_url" t.integer "default_branch_protection", default: 2 + t.boolean "twitter_sharing_enabled", default: true end create_table "broadcast_messages", force: true do |t| @@ -333,10 +334,10 @@ ActiveRecord::Schema.define(version: 20150211174341) do t.string "import_url" t.integer "visibility_level", default: 0, null: false t.boolean "archived", default: false, null: false + t.string "avatar" t.string "import_status" t.float "repository_size", default: 0.0 t.integer "star_count", default: 0, null: false - t.string "avatar" t.string "import_type" t.string "import_source" end @@ -440,6 +441,7 @@ ActiveRecord::Schema.define(version: 20150211174341) do t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" + t.datetime "last_credential_check_at" t.string "avatar" t.string "confirmation_token" t.datetime "confirmed_at" @@ -447,7 +449,6 @@ ActiveRecord::Schema.define(version: 20150211174341) do t.string "unconfirmed_email" t.boolean "hide_no_ssh_key", default: false t.string "website_url", default: "", null: false - t.datetime "last_credential_check_at" t.string "github_access_token" t.string "gitlab_access_token" t.string "notification_email" -- GitLab From d702a2525df1b7a9a9fc774e04ceac717b5f2932 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 12:09:11 +0100 Subject: [PATCH 0877/1609] Update changelog. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 2e0d86862bf..8aa8a8151ca 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -50,6 +50,7 @@ v 7.8.0 (unreleased) - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. - Don't allow page to be scaled on mobile. - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up. + - Show users button to share their newly created public or internal projects on twitter v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch -- GitLab From 161d15541a65ba167830f9a9bf5d181d0c5f4d77 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 12:39:11 +0100 Subject: [PATCH 0878/1609] Prevent autogenerated OAuth username to clash with existing namespace. --- app/models/namespace.rb | 4 ++++ app/models/user.rb | 5 +++-- spec/models/user_spec.rb | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/models/namespace.rb b/app/models/namespace.rb index ba0b2b71cf9..2c7ed376265 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -44,6 +44,10 @@ class Namespace < ActiveRecord::Base scope :root, -> { where('type IS NULL') } + def self.by_path(path) + where('lower(path) = :value', value: path.downcase).first + end + def self.search(query) where("name LIKE :query OR path LIKE :query", query: "%#{query}%") end diff --git a/app/models/user.rb b/app/models/user.rb index d7f688ec138..a97678999bc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -252,7 +252,7 @@ class User < ActiveRecord::Base counter = 0 base = username - while by_login(username).present? + while User.by_login(username).present? || Namespace.by_path(username).present? counter += 1 username = "#{base}#{counter}" end @@ -290,7 +290,8 @@ class User < ActiveRecord::Base def namespace_uniq namespace_name = self.username - if Namespace.find_by(path: namespace_name) + existing_namespace = Namespace.by_path(namespace_name) + if existing_namespace && existing_namespace != self.namespace self.errors.add :username, "already exists" end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6102b2e30be..c015a1d2687 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -303,8 +303,8 @@ describe User do describe ".clean_username" do - let!(:user1) { create(:user, username: "johngitlab-etc") } - let!(:user2) { create(:user, username: "JohnGitLab-etc1") } + let!(:user) { create(:user, username: "johngitlab-etc") } + let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") } it "cleans a username and makes sure it's available" do expect(User.clean_username("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2") -- GitLab From 25ff20677e898a3977f4e46baed75626a3987029 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 13 Feb 2015 14:58:54 +0200 Subject: [PATCH 0879/1609] log documentation --- doc/README.md | 1 + doc/logs/logs.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 doc/logs/logs.md diff --git a/doc/README.md b/doc/README.md index 8c6d13e8506..79d4f5273ee 100644 --- a/doc/README.md +++ b/doc/README.md @@ -24,6 +24,7 @@ - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. - [Operations](operations/README.md) Keeping GitLab up and running +- [Log system](logs/logs.md) Log system ## Contributor documentation diff --git a/doc/logs/logs.md b/doc/logs/logs.md new file mode 100644 index 00000000000..07302894dd4 --- /dev/null +++ b/doc/logs/logs.md @@ -0,0 +1,100 @@ +## Log system +GitLab has advanced log system so everything is logging and you can analize your instance using various system log files. +These log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. + +#### production.log +This file lives in `/var/log/gitlab/gitlab-rails/production.log` for omnibus package or in `/home/git/gitlab/logs/production.log` for installations from the source. + +This file contains information about all performed requests. You can see url and type of request, IP address and what exactly parts of code were involved to service this particular request. Also you can see all SQL request that have been performed and how much time it took. +This task is more useful for GitLab contributors and developers. Use part of this log file when you are going to report bug. + +``` +Started GET "/gitlabhq/yaml_db/tree/master" for 168.111.56.1 at 2015-02-12 19:34:53 +0200 +Processing by Projects::TreeController#show as HTML + Parameters: {"project_id"=>"gitlabhq/yaml_db", "id"=>"master"} + + ... [CUT OUT] + + amespaces"."created_at" DESC, "namespaces"."id" DESC LIMIT 1 [["id", 26]] + CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members"."type" IN ('ProjectMember') AND "members"."source_id" = $1 AND "members"."source_type" = $2 AND "members"."user_id" = 1 ORDER BY "members"."created_at" DESC, "members"."id" DESC LIMIT 1 [["source_id", 18], ["source_type", "Project"]] + CACHE (0.0ms) SELECT "members".* FROM "members" WHERE "members"."source_type" = 'Project' AND "members". +  (1.4ms) SELECT COUNT(*) FROM "merge_requests" WHERE "merge_requests"."target_project_id" = $1 AND ("merge_requests"."state" IN ('opened','reopened')) [["target_project_id", 18]] + Rendered layouts/nav/_project.html.haml (28.0ms) + Rendered layouts/_collapse_button.html.haml (0.2ms) + Rendered layouts/_flash.html.haml (0.1ms) + Rendered layouts/_page.html.haml (32.9ms) +Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms) +``` +In this example we can see that server processed HTTP request with url `/gitlabhq/yaml_db/tree/master` from IP 168.111.56.1 at 2015-02-12 19:34:53 +0200. Also we can see that request was processed by Projects::TreeController. + +#### application.log +This file lives in `/var/log/gitlab/gitlab-rails/application.log` for omnibus package or in `/home/git/gitlab/logs/application.log` for installations from the source. + +This log file helps you discover events happening in your instance such as user creation, project removing and so on. + +``` +October 06, 2014 11:56: User "Administrator" (admin@example.com) was created +October 06, 2014 11:56: Documentcloud created a new project "Documentcloud / Underscore" +October 06, 2014 11:56: Gitlab Org created a new project "Gitlab Org / Gitlab Ce" +October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was removed +October 07, 2014 11:25: Project "project133" was removed +``` +#### githost.log +This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for omnibus package or in `/home/git/gitlab/logs/githost.log` for installations from the source. + +The GitLab has to interact with git repositories but in some rare cases something can go wrong and in this case you will know what exactly happened. This log file contains all failed requests from GitLab to git repository. In majority of cases this file will be useful for developers only. +``` +December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq/.git --work-tree=/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/group184/gitlabhq merge --no-ff -mMerge branch 'feature_conflict' into 'feature' source/feature_conflict + +error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git' +``` + +#### satellites.log +This file lives in `/var/log/gitlab/gitlab-rails/satellites.log` for omnibus package or in `/home/git/gitlab/logs/satellites.log` for installations from the source. + +In some cases GitLab should perform write actions to git repository, for example when it is needed to merge the merge request or edit a file with online editor. If something went wrong you can look into this file to find out what exactly happened. +``` +October 07, 2014 11:36: Failed to create satellite for Chesley Weimann III / project1817 +October 07, 2014 11:36: PID: 1872: git clone /Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/repositories/conrad6841/gitlabhq.git /Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/gitlab-satellites/conrad6841/gitlabhq +October 07, 2014 11:36: PID: 1872: -> fatal: repository '/Users/vsizov/gitlab-development-kit/gitlab/tmp/tests/repositories/conrad6841/gitlabhq.git' does not exist +``` + +#### sidekiq.log +This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for omnibus package or in `/home/git/gitlab/logs/sidekiq.log` for installations from the source. + +GitLab uses background jobs for processing tasks which can take a long time. All information about processing these jobs are writing down to this file. +``` +2014-06-10T07:55:20Z 2037 TID-tm504 ERROR: /opt/bitnami/apps/discourse/htdocs/vendor/bundle/ruby/1.9.1/gems/redis-3.0.7/lib/redis/client.rb:228:in `read' +2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"} +``` + +#### gitlab-shell.log +This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for omnibus package or in `/home/git/gitlab-shell/logs/sidekiq.log` for installations from the source. + +gitlab-shell is using by Gitlab for executing git commands and provide ssh access to git repositories. + +``` +I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git at . +I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory and simlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git. +``` + +#### unicorn_stderr.log +This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for omnibus package or in `/home/git/gitlab/logs/unicorn_stderr.log` for installations from the source. + +Unicorn is a high-performance forking Web server which is used for serving GitLab application. You can look at this log, for example, if your application does not respond. This log cantains all information about state of unicorn processes at any given time. + +``` +I, [2015-02-13T06:14:46.680381 #9047] INFO -- : Refreshing Gem list +I, [2015-02-13T06:14:56.931002 #9047] INFO -- : listening on addr=127.0.0.1:8080 fd=12 +I, [2015-02-13T06:14:56.931381 #9047] INFO -- : listening on addr=/var/opt/gitlab/gitlab-rails/sockets/gitlab.socket fd=13 +I, [2015-02-13T06:14:56.936638 #9047] INFO -- : master process ready +I, [2015-02-13T06:14:56.946504 #9092] INFO -- : worker=0 spawned pid=9092 +I, [2015-02-13T06:14:56.946943 #9092] INFO -- : worker=0 ready +I, [2015-02-13T06:14:56.947892 #9094] INFO -- : worker=1 spawned pid=9094 +I, [2015-02-13T06:14:56.948181 #9094] INFO -- : worker=1 ready +W, [2015-02-13T07:16:01.312916 #9094] WARN -- : #: worker (pid: 9094) exceeds memory limit (320626688 bytes > 247066940 bytes) +W, [2015-02-13T07:16:01.313000 #9094] WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 9094) alive: 3621 sec (trial 1) +I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped # worker=1 +I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379 +I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready +``` -- GitLab From 34cc4c598232d2e27dcc99f5534dbe318e89cff9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 14:31:42 +0100 Subject: [PATCH 0880/1609] Link head panel titles to relevant root page. --- CHANGELOG | 1 + app/controllers/snippets_controller.rb | 1 + app/controllers/users_controller.rb | 1 + app/views/layouts/admin.html.haml | 2 +- app/views/layouts/application.html.haml | 2 +- app/views/layouts/explore.html.haml | 4 ++-- app/views/layouts/group.html.haml | 2 +- app/views/layouts/navless.html.haml | 2 +- app/views/layouts/profile.html.haml | 2 +- app/views/layouts/public_group.html.haml | 2 +- app/views/layouts/public_users.html.haml | 2 +- app/views/layouts/search.html.haml | 2 +- 12 files changed, 13 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2e0d86862bf..4fb635ea1e5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -50,6 +50,7 @@ v 7.8.0 (unreleased) - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. - Don't allow page to be scaled on mobile. - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up. + - Link head panel titles to relevant root page. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 1ed3bc388fb..6ac048e4b83 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -106,6 +106,7 @@ class SnippetsController < ApplicationController def set_title @title = 'Snippets' + @title_url = snippets_path end def snippet_params diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 57d8ef09faf..84a04c5ebe6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -19,6 +19,7 @@ class UsersController < ApplicationController where(project_id: authorized_projects_ids).limit(30) @title = @user.name + @title_url = user_path(@user) respond_to do |format| format.html diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index dc8652cb145..e8751a6987e 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -2,5 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: "Admin area" %body{class: "#{app_theme} #{theme_type} admin", :'data-page' => body_data_page} - = render "layouts/head_panel", title: "Admin area" + = render "layouts/head_panel", title: link_to("Admin area", admin_root_path) = render 'layouts/page', sidebar: 'layouts/nav/admin' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index e5420a13605..49123744ffa 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,5 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: "Dashboard" %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page } - = render "layouts/head_panel", title: "Dashboard" + = render "layouts/head_panel", title: link_to("Dashboard", root_path) = render 'layouts/page', sidebar: 'layouts/nav/dashboard' diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml index 9813d846542..09855b222dc 100644 --- a/app/views/layouts/explore.html.haml +++ b/app/views/layouts/explore.html.haml @@ -5,9 +5,9 @@ %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" - if current_user - = render "layouts/head_panel", title: page_title + = render "layouts/head_panel", title: link_to(page_title, explore_root_path) - else - = render "layouts/public_head_panel", title: page_title + = render "layouts/public_head_panel", title: link_to(page_title, explore_root_path) .container.navless-container .content .explore-title diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 98edcf3a140..fa0ed317ce1 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -2,5 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: group_head_title %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} - = render "layouts/head_panel", title: @group.name + = render "layouts/head_panel", title: link_to(@group.name, group_path(@group)) = render 'layouts/page', sidebar: 'layouts/nav/group' diff --git a/app/views/layouts/navless.html.haml b/app/views/layouts/navless.html.haml index 730f3d09277..a3b55542bf7 100644 --- a/app/views/layouts/navless.html.haml +++ b/app/views/layouts/navless.html.haml @@ -3,7 +3,7 @@ = render "layouts/head", title: @title %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" - = render "layouts/head_panel", title: @title + = render "layouts/head_panel", title: defined?(@title_url) ? link_to(@title, @title_url) : @title .container.navless-container .content = render "layouts/flash" diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 89d816061e2..19d6efed78e 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -2,5 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: "Profile" %body{class: "#{app_theme} #{theme_type} profile", :'data-page' => body_data_page} - = render "layouts/head_panel", title: "Profile" + = render "layouts/head_panel", title: link_to("Profile", profile_path) = render 'layouts/page', sidebar: 'layouts/nav/profile' diff --git a/app/views/layouts/public_group.html.haml b/app/views/layouts/public_group.html.haml index ae3d2bd8a89..4b69329b8fe 100644 --- a/app/views/layouts/public_group.html.haml +++ b/app/views/layouts/public_group.html.haml @@ -2,5 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: group_head_title %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} - = render "layouts/public_head_panel", title: "group: #{@group.name}" + = render "layouts/public_head_panel", title: link_to(@group.name, group_path(@group)) = render 'layouts/page', sidebar: 'layouts/nav/group' diff --git a/app/views/layouts/public_users.html.haml b/app/views/layouts/public_users.html.haml index 37767df33d2..3538a8b1699 100644 --- a/app/views/layouts/public_users.html.haml +++ b/app/views/layouts/public_users.html.haml @@ -2,5 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: @title %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} - = render "layouts/public_head_panel", title: @title + = render "layouts/public_head_panel", title: defined?(@title_url) ? link_to(@title, @title_url) : @title = render 'layouts/page' diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml index 6d001e7ee1c..177e2073a0d 100644 --- a/app/views/layouts/search.html.haml +++ b/app/views/layouts/search.html.haml @@ -3,7 +3,7 @@ = render "layouts/head", title: "Search" %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} = render "layouts/broadcast" - = render "layouts/head_panel", title: "Search" + = render "layouts/head_panel", title: link_to("Search", search_path) .container.navless-container .content = render "layouts/flash" -- GitLab From 421e882ea422a4a33b64f0c5d466d6b22731199c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 13:48:27 +0100 Subject: [PATCH 0881/1609] Fix specs. --- features/dashboard/dashboard.feature | 4 ++-- features/steps/dashboard/dashboard.rb | 8 ++++---- spec/requests/api/projects_spec.rb | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index bebaa78e46c..1959d327082 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -27,11 +27,11 @@ Feature: Dashboard Scenario: I should see User joined Project event Given user with name "John Doe" joined project "Shop" When I visit dashboard page - Then I should see "John Doe joined project at Shop" event + Then I should see "John Doe joined project Shop" event @javascript Scenario: I should see User left Project event Given user with name "John Doe" joined project "Shop" And user with name "John Doe" left project "Shop" When I visit dashboard page - Then I should see "John Doe left project at Shop" event + Then I should see "John Doe left project Shop" event diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 1826ead1d51..961f8b284b8 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -37,8 +37,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps ) end - step 'I should see "John Doe joined project at Shop" event' do - page.should have_content "John Doe joined project at #{project.name_with_namespace}" + step 'I should see "John Doe joined project Shop" event' do + page.should have_content "John Doe joined project #{project.name_with_namespace}" end step 'user with name "John Doe" left project "Shop"' do @@ -50,8 +50,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps ) end - step 'I should see "John Doe left project at Shop" event' do - page.should have_content "John Doe left project at #{project.name_with_namespace}" + step 'I should see "John Doe left project Shop" event' do + page.should have_content "John Doe left project #{project.name_with_namespace}" end step 'I have group with projects' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 170ede57310..0b3a47e3273 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -347,7 +347,7 @@ describe API::API, api: true do end describe 'GET /projects/:id/events' do - before { project_member } + before { project_member2 } it 'should return a project events' do get api("/projects/#{project.id}/events", user) @@ -356,7 +356,7 @@ describe API::API, api: true do expect(json_event['action_name']).to eq('joined') expect(json_event['project_id'].to_i).to eq(project.id) - expect(json_event['author_username']).to eq(user.username) + expect(json_event['author_username']).to eq(user3.username) end it 'should return a 404 error if not found' do -- GitLab From 25e44d05300a6b5b35232b27b4ccb27f47f09a67 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 13:33:28 +0100 Subject: [PATCH 0882/1609] Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). --- CHANGELOG | 1 + app/assets/javascripts/project.js.coffee | 8 +++- app/controllers/admin/users_controller.rb | 2 +- .../profiles/passwords_controller.rb | 8 ++-- app/controllers/profiles_controller.rb | 2 +- app/models/user.rb | 2 + app/views/profiles/passwords/edit.html.haml | 23 +++++++---- app/views/profiles/passwords/new.html.haml | 9 ++-- app/views/projects/empty.html.haml | 1 + app/views/projects/show.html.haml | 1 + app/views/shared/_clone_panel.html.haml | 16 +++++++- app/views/shared/_no_password.html.haml | 8 ++++ app/views/shared/_no_ssh.html.haml | 2 +- ...0213114800_add_hide_no_password_to_user.rb | 5 +++ ..._add_password_automatically_set_to_user.rb | 5 +++ db/schema.rb | 41 ++++++++++--------- lib/gitlab/oauth/user.rb | 11 ++--- 17 files changed, 99 insertions(+), 46 deletions(-) create mode 100644 app/views/shared/_no_password.html.haml create mode 100644 db/migrate/20150213114800_add_hide_no_password_to_user.rb create mode 100644 db/migrate/20150213121042_add_password_automatically_set_to_user.rb diff --git a/CHANGELOG b/CHANGELOG index 2e0d86862bf..ead51cb3c8a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -50,6 +50,7 @@ v 7.8.0 (unreleased) - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. - Don't allow page to be scaled on mobile. - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up. + - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 5a9cc66c8f0..eb8c1fa1426 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -16,5 +16,11 @@ class @Project $('.hide-no-ssh-message').on 'click', (e) -> path = '/' $.cookie('hide_no_ssh_message', 'false', { path: path }) - $(@).parents('.no-ssh-key-message').hide() + $(@).parents('.no-ssh-key-message').remove() + e.preventDefault() + + $('.hide-no-password-message').on 'click', (e) -> + path = '/' + $.cookie('hide_no_password_message', 'false', { path: path }) + $(@).parents('.no-password-message').remove() e.preventDefault() diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 232f30b759d..ecedb31a7f8 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -121,7 +121,7 @@ class Admin::UsersController < Admin::ApplicationController params.require(:user).permit( :email, :remember_me, :bio, :name, :username, :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, - :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, + :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password, :projects_limit, :can_create_group, :admin, :key_id ) end diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index 1191ce47eba..0c614969a3f 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -11,7 +11,7 @@ class Profiles::PasswordsController < ApplicationController end def create - unless @user.valid_password?(user_params[:current_password]) + unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password]) redirect_to new_profile_password_path, alert: 'You must provide a valid current password' return end @@ -21,7 +21,8 @@ class Profiles::PasswordsController < ApplicationController result = @user.update_attributes( password: new_password, - password_confirmation: new_password_confirmation + password_confirmation: new_password_confirmation, + password_automatically_set: false ) if result @@ -39,8 +40,9 @@ class Profiles::PasswordsController < ApplicationController password_attributes = user_params.select do |key, value| %w(password password_confirmation).include?(key.to_s) end + password_attributes[:password_automatically_set] = false - unless @user.valid_password?(user_params[:current_password]) + unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password]) redirect_to edit_profile_password_path, alert: 'You must provide a valid current password' return end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index c0b7e2223a2..f7584c03411 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -67,7 +67,7 @@ class ProfilesController < ApplicationController params.require(:user).permit( :email, :password, :password_confirmation, :bio, :name, :username, :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, - :avatar, :hide_no_ssh_key, + :avatar, :hide_no_ssh_key, :hide_no_password ) end end diff --git a/app/models/user.rb b/app/models/user.rb index d7f688ec138..23d1e69e69a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,6 +40,7 @@ # confirmation_sent_at :datetime # unconfirmed_email :string(255) # hide_no_ssh_key :boolean default(FALSE) +# hide_no_password :boolean default(FALSE) # website_url :string(255) default(""), not null # last_credential_check_at :datetime # github_access_token :string(255) @@ -60,6 +61,7 @@ class User < ActiveRecord::Base default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false + default_value_for :hide_no_password, false default_value_for :projects_limit, current_application_settings.default_projects_limit default_value_for :theme_id, gitlab_config.default_theme diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 2a7d317aa3e..6b19db4eb5d 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,25 +1,30 @@ %h3.page-title Password %p.light - Change your password or recover your current one. + - if @user.password_automatically_set? + Set your password. + - else + Change your password or recover your current one. %hr .update-password = form_for @user, url: profile_password_path, method: :put, html: { class: 'form-horizontal' } do |f| %div %p.slead - You must provide current password in order to change it. - %br + - unless @user.password_automatically_set? + You must provide current password in order to change it. + %br After a successful password update you will be redirected to login page where you should login with your new password -if @user.errors.any? .alert.alert-danger %ul - @user.errors.full_messages.each do |msg| %li= msg - .form-group - = f.label :current_password, class: 'control-label' - .col-sm-10 - = f.password_field :current_password, required: true, class: 'form-control' - %div - = link_to "Forgot your password?", reset_profile_password_path, method: :put + - unless @user.password_automatically_set? + .form-group + = f.label :current_password, class: 'control-label' + .col-sm-10 + = f.password_field :current_password, required: true, class: 'form-control' + %div + = link_to "Forgot your password?", reset_profile_password_path, method: :put .form-group = f.label :password, 'New password', class: 'control-label' diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index aef7348fd20..8bed6e0dbee 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -10,10 +10,11 @@ %ul - @user.errors.full_messages.each do |msg| %li= msg - - .form-group - = f.label :current_password, class: 'control-label' - .col-sm-10= f.password_field :current_password, required: true, class: 'form-control' + + - unless @user.password_automatically_set? + .form-group + = f.label :current_password, class: 'control-label' + .col-sm-10= f.password_field :current_password, required: true, class: 'form-control' .form-group = f.label :password, class: 'control-label' .col-sm-10= f.password_field :password, required: true, class: 'form-control' diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index d7dee2208de..b925bcb7fac 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,5 +1,6 @@ - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' + = render 'shared/no_password' = render "home_panel" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 737a34decde..435b2648404 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -1,5 +1,6 @@ - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' + = render 'shared/no_password' = render "home_panel" diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 1cc6043f56b..df0bde76980 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,8 +1,20 @@ - project = project || @project .git-clone-holder.input-group .input-group-btn - %button{class: "btn #{ 'active' if default_clone_protocol == 'ssh' }", :"data-clone" => project.ssh_url_to_repo} SSH - %button{class: "btn #{ 'active' if default_clone_protocol == 'http' }", :"data-clone" => project.http_url_to_repo}= gitlab_config.protocol.upcase + %button{ | + class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", | + :"data-clone" => project.ssh_url_to_repo, | + :"data-title" => "Add an SSH key to your profile
    to pull or push via SSH", + :"data-html" => "true", + :"data-container" => "body"} + SSH + %button{ | + class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.password_automatically_set? }", | + :"data-clone" => project.http_url_to_repo, | + :"data-title" => "Set a password on your account
    to pull or push via #{gitlab_config.protocol.upcase}", + :"data-html" => "true", + :"data-container" => "body"} + = gitlab_config.protocol.upcase = text_field_tag :project_clone, default_url_to_repo(project), class: "one_click_select form-control", readonly: true - if project.kind_of?(Project) .input-group-addon diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml new file mode 100644 index 00000000000..022097cda16 --- /dev/null +++ b/app/views/shared/_no_password.html.haml @@ -0,0 +1,8 @@ +- if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.password_automatically_set? + .no-password-message.alert.alert-warning.hidden-xs + You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account + + .pull-right + = link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put + | + = link_to 'Remind later', '#', class: 'hide-no-password-message' diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index 8e6f802fd3b..1a2946baccb 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,4 +1,4 @@ -- if cookies[:hide_no_ssh_message].blank? && current_user.require_ssh_key? && !current_user.hide_no_ssh_key +- if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? .no-ssh-key-message.alert.alert-warning.hidden-xs You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path} to your profile diff --git a/db/migrate/20150213114800_add_hide_no_password_to_user.rb b/db/migrate/20150213114800_add_hide_no_password_to_user.rb new file mode 100644 index 00000000000..685f0844276 --- /dev/null +++ b/db/migrate/20150213114800_add_hide_no_password_to_user.rb @@ -0,0 +1,5 @@ +class AddHideNoPasswordToUser < ActiveRecord::Migration + def change + add_column :users, :hide_no_password, :boolean, default: false + end +end diff --git a/db/migrate/20150213121042_add_password_automatically_set_to_user.rb b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb new file mode 100644 index 00000000000..c3c7c1ffc77 --- /dev/null +++ b/db/migrate/20150213121042_add_password_automatically_set_to_user.rb @@ -0,0 +1,5 @@ +class AddPasswordAutomaticallySetToUser < ActiveRecord::Migration + def change + add_column :users, :password_automatically_set, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index f33766a1fe8..e11a068c9c5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150211174341) do +ActiveRecord::Schema.define(version: 20150213121042) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -26,6 +26,7 @@ ActiveRecord::Schema.define(version: 20150211174341) do t.datetime "updated_at" t.string "home_page_url" t.integer "default_branch_protection", default: 2 + t.boolean "twitter_sharing_enabled", default: true end create_table "broadcast_messages", force: true do |t| @@ -333,10 +334,10 @@ ActiveRecord::Schema.define(version: 20150211174341) do t.string "import_url" t.integer "visibility_level", default: 0, null: false t.boolean "archived", default: false, null: false + t.string "avatar" t.string "import_status" t.float "repository_size", default: 0.0 t.integer "star_count", default: 0, null: false - t.string "avatar" t.string "import_type" t.string "import_source" end @@ -409,12 +410,12 @@ ActiveRecord::Schema.define(version: 20150211174341) do end create_table "users", force: true do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" @@ -422,35 +423,37 @@ ActiveRecord::Schema.define(version: 20150211174341) do t.datetime "created_at" t.datetime "updated_at" t.string "name" - t.boolean "admin", default: false, null: false - t.integer "projects_limit", default: 10 - t.string "skype", default: "", null: false - t.string "linkedin", default: "", null: false - t.string "twitter", default: "", null: false + t.boolean "admin", default: false, null: false + t.integer "projects_limit", default: 10 + t.string "skype", default: "", null: false + t.string "linkedin", default: "", null: false + t.string "twitter", default: "", null: false t.string "authentication_token" - t.integer "theme_id", default: 1, null: false + t.integer "theme_id", default: 1, null: false t.string "bio" - t.integer "failed_attempts", default: 0 + t.integer "failed_attempts", default: 0 t.datetime "locked_at" t.string "username" - t.boolean "can_create_group", default: true, null: false - t.boolean "can_create_team", default: true, null: false + t.boolean "can_create_group", default: true, null: false + t.boolean "can_create_team", default: true, null: false t.string "state" - t.integer "color_scheme_id", default: 1, null: false - t.integer "notification_level", default: 1, null: false + t.integer "color_scheme_id", default: 1, null: false + t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" + t.datetime "last_credential_check_at" t.string "avatar" t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" - t.boolean "hide_no_ssh_key", default: false - t.string "website_url", default: "", null: false - t.datetime "last_credential_check_at" + t.boolean "hide_no_ssh_key", default: false + t.string "website_url", default: "", null: false t.string "github_access_token" t.string "gitlab_access_token" t.string "notification_email" + t.boolean "hide_no_password", default: false + t.boolean "password_automatically_set", default: false end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index 9f55e8c4950..c023d275703 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -85,11 +85,12 @@ module Gitlab def user_attributes { - name: auth_hash.name, - username: ::User.clean_username(auth_hash.username), - email: auth_hash.email, - password: auth_hash.password, - password_confirmation: auth_hash.password + name: auth_hash.name, + username: ::User.clean_username(auth_hash.username), + email: auth_hash.email, + password: auth_hash.password, + password_confirmation: auth_hash.password, + password_automatically_set: true } end -- GitLab From da1608196b2c8401939c727ec70efc9c342e4945 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 15:16:00 +0100 Subject: [PATCH 0883/1609] Add headings to signin/signup blocks on signin page. --- app/assets/stylesheets/sections/login.scss | 3 +-- app/views/devise/shared/_signin_box.html.haml | 8 ++++++-- app/views/devise/shared/_signup_box.html.haml | 8 ++++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/sections/login.scss b/app/assets/stylesheets/sections/login.scss index 3a3644c12b7..d366300511e 100644 --- a/app/assets/stylesheets/sections/login.scss +++ b/app/assets/stylesheets/sections/login.scss @@ -40,8 +40,7 @@ .login-heading h3 { font-weight: 300; line-height: 1.5; - margin: 0; - display: none; + margin: 0 0 10px 0; } .login-footer { diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 70587329033..04d33132e91 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -1,6 +1,10 @@ .login-box - .login-heading - %h3 Sign in + - if signup_enabled? + .login-heading + %h3 Existing user? Sign in + - else + .login-heading + %h3 Sign in .login-body - if ldap_enabled? %ul.nav.nav-tabs diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 8a6dc19ab64..0db123d78ff 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -1,6 +1,10 @@ .login-box - .login-heading - %h3 Sign up + - if signin_enabled? + .login-heading + %h3 New user? Create an account + - else + .login-heading + %h3 Create an account .login-body = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| .devise-errors -- GitLab From 7c39e728ef348482bd8caeeeeba2316ace92f4e9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 15:16:17 +0100 Subject: [PATCH 0884/1609] Move OAuth signin options just below signin box. --- app/views/devise/registrations/new.html.haml | 6 +----- app/views/devise/sessions/new.html.haml | 9 --------- app/views/devise/shared/_oauth_box.html.haml | 10 ---------- app/views/devise/shared/_signin_box.html.haml | 13 +++++++++++++ app/views/devise/shared/_signup_box.html.haml | 5 +++++ 5 files changed, 19 insertions(+), 24 deletions(-) delete mode 100644 app/views/devise/shared/_oauth_box.html.haml diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index c07e409d583..d3e37f7494c 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -1,7 +1,3 @@ = render 'devise/shared/signup_box' -.clearfix.prepend-top-20 - = render 'devise/shared/sign_in_link' - %p - %span.light Did not receive confirmation email? - = link_to "Send again", new_confirmation_path(resource_name) \ No newline at end of file += render 'devise/shared/sign_in_link' \ No newline at end of file diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 6d8415613d1..fa2460518fc 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,15 +1,6 @@ %div = render 'devise/shared/signin_box' - - if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable? - .prepend-top-20 - = render 'devise/shared/oauth_box' - - if signup_enabled? .prepend-top-20 = render 'devise/shared/signup_box' - -.clearfix.prepend-top-20 - %p - %span.light Did not receive confirmation email? - = link_to "Send again", new_confirmation_path(resource_name) diff --git a/app/views/devise/shared/_oauth_box.html.haml b/app/views/devise/shared/_oauth_box.html.haml deleted file mode 100644 index c2e1373de30..00000000000 --- a/app/views/devise/shared/_oauth_box.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- providers = additional_providers -- if providers.present? - .login-box{:'data-no-turbolink' => 'data-no-turbolink'} - %span Sign in with   - - providers.each do |provider| - %span - - if default_providers.include?(provider) - = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider) - - else - = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn" diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 04d33132e91..805cf816231 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -27,3 +27,16 @@ - else %div No authentication methods configured. + +- if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable? + .clearfix.prepend-top-20 + %p + %span.light + Sign in with   + - providers = additional_providers + - providers.each do |provider| + %span.light + - if default_providers.include?(provider) + = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider) + - else + = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn" \ No newline at end of file diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 0db123d78ff..dcf60c90430 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -19,3 +19,8 @@ = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true %div = f.submit "Sign up", class: "btn-create btn" + +.clearfix.prepend-top-20 + %p + %span.light Did not receive confirmation email? + = link_to "Send again", new_confirmation_path(resource_name) \ No newline at end of file -- GitLab From 4a62a0f01a0058d1d260ddecb0341f48b5b2e26d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 15:30:54 +0100 Subject: [PATCH 0885/1609] Only send "Account was created for you" email when created by admin. --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index d7f688ec138..278f5c662df 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -556,7 +556,7 @@ class User < ActiveRecord::Base def post_create_hook log_info("User \"#{self.name}\" (#{self.email}) was created") - notification_service.new_user(self, @reset_token) + notification_service.new_user(self, @reset_token) if self.created_by_id system_hook_service.execute_hooks_for(self, :create) end -- GitLab From 055c2f1e33ffe0bae1b5f1ec6d5fea68ee055bad Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 15:43:02 +0100 Subject: [PATCH 0886/1609] Add "New Project" button to dashboard projects page. --- app/views/dashboard/_zero_authorized_projects.html.haml | 8 +++++--- app/views/dashboard/projects.html.haml | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/_zero_authorized_projects.html.haml index f78ce69ef9e..6e76f95b34e 100644 --- a/app/views/dashboard/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/_zero_authorized_projects.html.haml @@ -17,7 +17,8 @@ - if current_user.can_create_project? .link_holder = link_to new_project_path, class: "btn btn-new" do - New project » + %i.fa.fa-plus + New Project - if current_user.can_create_group? %hr @@ -31,7 +32,8 @@ Groups are the best way to manage projects and members. .link_holder = link_to new_group_path, class: "btn btn-new" do - New group » + %i.fa.fa-plus + New Group -if @publicish_project_count > 0 %hr @@ -47,4 +49,4 @@ Public projects are an easy way to allow everyone to have read-only access. .link_holder = link_to trending_explore_projects_path, class: "btn btn-new" do - Browse public projects » + Browse public projects diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml index dba3025b3cc..21e44fb1c60 100644 --- a/app/views/dashboard/projects.html.haml +++ b/app/views/dashboard/projects.html.haml @@ -1,6 +1,10 @@ %h3.page-title My Projects + = link_to new_project_path, class: "btn btn-new pull-right" do + %i.fa.fa-plus + New Project + %p.light All projects you have access to are listed here. Public projects are not included here unless you are a member %hr -- GitLab From 01c6806f804d9b76042229e11077190975eb8bf0 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 13 Feb 2015 08:56:25 -0800 Subject: [PATCH 0887/1609] Text changes recommended by Job. --- doc/integration/external-issue-tracker.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index a4f67daa563..53d6898b6e8 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,6 +1,6 @@ # External issue tracker -GitLab has a great issue tracker but you can also use an external issue tracker such as Jira, Bugzilla or Redmine. This is something that you can turn on per GitLab project. If for example you configure Jira it provides the following functionality: +GitLab has a great issue tracker but you can also use an external issue tracker such as Jira, Bugzilla or Redmine. You can configure issue trackers per GitLab project. For instance, if you configure Jira it allows you to do the following: - the 'Issues' link on the GitLab project pages takes you to the appropriate Jira issue index; - clicking 'New issue' on the project dashboard creates a new Jira issue; @@ -12,7 +12,7 @@ GitLab has a great issue tracker but you can also use an external issue tracker ### Project Service -External issue tracker can be enabled per project basis. As an example, we will configure `Redmine` for project named gitlab-ci. +You can enable an external issue tracker per project. As an example, we will configure `Redmine` for project named gitlab-ci. Fill in the required details on the page: @@ -20,14 +20,14 @@ Fill in the required details on the page: * `description` A name for the issue tracker (to differentiate between instances, for example). * `project_url` The URL to the project in Redmine which is being linked to this GitLab project. -* `issues_url` The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the url. This id GitLab uses as a placeholder to replace the issue number. +* `issues_url` The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the url. This id is used by GitLab as a placeholder to replace the issue number. * `new_issue_url` This is the URL to create a new issue in Redmine for the project linked to this GitLab project. ### Service Template -Since external issue tracker needs some project specific details, it is required to enable issue tracker per project level. -GitLab makes this easier by allowing admin to add a service template which will allow GitLab project user with permissions to edit details for its project. +It is necessary to configure the external issue tracker per project, because project specific details are needed for the integration with GitLab. +The admin can add a service template that sets a default for each project. This makes it much easier to configure individual projects. In GitLab Admin section, navigate to `Service Templates` and choose the service template you want to create: -- GitLab From 7474b1b03f3111e1fefa05cbee1432a35bc55c6d Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 13 Feb 2015 09:15:08 -0800 Subject: [PATCH 0888/1609] Add assignees in mr page to the CHANGELOG. --- CHANGELOG | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2e0d86862bf..ccf7043f57e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -50,6 +50,7 @@ v 7.8.0 (unreleased) - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. - Don't allow page to be scaled on mobile. - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up. + - Show assignees in merge request index page (Kelvin Mutuma) v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch @@ -85,9 +86,9 @@ v 7.7.0 - When accept merge request - do merge using sidaekiq job - Enable web signups by default - Fixes for diff comments: drag-n-drop images, selecting images - - Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update + - Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update - Remove password strength indicator - + v 7.6.0 -- GitLab From d9b32f20c6847e45200c38cc4476c3b825434f4f Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 13 Feb 2015 18:17:08 +0200 Subject: [PATCH 0889/1609] OAuth2 provider documentation --- doc/integration/README.md | 3 +- doc/integration/oauth_provider.md | 31 ++++++++++++++++++ .../oauth_provider/admin_application.png | Bin 0 -> 55533 bytes .../oauth_provider/application_form.png | Bin 0 -> 25075 bytes .../oauth_provider/authorized_application.png | Bin 0 -> 17260 bytes .../oauth_provider/user_wide_applications.png | Bin 0 -> 46238 bytes 6 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 doc/integration/oauth_provider.md create mode 100644 doc/integration/oauth_provider/admin_application.png create mode 100644 doc/integration/oauth_provider/application_form.png create mode 100644 doc/integration/oauth_provider/authorized_application.png create mode 100644 doc/integration/oauth_provider/user_wide_applications.png diff --git a/doc/integration/README.md b/doc/integration/README.md index 0087167bb84..1fc8ab997ec 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -6,8 +6,9 @@ See the documentation below for details on how to configure these services. - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, and Google via OAuth. +- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. - [Slack](slack.md) Integrate with the Slack chat service +- [OAuth2 provider](oauth_provider.md) OAuth2 application creation Jenkins support is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jenkins.html). diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md new file mode 100644 index 00000000000..5fdb74a43df --- /dev/null +++ b/doc/integration/oauth_provider.md @@ -0,0 +1,31 @@ +## GitLab as OAuth2 provider +OAuth2 provides client applications a 'secure delegated access' to server resources on behalf of a resource owner. Or you can allow users to sign in to your application with their GitLab.com account. +In fact OAuth allows to issue access token to third-party clients by an authorization server, +with the approval of the resource owner, or end-user. +Mostly, OAuth2 is using for SSO (Single sign-on). But you can find a lot of different usages for this functionality. +For example, our feature 'GitLab Importer' is using OAuth protocol to give an access to repositories without sharing user credentials to GitLab.com account. +Also GitLab.com application can be used for authentication to your GitLab instance if needed [GitLab OmniAuth](gitlab.md). + +GitLab has two ways to add new OAuth2 application to an instance, you can add application as regular user and through admin area. So GitLab actually can have an instance-wide and a user-wide applications. There is no defferences between them except the different permission levels. + +### Adding application through profile +Go to your profile section 'Application' and press button 'New Application' + +![applications](oauth_provider/user_wide_applications.png) + +After this you will see application form, where "Name" is arbitrary name, "Redirect URI" is URL in your app where users will be sent after authorization on GitLab.com. + +![application_form](oauth_provider/application_form.png) + +### Authorized application +Every application you authorized will be shown in your "Authorized application" sections. + +![authorized_application](oauth_provider/authorized_application.png) + +At any time you can revoke access just clicking button "Revoke" + +### OAuth applications in admin area + +If you want to create application that does not belong to certain user you can create it from admin area + +![admin_application](oauth_provider/admin_application.png) \ No newline at end of file diff --git a/doc/integration/oauth_provider/admin_application.png b/doc/integration/oauth_provider/admin_application.png new file mode 100644 index 0000000000000000000000000000000000000000..a5f34512aa85236ac9ecc7d70129d8a3a3f1a71b GIT binary patch literal 55533 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 literal 0 HcmV?d00001 diff --git a/doc/integration/oauth_provider/application_form.png b/doc/integration/oauth_provider/application_form.png new file mode 100644 index 0000000000000000000000000000000000000000..ae135db2627f24b24e4b4b1ba401ab78e2de0b09 GIT binary patch literal 25075 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

    HU4jpoeA?d zTCBYHW&5LL@;N12U$%3dzOh>S_}ngSXR)->tw{^2Mf@*Lxq5lF;W;6{FLIfIVyDl< zy597AYt?TH$<$k!D$=sA@Y!1NLB>oQ4ty4H*znb6N+3I|d9gT*;o!9uvQ;HA&`wO_ zbH#>)*OeNJ`M_p0FtE6-WoTL(X#M4d1x(Xh}Pp6;# z`+G$(|M2I0hwX8f^-q|Zo7?|S;|c5iGWpQnKOFZGt~M2cTZ#!8tO7Z_YyTg}{-5yD z-fF+X;*)Z>*y6fe)`{%)1lea+`Rd9_$kv`ov+plr>(*Va7x9hbH`tv9pIAOTuTrsD zW`8&H@0Jhs@qf?H=D(%7XUetdjNp;|MIPC=Ql{tq{90wp8vSMal6f|jm)>3}uv-Ee z_Fv@To%c~)qP@8JBBT<05xZdeR4$h2y_X+Cwhe$9YZcoZ^bd+n6P$mxT?T9^XtQa; z0Z{kAr=1(@Z_v)D1Dja|a%^vNSc1I;7HI%^Z3dq>qMwq^t#G4in_v;h+y>BQwnj#7 zg&R@^vU-Rf!+9}>4PSGVpMd4Tk<2XSu)#FJDh5&@f!r`pw;|#6O}7(_V0n-Tn@&SQ z_JNWe0?;^TN+Jp=oWpv?4t12uN&TXj;p7 z`JodmsVIENVPuIuZ{`F~JfLwOmT2Z>hg(4wgT4FUCKJcDdAThp0n}H!zv$18kG1D} zp!@E@j#X)xsawMX)&&ZrL2!&9!$?0zaWnu%Qvm*>L=XJu54GFsd*S6AC(sGEp00i_ I>zopr0NP|&rvLx| literal 0 HcmV?d00001 diff --git a/doc/integration/oauth_provider/authorized_application.png b/doc/integration/oauth_provider/authorized_application.png new file mode 100644 index 0000000000000000000000000000000000000000..d3ce05be9cc71cf965a8941313057f6d0851b085 GIT binary patch literal 17260 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 literal 0 HcmV?d00001 diff --git a/doc/integration/oauth_provider/user_wide_applications.png b/doc/integration/oauth_provider/user_wide_applications.png new file mode 100644 index 0000000000000000000000000000000000000000..719e1974068eaf081fbaea3c0b397654a56f9379 GIT binary patch literal 46238 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

    bIGnK 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 literal 0 HcmV?d00001 -- GitLab From 78124d97836ebdfbb45c5567b9f2775889fd9718 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 13 Feb 2015 20:13:47 +0200 Subject: [PATCH 0890/1609] GitLab.com importer: documentation --- doc/integration/omniauth.md | 1 + doc/workflow/README.md | 1 + doc/workflow/gitlab_importer/importer.png | Bin 0 -> 40778 bytes .../gitlab_importer/new_project_page.png | Bin 0 -> 72663 bytes .../import_projects_from_gitlab_com.md | 18 ++++++++++++++++++ 5 files changed, 20 insertions(+) create mode 100644 doc/workflow/gitlab_importer/importer.png create mode 100644 doc/workflow/gitlab_importer/new_project_page.png create mode 100644 doc/workflow/import_projects_from_gitlab_com.md diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 15b4fb622af..7911cd3e84d 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -76,6 +76,7 @@ Before configuring individual OmniAuth providers there are a few global settings ## Supported Providers - [GitHub](github.md) +- [GitLab](gitlab.md) - [Google](google.md) - [Shibboleth](shibboleth.md) - [Twitter](twitter.md) diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 3c0007d8198..6e70235f5b8 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -9,5 +9,6 @@ - [Notifications](notifications.md) - [Migrating from SVN to GitLab](migrating_from_svn.md) - [Project importing from GitHub to GitLab](import_projects_from_github.md) +- [Project importing from GitLab.com to your private GitLab instance](import_projects_from_gitlab_com.md) - [Protected branches](protected_branches.md) - [Web Editor](web_editor.md) diff --git a/doc/workflow/gitlab_importer/importer.png b/doc/workflow/gitlab_importer/importer.png new file mode 100644 index 0000000000000000000000000000000000000000..d2a286d8cac32caf6594537b6842ebdcfaedee58 GIT binary patch literal 40778 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 Date: Fri, 13 Feb 2015 13:36:59 -0800 Subject: [PATCH 0891/1609] Show tooltips on collapsed sidebar --- app/assets/stylesheets/sections/nav_sidebar.scss | 2 +- app/views/layouts/_page.html.haml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss index b35043821da..9c7d1a03a03 100644 --- a/app/assets/stylesheets/sections/nav_sidebar.scss +++ b/app/assets/stylesheets/sections/nav_sidebar.scss @@ -126,7 +126,7 @@ .nav-sidebar { margin-top: 20px; - position: absolute; + position: fixed; top: 45px; width: 52px; diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 98a3d2278a3..422966cdc55 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -17,3 +17,7 @@ = yield = yield :embedded_scripts + +:coffeescript + $('.page-sidebar-collapsed .nav-sidebar a').tooltip placement: "right" + -- GitLab From d76c5824bc05640d276be96f7853f2d266fd6750 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 13 Feb 2015 14:49:19 -0800 Subject: [PATCH 0892/1609] Update oauth documenatation with examples for omnibus package and installations from source. --- doc/integration/github.md | 38 +++++++++++++---- doc/integration/gitlab.md | 45 +++++++++++++++----- doc/integration/google.md | 39 ++++++++++++++---- doc/integration/omniauth.md | 78 ++++++++++++++++------------------- doc/integration/shibboleth.md | 14 +++---- doc/integration/twitter.md | 35 ++++++++++++---- 6 files changed, 168 insertions(+), 81 deletions(-) diff --git a/doc/integration/github.md b/doc/integration/github.md index a586334b98d..c9c27859c5e 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -21,20 +21,44 @@ To enable the GitHub OmniAuth provider you must register your application with G 1. On your GitLab server, open the configuration file. + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + ```sh - cd /home/git/gitlab + cd /home/git/gitlab - sudo -u git -H editor config/gitlab.yml + sudo -u git -H editor config/gitlab.yml ``` -1. Find the section dealing with OmniAuth. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for more details. +1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "github", + "app_id" => "YOUR APP ID", + "app_secret" => "YOUR APP SECRET", + "url" => "https://github.com/", + "args" => { "scope" => "user:email" } } + } + ] + ``` -1. Under `providers:` uncomment (or add) lines that look like the following: + For installation from source: ``` - - { name: 'github', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET', - args: { scope: 'user:email' } } + - { name: 'github', app_id: 'YOUR APP ID', + app_secret: 'YOUR APP SECRET', + args: { scope: 'user:email' } } ``` 1. Change 'YOUR APP ID' to the client ID from the GitHub application page from step 7. diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index b3b1d897225..b95ef5c0af3 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -12,35 +12,60 @@ To enable the GitLab OmniAuth provider you must register your application with G 1. Provide the required details. - Name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. - - Redirect URI: - + - Redirect URI: + ``` http://gitlab.example.com/import/gitlab/callback http://gitlab.example.com/users/auth/gitlab/callback ``` - The first link is required for the importer and second for the authorization. + The first link is required for the importer and second for the authorization. 1. Select "Submit". 1. You should now see a Application ID and Secret. Keep this page open as you continue configuration. +1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). Keep this page open as you continue configuration. ![GitHub app](github_app.png) + 1. On your GitLab server, open the configuration file. + For omnibus package: + ```sh - cd /home/git/gitlab + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: - sudo -u git -H editor config/gitlab.yml + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml ``` -1. Find the section dealing with OmniAuth. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for more details. +1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "gitlab", + "app_id" => "YOUR APP ID", + "app_secret" => "YOUR APP SECRET", + "args" => { "scope" => "api" } } + } + ] + ``` -1. Under `providers:` uncomment (or add) lines that look like the following: + For installations from source: ``` - - { name: 'gitlab', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET', - args: { scope: 'api' } } + - { name: 'gitlab', app_id: 'YOUR APP ID', + app_secret: 'YOUR APP SECRET', + args: { scope: 'api' } } ``` 1. Change 'YOUR APP ID' to the Application ID from the GitLab application page. diff --git a/doc/integration/google.md b/doc/integration/google.md index 7a78aff8ea4..76beac16c49 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -27,22 +27,45 @@ To enable the Google OAuth2 OmniAuth provider you must register your application - Authorized redirect URI: 'https://gitlab.example.com/users/auth/google_oauth2/callback' 1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration. ![Google app](google_app.png) -1. On your GitLab server, open the configuration file. +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: ```sh - cd /home/git/gitlab + cd /home/git/gitlab - sudo -u git -H editor config/gitlab.yml + sudo -u git -H editor config/gitlab.yml ``` -1. Find the section dealing with OmniAuth. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for more details. +1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "google_oauth2", + "app_id" => "YOUR APP ID", + "app_secret" => "YOUR APP SECRET", + "args" => { "access_type" => "offline", "approval_prompt" => '' } } + } + ] + ``` -1. Under `providers:` uncomment (or add) lines that look like the following: + For installations from source: ``` - - { name: 'google_oauth2', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET', - args: { access_type: 'offline', approval_prompt: '' } } + - { name: 'google_oauth2', app_id: 'YOUR APP ID', + app_secret: 'YOUR APP SECRET', + args: { access_type: 'offline', approval_prompt: '' } } ``` 1. Change 'YOUR APP ID' to the client ID from the GitHub application page from step 7. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 7911cd3e84d..7433de33909 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -1,8 +1,8 @@ # OmniAuth -GitLab leverages OmniAuth to allow users to sign in using Twitter, GitHub, and other popular services. Configuring +GitLab leverages OmniAuth to allow users to sign in using Twitter, GitHub, and other popular services. -OmniAuth does not prevent standard GitLab authentication or LDAP (if configured) from continuing to work. Users can choose to sign in using any of the configured mechanisms. +Configuring OmniAuth does not prevent standard GitLab authentication or LDAP (if configured) from continuing to work. Users can choose to sign in using any of the configured mechanisms. - [Initial OmniAuth Configuration](#initial-omniauth-configuration) - [Supported Providers](#supported-providers) @@ -11,9 +11,37 @@ OmniAuth does not prevent standard GitLab authentication or LDAP (if configured) ## Initial OmniAuth Configuration -Before configuring individual OmniAuth providers there are a few global settings that need to be verified. +Before configuring individual OmniAuth providers there are a few global settings that are in common for all providers that we need to consider. -1. Open the configuration file. +- Omniauth needs to be enabled, see details below for example. +- `allow_single_sign_on` defaults to `false`. If `false` users must be created manually or they will not be able to +sign in via OmniAuth. +- `block_auto_created_users` defaults to `true`. If `true` auto created users will be blocked by default and will +have to be unblocked by an administrator before they are able to sign in. +- **Note:** If you set `allow_single_sign_on` to `true` and `block_auto_created_users` to `false` please be aware +that any user on the Internet will be able to successfully sign in to your GitLab without administrative approval. + +If you want to change these settings: + +* **For omnibus package** + + Open the configuration file: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + and change + + ``` + gitlab_rails['omniauth_enabled'] = true + gitlab_rails['omniauth_allow_single_sign_on'] = false + gitlab_rails['block_auto_created_users'] = true + ``` + +* **For installations from source** + + Open the configuration file: ```sh cd /home/git/gitlab @@ -21,13 +49,13 @@ Before configuring individual OmniAuth providers there are a few global settings sudo -u git -H editor config/gitlab.yml ``` -1. Find the section dealing with OmniAuth. The section will look similar to the following. + and change the following section ``` - ## OmniAuth settings + ## OmniAuth settings omniauth: # Allow login via Twitter, Google, etc. using OmniAuth providers - enabled: false + enabled: true # CAUTION! # This allows users to login without having a user account first (default: false). @@ -35,43 +63,9 @@ Before configuring individual OmniAuth providers there are a few global settings allow_single_sign_on: false # Locks down those users until they have been cleared by the admin (default: true). block_auto_created_users: true - - ## Auth providers - # Uncomment the following lines and fill in the data of the auth provider you want to use - # If your favorite auth provider is not listed you can use others: - # see https://github.com/gitlabhq/gitlab-public-wiki/wiki/Custom-omniauth-provider-configurations - # The 'app_id' and 'app_secret' parameters are always passed as the first two - # arguments, followed by optional 'args' which can be either a hash or an array. - providers: - # - { name: 'google_oauth2', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET', - # args: { access_type: 'offline', approval_prompt: '' } } - # - { name: 'twitter', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET'} - # - { name: 'github', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET', - # args: { scope: 'user:email' } } - # - {"name": 'shibboleth', - # args: { shib_session_id_field: "HTTP_SHIB_SESSION_ID", - # shib_application_id_field: "HTTP_SHIB_APPLICATION_ID", - # uid_field: "HTTP_EPPN", - # name_field: "HTTP_CN", - # info_fields: {"email": "HTTP_MAIL" } } } - ``` -1. Change `enabled` to `true`. - -1. Consider the next two configuration options: `allow_single_sign_on` and `block_auto_created_users`. - - - `allow_single_sign_on` defaults to `false`. If `false` users must be created manually or they will not be able to - sign in via OmniAuth. - - `block_auto_created_users` defaults to `true`. If `true` auto created users will be blocked by default and will - have to be unblocked by an administrator before they are able to sign in. - - **Note:** If you set `allow_single_sign_on` to `true` and `block_auto_created_users` to `false` please be aware - that any user on the Internet will be able to successfully sign in to your GitLab without administrative approval. - -1. Choose one or more of the Supported Providers below to continue configuration. +Now we can choose one or more of the Supported Providers below to continue configuration. ## Supported Providers diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index ea11f1afeab..6258e5f1030 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -2,12 +2,12 @@ This documentation is for enabling shibboleth with gitlab-omnibus package. -In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however I did not found way to easily configure Nginx that is bundled in gitlab-omnibus package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider. +In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however I did not found way to easily configure Nginx that is bundled in gitlab-omnibus package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider. To enable the Shibboleth OmniAuth provider you must: -1. Configure Apache shibboleth module. Installation and configuration of module it self is out of scope of this document. +1. Configure Apache shibboleth module. Installation and configuration of module it self is out of scope of this document. Check https://wiki.shibboleth.net/ for more info. 1. You can find Apache config in gitlab-recipes (https://github.com/gitlabhq/gitlab-recipes/blob/master/web-server/apache/gitlab-ssl.conf) @@ -37,15 +37,15 @@ exclude shibboleth URLs from rewriting, add "RewriteCond %{REQUEST_URI} !/Shibbo # Apache equivalent of Nginx try files RewriteEngine on RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_URI} !/Shibboleth.sso - RewriteCond %{REQUEST_URI} !/shibboleth-sp + RewriteCond %{REQUEST_URI} !/Shibboleth.sso + RewriteCond %{REQUEST_URI} !/shibboleth-sp RewriteRule .* http://127.0.0.1:8080%{REQUEST_URI} [P,QSA] RequestHeader set X_FORWARDED_PROTO 'https' ``` -1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should addjust them to your need and environment. Add any other configuration you need. +1. Edit /etc/gitlab/gitlab.rb configuration file, your shibboleth attributes should be in form of "HTTP_ATTRIBUTE" and you should addjust them to your need and environment. Add any other configuration you need. -File it should look like this: +File should look like this: ``` external_url 'https://gitlab.example.com' gitlab_rails['internal_api_url'] = 'https://gitlab.example.com' @@ -70,7 +70,7 @@ gitlab_rails['omniauth_providers'] = [ ] ``` -1. Save changes and reconfigure gitlab: +1. Save changes and reconfigure gitlab: ``` sudo gitlab-ctl reconfigure ``` diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index b9e501c5ec1..2d517b2fbc9 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -33,20 +33,41 @@ To enable the Twitter OmniAuth provider you must register your application with 1. On your GitLab server, open the configuration file. + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + ```sh - cd /home/git/gitlab + cd /home/git/gitlab - sudo -u git -H editor config/gitlab.yml + sudo -u git -H editor config/gitlab.yml ``` -1. Find the section dealing with OmniAuth. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) -for more details. +1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "twitter", + "app_id" => "YOUR APP ID", + "app_secret" => "YOUR APP SECRET" + } + ] + ``` -1. Under `providers:` uncomment (or add) lines that look like the following: + For installations from source: ``` - - { name: 'twitter', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET' } + - { name: 'twitter', app_id: 'YOUR APP ID', + app_secret: 'YOUR APP SECRET' } ``` 1. Change 'YOUR APP ID' to the API key from Twitter page in step 11. -- GitLab From 252ee4e7e51ae18f30f4b9089be850daaca958ac Mon Sep 17 00:00:00 2001 From: Aaron Stone Date: Mon, 11 Aug 2014 11:06:15 -0700 Subject: [PATCH 0893/1609] Improve login screen when only OmniAuth providers are enabled Avoids an empty Sign in box when signup_enabled? is false, and avoids showing "No authentication methods configured" unless there really are none. OmniAuth signin gets its own file for consistency with signin and signup and LDAP. --- app/views/devise/sessions/new.html.haml | 16 ++++++++++++++-- app/views/devise/shared/_omniauth_box.html.haml | 10 ++++++++++ app/views/devise/shared/_signin_box.html.haml | 16 ---------------- 3 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 app/views/devise/shared/_omniauth_box.html.haml diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index fa2460518fc..89e4e229ac0 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,6 +1,18 @@ %div - = render 'devise/shared/signin_box' + - if signin_enabled? || ldap_enabled? + = render 'devise/shared/signin_box' - - if signup_enabled? + -# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box + - if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable? + .clearfix.prepend-top-20 + = render 'devise/shared/omniauth_box' + + -# Signup only makes sense if you can also sign-in + - if signin_enabled? && signup_enabled? .prepend-top-20 = render 'devise/shared/signup_box' + + -# Show a message if none of the mechanisms above are enabled + - if !signin_enabled? && !ldap_enabled? && !(Gitlab.config.omniauth.enabled && devise_mapping.omniauthable?) + %div + No authentication methods configured. diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml new file mode 100644 index 00000000000..4cd1c303b22 --- /dev/null +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -0,0 +1,10 @@ +%p + %span.light + Sign in with   + - providers = additional_providers + - providers.each do |provider| + %span.light + - if default_providers.include?(provider) + = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider) + - else + = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn" diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 805cf816231..8faa6398a60 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -24,19 +24,3 @@ - elsif signin_enabled? = render 'devise/sessions/new_base' - - else - %div - No authentication methods configured. - -- if Gitlab.config.omniauth.enabled && devise_mapping.omniauthable? - .clearfix.prepend-top-20 - %p - %span.light - Sign in with   - - providers = additional_providers - - providers.each do |provider| - %span.light - - if default_providers.include?(provider) - = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider) - - else - = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn" \ No newline at end of file -- GitLab From 9efb838be2d4a1b28f1378074af1f44442a7114a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 14 Feb 2015 13:31:46 +0100 Subject: [PATCH 0894/1609] Change tweet text. --- app/views/events/event/_created_project.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 0ebbb841cca..551e0160530 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -19,9 +19,10 @@ href: "https://twitter.com/share", | class: "twitter-share-button", | "data-url" => event.project.web_url, | - "data-text" => "I just created a new project in GitLab! GitLab is version control on your server, like GitHub but better.", | + "data-text" => "I just created a new project in GitLab! GitLab is version control on your server.", | "data-size" => "medium", | "data-related" => "gitlab", | + "data-hashtags" => "gitlab", | "data-count" => "none"} Tweet \ No newline at end of file -- GitLab From 67afb5b145dd98ba92f653180d2b7ba470a59e37 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 14 Feb 2015 13:32:05 +0100 Subject: [PATCH 0895/1609] Make sure twitter widgets are loaded when rendered through turbolinks. --- app/views/events/event/_created_project.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 551e0160530..3c7153d235f 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -17,7 +17,6 @@ %a.twitter-share-button{ | href: "https://twitter.com/share", | - class: "twitter-share-button", | "data-url" => event.project.web_url, | "data-text" => "I just created a new project in GitLab! GitLab is version control on your server.", | "data-size" => "medium", | @@ -25,4 +24,4 @@ "data-hashtags" => "gitlab", | "data-count" => "none"} Tweet - \ No newline at end of file + %script{src: "//platform.twitter.com/widgets.js"} \ No newline at end of file -- GitLab From 76aad9b76ed756ca9ba2cbcdb399c815e542b3ae Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 24 Jan 2015 11:02:58 -0700 Subject: [PATCH 0896/1609] Upgrade to Rails 4.1.9 Make the following changes to deal with new behavior in Rails 4.1.2: * Use nested resources to avoid slashes in arguments to path helpers. --- CHANGELOG | 1 + Gemfile.lock | 69 ++--- app/controllers/admin/projects_controller.rb | 7 +- app/controllers/application_controller.rb | 6 +- .../projects/application_controller.rb | 8 +- .../projects/avatars_controller.rb | 2 +- app/controllers/projects/blob_controller.rb | 11 +- .../projects/branches_controller.rb | 8 +- .../projects/compare_controller.rb | 3 +- .../projects/deploy_keys_controller.rb | 9 +- app/controllers/projects/forks_controller.rb | 7 +- app/controllers/projects/hooks_controller.rb | 4 +- .../projects/imports_controller.rb | 10 +- app/controllers/projects/issues_controller.rb | 8 +- app/controllers/projects/labels_controller.rb | 16 +- .../projects/merge_requests_controller.rb | 11 +- .../projects/milestones_controller.rb | 5 +- .../projects/protected_branches_controller.rb | 5 +- app/controllers/projects/refs_controller.rb | 10 +- .../projects/repositories_controller.rb | 2 +- .../projects/services_controller.rb | 7 +- .../projects/snippets_controller.rb | 7 +- app/controllers/projects/tags_controller.rb | 4 +- .../projects/team_members_controller.rb | 13 +- app/controllers/projects/tree_controller.rb | 5 +- app/controllers/projects/wikis_controller.rb | 17 +- app/controllers/projects_controller.rb | 23 +- app/helpers/application_helper.rb | 2 +- app/helpers/blob_helper.rb | 8 +- app/helpers/commits_helper.rb | 50 ++- app/helpers/compare_helper.rb | 9 +- app/helpers/events_helper.rb | 44 ++- app/helpers/issues_helper.rb | 6 +- app/helpers/merge_requests_helper.rb | 6 +- app/helpers/milestones_helper.rb | 2 +- app/helpers/notes_helper.rb | 6 +- app/helpers/projects_helper.rb | 28 +- app/helpers/search_helper.rb | 22 +- app/helpers/snippets_helper.rb | 3 +- app/helpers/submodule_helper.rb | 29 +- app/helpers/tab_helper.rb | 3 +- app/mailers/emails/issues.rb | 8 +- app/mailers/emails/merge_requests.rb | 20 +- app/mailers/emails/notes.rb | 13 +- app/mailers/emails/projects.rb | 12 +- app/models/project.rb | 2 +- .../gitlab_issue_tracker_service.rb | 6 +- app/services/projects/transfer_service.rb | 2 +- app/views/admin/dashboard/index.html.haml | 6 +- app/views/admin/groups/show.html.haml | 2 +- app/views/admin/projects/index.html.haml | 20 +- app/views/admin/projects/show.html.haml | 14 +- app/views/admin/users/show.html.haml | 4 +- app/views/dashboard/_project.html.haml | 4 +- app/views/dashboard/_projects.html.haml | 2 +- app/views/dashboard/projects.html.haml | 8 +- app/views/events/_commit.html.haml | 2 +- app/views/events/_event_last_push.html.haml | 2 +- app/views/events/_event_push.atom.haml | 2 +- app/views/events/event/_common.html.haml | 2 +- app/views/events/event/_push.html.haml | 4 +- app/views/explore/projects/_project.html.haml | 8 +- app/views/groups/_projects.html.haml | 4 +- app/views/groups/milestones/_issue.html.haml | 4 +- .../milestones/_merge_request.html.haml | 4 +- app/views/groups/milestones/show.html.haml | 2 +- app/views/groups/projects.html.haml | 4 +- app/views/layouts/_head.html.haml | 4 +- .../layouts/_init_auto_complete.html.haml | 2 +- app/views/layouts/nav/_admin.html.haml | 2 +- app/views/layouts/nav/_project.html.haml | 22 +- app/views/layouts/notify.html.haml | 2 +- .../_reassigned_issuable_email.text.erb | 2 +- app/views/notify/closed_issue_email.text.haml | 2 +- .../closed_merge_request_email.text.haml | 2 +- .../issue_status_changed_email.text.erb | 2 +- .../merge_request_status_email.text.haml | 2 +- .../merged_merge_request_email.text.haml | 2 +- app/views/notify/new_issue_email.text.erb | 2 +- .../notify/new_merge_request_email.text.erb | 2 +- app/views/notify/note_commit_email.text.erb | 2 +- app/views/notify/note_issue_email.text.erb | 2 +- .../notify/note_merge_request_email.text.erb | 2 +- .../project_access_granted_email.html.haml | 2 +- .../project_access_granted_email.text.erb | 2 +- .../notify/project_was_moved_email.html.haml | 2 +- .../notify/project_was_moved_email.text.erb | 2 +- .../notify/repository_push_email.html.haml | 4 +- .../notify/repository_push_email.text.haml | 4 +- app/views/projects/_dropdown.html.haml | 10 +- app/views/projects/_home_panel.html.haml | 10 +- app/views/projects/_issuable_form.html.haml | 6 +- app/views/projects/_issues_nav.html.haml | 16 +- app/views/projects/_md_preview.html.haml | 2 +- app/views/projects/_settings_nav.html.haml | 12 +- app/views/projects/blame/show.html.haml | 4 +- app/views/projects/blob/_actions.html.haml | 12 +- app/views/projects/blob/_blob.html.haml | 6 +- app/views/projects/blob/_download.html.haml | 2 +- app/views/projects/blob/_remove.html.haml | 2 +- app/views/projects/blob/edit.html.haml | 4 +- app/views/projects/blob/new.html.haml | 4 +- app/views/projects/branches/_branch.html.haml | 6 +- app/views/projects/branches/index.html.haml | 8 +- app/views/projects/branches/new.html.haml | 4 +- .../projects/commit/_commit_box.html.haml | 12 +- app/views/projects/commit/branches.html.haml | 4 +- app/views/projects/commits/_commit.html.haml | 4 +- app/views/projects/commits/_head.html.haml | 8 +- .../projects/commits/_inline_commit.html.haml | 4 +- app/views/projects/commits/show.atom.builder | 10 +- app/views/projects/commits/show.html.haml | 2 +- app/views/projects/compare/_form.html.haml | 2 +- .../deploy_keys/_deploy_key.html.haml | 9 +- .../projects/deploy_keys/_form.html.haml | 4 +- .../projects/deploy_keys/index.html.haml | 4 +- app/views/projects/deploy_keys/show.html.haml | 4 +- app/views/projects/diffs/_file.html.haml | 2 +- app/views/projects/diffs/_image.html.haml | 4 +- app/views/projects/diffs/_warning.html.haml | 8 +- app/views/projects/edit.html.haml | 20 +- app/views/projects/empty.html.haml | 4 +- app/views/projects/forks/error.html.haml | 2 +- app/views/projects/forks/new.html.haml | 2 +- app/views/projects/graphs/_head.html.haml | 4 +- app/views/projects/hooks/index.html.haml | 6 +- app/views/projects/imports/new.html.haml | 2 +- .../projects/issues/_discussion.html.haml | 6 +- app/views/projects/issues/_form.html.haml | 4 +- app/views/projects/issues/_issue.html.haml | 12 +- .../projects/issues/_issue_context.html.haml | 4 +- app/views/projects/issues/_issues.html.haml | 2 +- app/views/projects/issues/index.atom.builder | 6 +- app/views/projects/issues/show.html.haml | 8 +- app/views/projects/issues/update.js.haml | 2 +- app/views/projects/labels/_form.html.haml | 4 +- app/views/projects/labels/_label.html.haml | 6 +- app/views/projects/labels/edit.html.haml | 2 +- app/views/projects/labels/index.html.haml | 4 +- app/views/projects/labels/new.html.haml | 2 +- .../merge_requests/_discussion.html.haml | 6 +- .../projects/merge_requests/_form.html.haml | 4 +- .../projects/merge_requests/_head.html.haml | 2 +- .../merge_requests/_merge_request.html.haml | 4 +- .../merge_requests/_new_compare.html.haml | 12 +- .../merge_requests/_new_submit.html.haml | 10 +- .../projects/merge_requests/_show.html.haml | 18 +- .../merge_requests/show/_context.html.haml | 4 +- .../merge_requests/show/_mr_accept.html.haml | 2 +- .../merge_requests/show/_mr_title.html.haml | 6 +- app/views/projects/milestones/_form.html.haml | 10 +- .../projects/milestones/_issue.html.haml | 6 +- .../milestones/_merge_request.html.haml | 6 +- .../projects/milestones/_milestone.html.haml | 10 +- app/views/projects/milestones/index.html.haml | 2 +- app/views/projects/milestones/show.html.haml | 10 +- app/views/projects/network/show.html.haml | 6 +- app/views/projects/no_repo.html.haml | 6 +- app/views/projects/notes/_edit_form.html.haml | 2 +- app/views/projects/notes/_form.html.haml | 4 +- app/views/projects/notes/_note.html.haml | 4 +- .../projects/notes/_notes_with_form.html.haml | 2 +- .../notes/discussions/_active.html.haml | 2 +- .../notes/discussions/_commit.html.haml | 2 +- .../_branches_list.html.haml | 8 +- .../protected_branches/index.html.haml | 2 +- app/views/projects/refs/logs_tree.js.haml | 4 +- .../repositories/_download_archive.html.haml | 14 +- .../projects/repositories/_feed.html.haml | 4 +- app/views/projects/services/_form.html.haml | 6 +- app/views/projects/services/index.html.haml | 2 +- app/views/projects/show.html.haml | 14 +- app/views/projects/snippets/edit.html.haml | 2 +- app/views/projects/snippets/index.html.haml | 2 +- app/views/projects/snippets/new.html.haml | 2 +- app/views/projects/snippets/show.html.haml | 10 +- app/views/projects/tags/_tag.html.haml | 4 +- app/views/projects/tags/index.html.haml | 2 +- app/views/projects/tags/new.html.haml | 4 +- .../projects/team_members/_form.html.haml | 4 +- .../team_members/_team_member.html.haml | 4 +- .../projects/team_members/import.html.haml | 4 +- .../projects/team_members/index.html.haml | 4 +- app/views/projects/transfer.js.haml | 2 +- app/views/projects/tree/_blob_item.html.haml | 2 +- app/views/projects/tree/_tree.html.haml | 12 +- .../tree/_tree_commit_column.html.haml | 2 +- app/views/projects/tree/_tree_item.html.haml | 2 +- app/views/projects/update.js.haml | 2 +- app/views/projects/wikis/_form.html.haml | 8 +- .../projects/wikis/_main_links.html.haml | 4 +- app/views/projects/wikis/_nav.html.haml | 6 +- app/views/projects/wikis/_new.html.haml | 2 +- app/views/projects/wikis/edit.html.haml | 2 +- app/views/projects/wikis/history.html.haml | 2 +- app/views/projects/wikis/pages.html.haml | 2 +- app/views/projects/wikis/show.html.haml | 2 +- app/views/search/_results.html.haml | 2 +- app/views/search/results/_blob.html.haml | 2 +- app/views/search/results/_issue.html.haml | 2 +- .../search/results/_merge_request.html.haml | 2 +- app/views/search/results/_note.html.haml | 4 +- app/views/search/results/_project.html.haml | 2 +- app/views/search/results/_wiki_blob.html.haml | 2 +- app/views/shared/_issuable_filter.html.haml | 2 +- app/views/shared/_issues.html.haml | 2 +- app/views/shared/_merge_requests.html.haml | 2 +- app/views/shared/_ref_switcher.html.haml | 2 +- app/views/shared/snippets/_form.html.haml | 2 +- config/routes.rb | 292 ++++++++++-------- features/steps/admin/projects.rb | 6 +- features/steps/dashboard/dashboard.rb | 2 +- features/steps/explore/projects.rb | 8 +- features/steps/groups.rb | 4 +- features/steps/project/archived.rb | 2 +- features/steps/project/commits/commits.rb | 6 +- features/steps/project/commits/user_lookup.rb | 4 +- features/steps/project/create.rb | 2 +- features/steps/project/deploy_keys.rb | 2 +- .../steps/project/forked_merge_requests.rb | 8 +- features/steps/project/graph.rb | 4 +- features/steps/project/hooks.rb | 4 +- features/steps/project/issues/issues.rb | 4 +- features/steps/project/issues/labels.rb | 2 +- features/steps/project/merge_requests.rb | 4 +- features/steps/project/network_graph.rb | 2 +- features/steps/project/redirects.rb | 6 +- features/steps/project/services.rb | 2 +- features/steps/project/snippets.rb | 2 +- features/steps/project/source/browse_files.rb | 17 +- .../steps/project/source/markdown_render.rb | 72 ++--- features/steps/project/wiki.rb | 8 +- features/steps/shared/paths.rb | 110 +++---- features/steps/shared/project.rb | 5 +- lib/extracts_path.rb | 3 +- lib/gitlab/markdown.rb | 16 +- lib/gitlab/url_builder.rb | 7 +- spec/controllers/blob_controller_spec.rb | 8 +- spec/controllers/branches_controller_spec.rb | 1 + spec/controllers/commit_controller_spec.rb | 24 +- spec/controllers/commits_controller_spec.rb | 3 +- .../merge_requests_controller_spec.rb | 21 +- spec/controllers/projects_controller_spec.rb | 21 +- spec/controllers/tree_controller_spec.rb | 8 +- spec/features/admin/admin_projects_spec.rb | 6 +- spec/features/admin/security_spec.rb | 2 +- spec/features/atom/issues_spec.rb | 6 +- .../features/gitlab_flavored_markdown_spec.rb | 24 +- spec/features/issues_spec.rb | 42 +-- spec/features/notes_on_merge_requests_spec.rb | 4 +- spec/features/projects_spec.rb | 2 +- .../security/project/internal_access_spec.rb | 34 +- .../security/project/private_access_spec.rb | 30 +- .../security/project/public_access_spec.rb | 34 +- spec/helpers/application_helper_spec.rb | 6 +- spec/helpers/gitlab_markdown_helper_spec.rb | 28 +- spec/helpers/issues_helper_spec.rb | 6 +- spec/helpers/submodule_helper_spec.rb | 10 +- spec/lib/gitlab/url_builder_spec.rb | 2 +- spec/mailers/notify_spec.rb | 22 +- spec/models/project_spec.rb | 2 +- spec/routing/admin_routing_spec.rb | 2 +- spec/routing/project_routing_spec.rb | 172 +++++------ spec/services/git_push_service_spec.rb | 12 +- .../projects/transfer_service_spec.rb | 6 +- 265 files changed, 1374 insertions(+), 1098 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0bf8de6bd17..9e10ea4afb7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,6 +53,7 @@ v 7.8.0 (unreleased) - Show assignees in merge request index page (Kelvin Mutuma) - Link head panel titles to relevant root page. - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). + - Upgrade Rails gem to version 4.1.9. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/Gemfile.lock b/Gemfile.lock index 3283da40f8d..1cd7caa782d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,31 +3,31 @@ GEM specs: RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.1.1) - actionpack (= 4.1.1) - actionview (= 4.1.1) - mail (~> 2.5.4) - actionpack (4.1.1) - actionview (= 4.1.1) - activesupport (= 4.1.1) + actionmailer (4.1.9) + actionpack (= 4.1.9) + actionview (= 4.1.9) + mail (~> 2.5, >= 2.5.4) + actionpack (4.1.9) + actionview (= 4.1.9) + activesupport (= 4.1.9) rack (~> 1.5.2) rack-test (~> 0.6.2) - actionview (4.1.1) - activesupport (= 4.1.1) + actionview (4.1.9) + activesupport (= 4.1.9) builder (~> 3.1) erubis (~> 2.7.0) - activemodel (4.1.1) - activesupport (= 4.1.1) + activemodel (4.1.9) + activesupport (= 4.1.9) builder (~> 3.1) - activerecord (4.1.1) - activemodel (= 4.1.1) - activesupport (= 4.1.1) + activerecord (4.1.9) + activemodel (= 4.1.9) + activesupport (= 4.1.9) arel (~> 5.0.0) activeresource (4.0.0) activemodel (~> 4.0) activesupport (~> 4.0) rails-observers (~> 0.1.1) - activesupport (4.1.1) + activesupport (4.1.9) i18n (~> 0.6, >= 0.6.9) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -303,9 +303,8 @@ GEM rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) lumberjack (1.0.4) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) + mail (2.6.3) + mime-types (>= 1.16, < 3) method_source (0.8.2) mime-types (1.25.1) mini_portile (0.6.1) @@ -372,7 +371,6 @@ GEM cliver (~> 0.3.1) multi_json (~> 1.0) websocket-driver (>= 0.2.0) - polyglot (0.3.4) posix-spawn (0.3.9) powerpack (0.0.9) pry (0.9.12.4) @@ -403,30 +401,30 @@ GEM rack (>= 1.1) rack-protection (1.5.1) rack - rack-test (0.6.2) + rack-test (0.6.3) rack (>= 1.0) - rails (4.1.1) - actionmailer (= 4.1.1) - actionpack (= 4.1.1) - actionview (= 4.1.1) - activemodel (= 4.1.1) - activerecord (= 4.1.1) - activesupport (= 4.1.1) + rails (4.1.9) + actionmailer (= 4.1.9) + actionpack (= 4.1.9) + actionview (= 4.1.9) + activemodel (= 4.1.9) + activerecord (= 4.1.9) + activesupport (= 4.1.9) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.1) + railties (= 4.1.9) sprockets-rails (~> 2.0) rails-observers (0.1.2) activemodel (~> 4.0) rails_autolink (1.1.6) rails (> 3.1) - railties (4.1.1) - actionpack (= 4.1.1) - activesupport (= 4.1.1) + railties (4.1.9) + actionpack (= 4.1.9) + activesupport (= 4.1.9) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.0.0) raindrops (0.13.0) - rake (10.3.2) + rake (10.4.2) raphael-rails (2.1.2) rb-fsevent (0.9.3) rb-inotify (0.9.2) @@ -553,10 +551,10 @@ GEM multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.1.3) + sprockets-rails (2.2.4) actionpack (>= 3.0) activesupport (>= 3.0) - sprockets (~> 2.8) + sprockets (>= 2.8, < 4.0) stamp (0.5.0) state_machine (1.2.0) stringex (2.5.2) @@ -587,9 +585,6 @@ GEM multi_json (~> 1.7) twitter-stream (~> 0.1) tins (0.13.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) turbolinks (2.0.0) coffee-rails twitter-stream (0.1.16) diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 7c2388e81be..2b1fc862b7f 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -25,13 +25,16 @@ class Admin::ProjectsController < Admin::ApplicationController def transfer ::Projects::TransferService.new(@project, current_user, params.dup).execute - redirect_to [:admin, @project.reload] + @project.reload + redirect_to admin_namespace_project_path(@project.namespace, @project) end protected def project - @project = Project.find_with_namespace(params[:id]) + @project = Project.find_with_namespace( + [params[:namespace_id], '/', params[:id]].join('') + ) @project || render_404 end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6553027b430..eb3be08df56 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -93,6 +93,7 @@ class ApplicationController < ActionController::Base def project unless @project + namespace = params[:namespace_id] id = params[:project_id] || params[:id] # Redirect from @@ -104,7 +105,7 @@ class ApplicationController < ActionController::Base redirect_to request.original_url.gsub(/\.git\Z/, '') and return end - @project = Project.find_with_namespace(id) + @project = Project.find_with_namespace("#{namespace}/#{id}") if @project and can?(current_user, :read_project, @project) @project @@ -121,7 +122,8 @@ class ApplicationController < ActionController::Base def repository @repository ||= project.repository - rescue Grit::NoSuchPathError + rescue Grit::NoSuchPathError(e) + log_exception(e) nil end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 7e4580017dd..4719933394f 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -8,7 +8,8 @@ class Projects::ApplicationController < ApplicationController # for non-signed users if !current_user id = params[:project_id] || params[:id] - @project = Project.find_with_namespace(id) + project_with_namespace = "#{params[:namespace_id]}/#{id}" + @project = Project.find_with_namespace(project_with_namespace) return if @project && @project.public? end @@ -26,7 +27,10 @@ class Projects::ApplicationController < ApplicationController def require_branch_head unless @repository.branch_names.include?(@ref) - redirect_to project_tree_path(@project, @ref), notice: "This action is not allowed unless you are on top of a branch" + redirect_to( + namespace_project_tree_path(@project.namespace, @project, @ref), + notice: "This action is not allowed unless you are on top of a branch" + ) end end end diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index a482b90880d..b90a95c3aab 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -24,6 +24,6 @@ class Projects::AvatarsController < Projects::ApplicationController @project.save @project.reset_events_cache - redirect_to edit_project_path(@project) + redirect_to edit_namespace_project_path(@project.namespace, @project) end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index dccb96ba1d1..cc42b1512dc 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -25,7 +25,7 @@ class Projects::BlobController < Projects::ApplicationController if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to project_blob_path(@project, File.join(@ref, file_path)) + redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@ref, file_path)) else flash[:alert] = result[:message] render :new @@ -70,7 +70,8 @@ class Projects::BlobController < Projects::ApplicationController if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to project_tree_path(@project, @ref) + redirect_to namespace_project_tree_path(@project.namespace, @project, + @ref) else flash[:alert] = result[:message] render :show @@ -102,7 +103,7 @@ class Projects::BlobController < Projects::ApplicationController else if tree = @repository.tree(@commit.id, @path) if tree.entries.any? - redirect_to project_tree_path(@project, File.join(@ref, @path)) and return + redirect_to namespace_project_tree_path(@project.namespace, @project, File.join(@ref, @path)) and return end end @@ -128,10 +129,10 @@ class Projects::BlobController < Projects::ApplicationController def after_edit_path @after_edit_path ||= if from_merge_request - diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) + + diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + "#file-path-#{hexdigest(@path)}" else - project_blob_path(@project, @id) + namespace_project_blob_path(@project.namespace, @project, @id) end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index cff1a907dc2..4d002aba972 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -24,7 +24,8 @@ class Projects::BranchesController < Projects::ApplicationController if result[:status] == :success @branch = result[:branch] - redirect_to project_tree_path(@project, @branch.name) + redirect_to namespace_project_tree_path(@project.namespace, @project, + @branch.name) else @error = result[:message] render action: 'new' @@ -36,7 +37,10 @@ class Projects::BranchesController < Projects::ApplicationController @branch_name = params[:id] respond_to do |format| - format.html { redirect_to project_branches_path(@project) } + format.html do + redirect_to namespace_project_branches_path(@project.namespace, + @project) + end format.js end end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index ffb8c2e4af1..0e12bbdc49f 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -25,6 +25,7 @@ class Projects::CompareController < Projects::ApplicationController end def create - redirect_to project_compare_path(@project, params[:from], params[:to]) + redirect_to namespace_project_compare_path(@project.namespace, @project, + params[:from], params[:to]) end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 024b9520d30..b7cc305899c 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -25,7 +25,8 @@ class Projects::DeployKeysController < Projects::ApplicationController @key = DeployKey.new(deploy_key_params) if @key.valid? && @project.deploy_keys << @key - redirect_to project_deploy_keys_path(@project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, + @project) else render "new" end @@ -44,13 +45,15 @@ class Projects::DeployKeysController < Projects::ApplicationController def enable @project.deploy_keys << available_keys.find(params[:id]) - redirect_to project_deploy_keys_path(@project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, + @project) end def disable @project.deploy_keys_projects.where(deploy_key_id: params[:id]).last.destroy - redirect_to project_deploy_keys_path(@project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, + @project) end protected diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index a0481d11582..72f73bedf54 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -9,11 +9,14 @@ class Projects::ForksController < Projects::ApplicationController end def create - namespace = Namespace.find(params[:namespace_id]) + namespace = Namespace.find(params[:namespace_key]) @forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute if @forked_project.saved? && @forked_project.forked? - redirect_to(@forked_project, notice: 'Project was successfully forked.') + redirect_to( + namespace_project_path(@forked_project.namespace, @forked_project), + notice: 'Project was successfully forked.' + ) else @title = 'Fork project' render :error diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 2d6c3111192..ba95bb13e1f 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -16,7 +16,7 @@ class Projects::HooksController < Projects::ApplicationController @hook.save if @hook.valid? - redirect_to project_hooks_path(@project) + redirect_to namespace_project_hooks_path(@project.namespace, @project) else @hooks = @project.hooks.select(&:persisted?) render :index @@ -43,7 +43,7 @@ class Projects::HooksController < Projects::ApplicationController def destroy hook.destroy - redirect_to project_hooks_path(@project) + redirect_to namespace_project_hooks_path(@project.namespace, @project) end private diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index b8350642804..e2f957a640c 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -20,7 +20,7 @@ class Projects::ImportsController < Projects::ApplicationController end end - redirect_to project_import_path(@project) + redirect_to namespace_project_import_path(@project.namespace, @project) end def show @@ -28,7 +28,8 @@ class Projects::ImportsController < Projects::ApplicationController if @project.import_finished? redirect_to(@project) and return else - redirect_to new_project_import_path(@project) and return + redirect_to new_namespace_project_import_path(@project.namespace, + @project) && return end end end @@ -37,13 +38,14 @@ class Projects::ImportsController < Projects::ApplicationController def require_no_repo if @project.repository_exists? - redirect_to(@project) and return + redirect_to(namespace_project_path(@project.namespace, @project)) and return end end def redirect_if_progress if @project.import_in_progress? - redirect_to project_import_path(@project) and return + redirect_to namespace_project_import_path(@project.namespace, @project) && + return end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 42e207cf376..d1bf842ec1a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -60,7 +60,8 @@ class Projects::IssuesController < Projects::ApplicationController respond_to do |format| format.html do if @issue.valid? - redirect_to project_issue_path(@project, @issue) + redirect_to namespace_project_issue_path(@project.namespace, + @project, @issue) else render :new end @@ -78,7 +79,7 @@ class Projects::IssuesController < Projects::ApplicationController format.js format.html do if @issue.valid? - redirect_to [@project, @issue] + redirect_to [@project.namespace, @project, @issue] else render :edit end @@ -128,7 +129,8 @@ class Projects::IssuesController < Projects::ApplicationController issue = @project.issues.find_by(id: params[:id]) if issue - redirect_to project_issue_path(@project, issue) + redirect_to namespace_project_issue_path(@project.namespace, @project, + issue) return else raise ActiveRecord::RecordNotFound.new diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index b61fef3b627..5e31fce4b0e 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -18,7 +18,7 @@ class Projects::LabelsController < Projects::ApplicationController @label = @project.labels.create(label_params) if @label.valid? - redirect_to project_labels_path(@project) + redirect_to namespace_project_labels_path(@project.namespace, @project) else render 'new' end @@ -29,7 +29,7 @@ class Projects::LabelsController < Projects::ApplicationController def update if @label.update_attributes(label_params) - redirect_to project_labels_path(@project) + redirect_to namespace_project_labels_path(@project.namespace, @project) else render 'edit' end @@ -39,11 +39,12 @@ class Projects::LabelsController < Projects::ApplicationController Gitlab::IssuesLabels.generate(@project) if params[:redirect] == 'issues' - redirect_to project_issues_path(@project) + redirect_to namespace_project_issues_path(@project.namespace, @project) elsif params[:redirect] == 'merge_requests' - redirect_to project_merge_requests_path(@project) + redirect_to namespace_project_merge_requests_path(@project.namespace, + @project) else - redirect_to project_labels_path(@project) + redirect_to namespace_project_labels_path(@project.namespace, @project) end end @@ -51,7 +52,10 @@ class Projects::LabelsController < Projects::ApplicationController @label.destroy respond_to do |format| - format.html { redirect_to project_labels_path(@project), notice: 'Label was removed' } + format.html do + redirect_to(namespace_project_labels_path(@project.namespace, @project), + notice: 'Label was removed') + end format.js end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 01be318ede2..98e4775e409 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -78,7 +78,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute if @merge_request.valid? - redirect_to project_merge_request_path(@merge_request.target_project, @merge_request), notice: 'Merge request was successfully created.' + redirect_to( + namespace_project_merge_request_path(@merge_request.target_project.namespace, + @merge_request.target_project, + @merge_request), + notice: 'Merge request was successfully created.' + ) else @source_project = @merge_request.source_project @target_project = @merge_request.target_project @@ -93,7 +98,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.js format.html do - redirect_to [@merge_request.target_project, @merge_request], notice: 'Merge request was successfully updated.' + redirect_to([@merge_request.target_project.namespace.becomes(Namespace), + @merge_request.target_project, @merge_request], + notice: 'Merge request was successfully updated.') end end else diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 95801f8b8fb..97eaabb15c3 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -40,7 +40,8 @@ class Projects::MilestonesController < Projects::ApplicationController @milestone = Milestones::CreateService.new(project, current_user, milestone_params).execute if @milestone.save - redirect_to project_milestone_path(@project, @milestone) + redirect_to namespace_project_milestone_path(@project.namespace, + @project, @milestone) else render "new" end @@ -67,7 +68,7 @@ class Projects::MilestonesController < Projects::ApplicationController @milestone.destroy respond_to do |format| - format.html { redirect_to project_milestones_path } + format.html { redirect_to namespace_project_milestones_path } format.js { render nothing: true } end end diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index f45df38b87c..ac36ac6fcd3 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -12,7 +12,8 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController def create @project.protected_branches.create(protected_branch_params) - redirect_to project_protected_branches_path(@project) + redirect_to namespace_project_protected_branches_path(@project.namespace, + @project) end def update @@ -37,7 +38,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController @project.protected_branches.find(params[:id]).destroy respond_to do |format| - format.html { redirect_to project_protected_branches_path } + format.html { redirect_to namespace_project_protected_branches_path } format.js { render nothing: true } end end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index b80472f8eb4..ec41cafda4d 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -9,13 +9,15 @@ class Projects::RefsController < Projects::ApplicationController respond_to do |format| format.html do new_path = if params[:destination] == "tree" - project_tree_path(@project, (@id)) + namespace_project_tree_path(@project.namespace, @project, + (@id)) elsif params[:destination] == "blob" - project_blob_path(@project, (@id)) + namespace_project_blob_path(@project.namespace, @project, + (@id)) elsif params[:destination] == "graph" - project_network_path(@project, @id, @options) + namespace_project_network_path(@project.namespace, @project, @id, @options) else - project_commits_path(@project, @id) + namespace_project_commits_path(@project.namespace, @project, @id) end redirect_to new_path diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 3a90c1c806d..8a997370dd7 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -7,7 +7,7 @@ class Projects::RepositoriesController < Projects::ApplicationController def create @project.create_repository - redirect_to @project + redirect_to namespace_project_path(@project.namespace, @project) end def archive diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 2b3e70f7bdb..5c29a6550f5 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -17,8 +17,11 @@ class Projects::ServicesController < Projects::ApplicationController def update if @service.update_attributes(service_params) - redirect_to edit_project_service_path(@project, @service.to_param), - notice: 'Successfully updated.' + redirect_to( + edit_namespace_project_service_path(@project.namespace, @project, + @service.to_param, notice: + 'Successfully updated.') + ) else render 'edit' end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 25c887deafa..6c250e4ffed 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -32,7 +32,8 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet.author = current_user if @snippet.save - redirect_to project_snippet_path(@project, @snippet) + redirect_to namespace_project_snippet_path(@project.namespace, @project, + @snippet) else respond_with(@snippet) end @@ -43,7 +44,7 @@ class Projects::SnippetsController < Projects::ApplicationController def update if @snippet.update_attributes(snippet_params) - redirect_to project_snippet_path(@project, @snippet) + redirect_to namespace_project_snippet_path(@project.namespace, @project, @snippet) else respond_with(@snippet) end @@ -60,7 +61,7 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet.destroy - redirect_to project_snippets_path(@project) + redirect_to namespace_project_snippets_path(@project.namespace, @project) end def raw diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 64b820160d3..dafbb4d51ea 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -16,7 +16,7 @@ class Projects::TagsController < Projects::ApplicationController if result[:status] == :success @tag = result[:tag] - redirect_to project_tags_path(@project) + redirect_to namespace_project_tags_path(@project.namespace, @project) else @error = result[:message] render action: 'new' @@ -31,7 +31,7 @@ class Projects::TagsController < Projects::ApplicationController end respond_to do |format| - format.html { redirect_to project_tags_path } + format.html { redirect_to namespace_project_tags_path } format.js end end diff --git a/app/controllers/projects/team_members_controller.rb b/app/controllers/projects/team_members_controller.rb index 0791e6080fb..71b0ab7ee82 100644 --- a/app/controllers/projects/team_members_controller.rb +++ b/app/controllers/projects/team_members_controller.rb @@ -21,7 +21,8 @@ class Projects::TeamMembersController < Projects::ApplicationController if params[:redirect_to] redirect_to params[:redirect_to] else - redirect_to project_team_index_path(@project) + redirect_to namespace_project_team_index_path(@project.namespace, + @project) end end @@ -32,7 +33,7 @@ class Projects::TeamMembersController < Projects::ApplicationController unless @user_project_relation.valid? flash[:alert] = "User should have at least one role" end - redirect_to project_team_index_path(@project) + redirect_to namespace_project_team_index_path(@project.namespace, @project) end def destroy @@ -40,7 +41,10 @@ class Projects::TeamMembersController < Projects::ApplicationController @user_project_relation.destroy respond_to do |format| - format.html { redirect_to project_team_index_path(@project) } + format.html do + redirect_to namespace_project_team_index_path(@project.namespace, + @project) + end format.js { render nothing: true } end end @@ -59,7 +63,8 @@ class Projects::TeamMembersController < Projects::ApplicationController status = @project.team.import(giver) notice = status ? "Successfully imported" : "Import failed" - redirect_to project_team_index_path(project), notice: notice + redirect_to(namespace_project_team_index_path(project.namespace, project), + notice: notice) end protected diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 5b52640a4e1..c7112a3cc10 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -9,7 +9,10 @@ class Projects::TreeController < Projects::ApplicationController def show if tree.entries.empty? if @repository.blob_at(@commit.id, @path) - redirect_to project_blob_path(@project, File.join(@ref, @path)) and return + redirect_to( + namespace_project_blob_path(@project.namespace, @project, + File.join(@ref, @path)) + ) and return else return not_found! end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 0145207bf6f..69824dca944 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -45,7 +45,7 @@ class Projects::WikisController < Projects::ApplicationController return render('empty') unless can?(current_user, :write_wiki, @project) if @page.update(content, format, message) - redirect_to [@project, @page], notice: 'Wiki was successfully updated.' + redirect_to [@project.namespace.becomes(Namespace), @project, @page], notice: 'Wiki was successfully updated.' else render 'edit' end @@ -55,7 +55,10 @@ class Projects::WikisController < Projects::ApplicationController @page = WikiPage.new(@project_wiki) if @page.create(wiki_params) - redirect_to project_wiki_path(@project, @page), notice: 'Wiki was successfully updated.' + redirect_to( + namespace_project_wiki_path(@project.namespace, @project, @page), + notice: 'Wiki was successfully updated.' + ) else render action: "edit" end @@ -65,7 +68,10 @@ class Projects::WikisController < Projects::ApplicationController @page = @project_wiki.find_page(params[:id]) unless @page - redirect_to(project_wiki_path(@project, :home), notice: "Page not found") + redirect_to( + namespace_project_wiki_path(@project.namespace, @project, :home), + notice: "Page not found" + ) end end @@ -73,7 +79,10 @@ class Projects::WikisController < Projects::ApplicationController @page = @project_wiki.find_page(params[:id]) @page.delete if @page - redirect_to project_wiki_path(@project, :home), notice: "Page was successfully deleted" + redirect_to( + namespace_project_wiki_path(@project.namespace, @project, :home), + notice: "Page was successfully deleted" + ) end def git_access diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 462ab3d4749..cf039d5f132 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -21,7 +21,10 @@ class ProjectsController < ApplicationController @project = ::Projects::CreateService.new(current_user, project_params).execute if @project.saved? - redirect_to project_path(@project), notice: 'Project was successfully created.' + redirect_to( + namespace_project_path(@project.namespace, @project), + notice: 'Project was successfully created.' + ) else render 'new' end @@ -33,7 +36,12 @@ class ProjectsController < ApplicationController respond_to do |format| if status flash[:notice] = 'Project was successfully updated.' - format.html { redirect_to edit_project_path(@project), notice: 'Project was successfully updated.' } + format.html do + redirect_to( + edit_namespace_project_path(@project.namespace, @project), + notice: 'Project was successfully updated.' + ) + end format.js else format.html { render 'edit', layout: 'project_settings' } @@ -43,7 +51,8 @@ class ProjectsController < ApplicationController end def transfer - ::Projects::TransferService.new(project, current_user, project_params).execute + transfer_params = params.permit(:new_namespace_id) + ::Projects::TransferService.new(project, current_user, transfer_params).execute if @project.errors[:namespace_id].present? flash[:alert] = @project.errors[:namespace_id].first end @@ -51,7 +60,7 @@ class ProjectsController < ApplicationController def show if @project.import_in_progress? - redirect_to project_import_path(@project) + redirect_to namespace_project_import_path(@project.namespace, @project) return end @@ -90,7 +99,7 @@ class ProjectsController < ApplicationController flash[:alert] = 'Project deleted.' if request.referer.include?('/admin') - redirect_to admin_projects_path + redirect_to admin_namespace_projects_path else redirect_to projects_dashboard_path end @@ -121,7 +130,7 @@ class ProjectsController < ApplicationController @project.archive! respond_to do |format| - format.html { redirect_to @project } + format.html { redirect_to namespace_project_path(@project.namespace, @project) } end end @@ -130,7 +139,7 @@ class ProjectsController < ApplicationController @project.unarchive! respond_to do |format| - format.html { redirect_to @project } + format.html { redirect_to namespace_project_path(@project.namespace, @project) } end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e45f4650309..c3c77d9880f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -55,7 +55,7 @@ module ApplicationHelper if project.avatar.present? image_tag project.avatar.url, options elsif project.avatar_in_git - image_tag project_avatar_path(project), options + image_tag namespace_project_avatar_path(project.namespace, project), options else # generated icon project_identicon(project, options) end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index e75eebd2da9..f5f27223d5b 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -36,8 +36,12 @@ module BlobHelper link_opts[:from_merge_request_id] = from_mr if from_mr cls = 'btn btn-small' if allowed_tree_edit?(project, ref) - link_to text, project_edit_blob_path(project, tree_join(ref, path), - link_opts), class: cls + link_to(text, + namespace_project_edit_blob_path(project.namespace, project, + tree_join(ref, path), + link_opts), + class: cls + ) else content_tag :span, text, class: cls + ' disabled' end + after.html_safe diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index b4ba14160ed..5aae697e2f0 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -37,7 +37,10 @@ module CommitsHelper # Add the root project link and the arrow icon crumbs = content_tag(:li) do - link_to(@project.path, project_commits_path(@project, @ref)) + link_to( + @project.path, + namespace_project_commits_path(@project.namespace, @project, @ref) + ) end if @path @@ -46,7 +49,14 @@ module CommitsHelper parts.each_with_index do |part, i| crumbs << content_tag(:li) do # The text is just the individual part, but the link needs all the parts before it - link_to part, project_commits_path(@project, tree_join(@ref, parts[0..i].join('/'))) + link_to( + part, + namespace_project_commits_path( + @project.namespace, + @project, + tree_join(@ref, parts[0..i].join('/')) + ) + ) end end end @@ -63,7 +73,9 @@ module CommitsHelper # Returns the sorted alphabetically links to branches, separated by a comma def commit_branches_links(project, branches) branches.sort.map do |branch| - link_to(project_tree_path(project, branch)) do + link_to( + namespace_project_tree_path(project.namespace, project, branch) + ) do content_tag :span, class: 'label label-gray' do icon('code-fork') + ' ' + branch end @@ -75,7 +87,10 @@ module CommitsHelper def commit_tags_links(project, tags) sorted = VersionSorter.rsort(tags) sorted.map do |tag| - link_to(project_commits_path(project, project.repository.find_tag(tag).name)) do + link_to( + namespace_project_commits_path(project.namespace, project, + project.repository.find_tag(tag).name) + ) do content_tag :span, class: 'label label-gray' do icon('tag') + ' ' + tag end @@ -86,12 +101,26 @@ module CommitsHelper def link_to_browse_code(project, commit) if current_controller?(:projects, :commits) if @repo.blob_at(commit.id, @path) - return link_to "Browse File »", project_blob_path(project, tree_join(commit.id, @path)), class: "pull-right" + return link_to( + "Browse File »", + namespace_project_blob_path(project.namespace, project, + tree_join(commit.id, @path)), + class: "pull-right" + ) elsif @path.present? - return link_to "Browse Dir »", project_tree_path(project, tree_join(commit.id, @path)), class: "pull-right" + return link_to( + "Browse Dir »", + namespace_project_tree_path(project.namespace, project, + tree_join(commit.id, @path)), + class: "pull-right" + ) end end - link_to "Browse Code »", project_tree_path(project, commit), class: "pull-right" + link_to( + "Browse Code »", + namespace_project_tree_path(project.namespace, project, commit), + class: "pull-right" + ) end protected @@ -133,8 +162,11 @@ module CommitsHelper end def view_file_btn(commit_sha, diff, project) - link_to project_blob_path(project, tree_join(commit_sha, diff.new_path)), - class: 'btn btn-small view-file js-view-file' do + link_to( + namespace_project_blob_path(project.namespace, project, + tree_join(commit_sha, diff.new_path)), + class: 'btn btn-small view-file js-view-file' + ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') end diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index dd2e713a54e..01847c6b807 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -10,6 +10,13 @@ module CompareHelper end def compare_mr_path - new_project_merge_request_path(@project, merge_request: { source_branch: params[:to], target_branch: params[:from] }) + new_namespace_project_merge_request_path( + @project.namespace, + @project, + merge_request: { + source_branch: params[:to], + target_branch: params[:from] + } + ) end end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index d05f6df5f9f..6e7aa521302 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -61,17 +61,23 @@ module EventsHelper def event_feed_url(event) if event.issue? - project_issue_url(event.project, event.issue) + namespace_project_issue_url(event.project.namespace, event.project, + event.issue) elsif event.merge_request? - project_merge_request_url(event.project, event.merge_request) + namespace_project_merge_request_url(event.project.namespace, + event.project, event.merge_request) elsif event.note? && event.note_commit? - project_commit_url(event.project, event.note_target) + namespace_project_commit_url(event.project.namespace, event.project, + event.note_target) elsif event.note? if event.note_target if event.note_commit? - project_commit_path(event.project, event.note_commit_id, anchor: dom_id(event.target)) + namespace_project_commit_path(event.project.namespace, event.project, + event.note_commit_id, + anchor: dom_id(event.target)) elsif event.note_project_snippet? - project_snippet_path(event.project, event.note_target) + namespace_project_snippet_path(event.project.namespace, + event.project, event.note_target) else event_note_target_path(event) end @@ -79,12 +85,16 @@ module EventsHelper elsif event.push? if event.push_with_commits? if event.commits_count > 1 - project_compare_url(event.project, from: event.commit_from, to: event.commit_to) + namespace_project_compare_url(event.project.namespace, event.project, + from: event.commit_from, to: + event.commit_to) else - project_commit_url(event.project, id: event.commit_to) + namespace_project_commit_url(event.project.namespace, event.project, + id: event.commit_to) end else - project_commits_url(event.project, event.ref_name) + namespace_project_commits_url(event.project.namespace, event.project, + event.ref_name) end end end @@ -105,20 +115,30 @@ module EventsHelper def event_note_target_path(event) if event.note? && event.note_commit? - project_commit_path(event.project, event.note_target) + namespace_project_commit_path(event.project.namespace, event.project, + event.note_target) else - polymorphic_path([event.project, event.note_target], anchor: dom_id(event.target)) + polymorphic_path([event.project.namespace.becomes(Namespace), + event.project, event.note_target], + anchor: dom_id(event.target)) end end def event_note_title_html(event) if event.note_target if event.note_commit? - link_to project_commit_path(event.project, event.note_commit_id, anchor: dom_id(event.target)), class: "commit_short_id" do + link_to( + namespace_project_commit_path(event.project.namespace, event.project, + event.note_commit_id, + anchor: dom_id(event.target)), + class: "commit_short_id" + ) do "#{event.note_target_type} #{event.note_short_commit_id}" end elsif event.note_project_snippet? - link_to(project_snippet_path(event.project, event.note_target)) do + link_to(namespace_project_snippet_path(event.project.namespace, + event.project, + event.note_target)) do "#{event.note_target_type} ##{truncate event.note_target_id}" end else diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index e1c1078344e..15c5dcb6a25 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -93,8 +93,10 @@ module IssuesHelper def issue_to_atom(xml, issue) xml.entry do - xml.id project_issue_url(issue.project, issue) - xml.link href: project_issue_url(issue.project, issue) + xml.id namespace_project_issue_url(issue.project.namespace, + issue.project, issue) + xml.link href: namespace_project_issue_url(issue.project.namespace, + issue.project, issue) xml.title truncate(issue.title, length: 80) xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email) diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 4c640d4fc5f..3b1589da57f 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,14 +1,16 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) target_project = event.project.forked_from_project || event.project - new_project_merge_request_path( + new_namespace_project_merge_request_path( + event.project.namespace, event.project, new_mr_from_push_event(event, target_project) ) end def new_mr_path_for_fork_from_push_event(event) - new_project_merge_request_path( + new_namespace_project_merge_request_path( + event.project.namespace, event.project, new_mr_from_push_event(event, event.project.forked_from_project) ) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 6847123d2d4..47fa147dccf 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -1,7 +1,7 @@ module MilestonesHelper def milestones_filter_path(opts = {}) if @project - project_milestones_path(@project, opts) + namespace_project_milestones_path(@project.namespace, @project, opts) elsif @group group_milestones_path(@group, opts) end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 8edcb8e6a80..92ecb2abe4d 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -11,7 +11,11 @@ module NotesHelper def link_to_commit_diff_line_note(note) if note.for_commit_diff_line? - link_to "#{note.diff_file_name}:L#{note.diff_new_line}", project_commit_path(@project, note.noteable, anchor: note.line_code) + link_to( + "#{note.diff_file_name}:L#{note.diff_new_line}", + namespace_project_commit_path(@project.namespace, @project, + note.noteable, anchor: note.line_code) + ) end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 36463892ebf..900afde4d9b 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -4,7 +4,7 @@ module ProjectsHelper end def link_to_project(project) - link_to project do + link_to [project.namespace.becomes(Namespace), project] do title = content_tag(:span, project.name, class: 'project-name') if project.namespace @@ -42,12 +42,20 @@ module ProjectsHelper def project_title(project) if project.group content_tag :span do - link_to(simple_sanitize(project.group.name), group_path(project.group)) + ' / ' + link_to(simple_sanitize(project.name), project_path(project)) + link_to( + simple_sanitize(project.group.name), group_path(project.group) + ) + ' / ' + + link_to(simple_sanitize(project.name), + namespace_project_path(project.namespace, project)) end else owner = project.namespace.owner content_tag :span do - link_to(simple_sanitize(owner.name), user_path(owner)) + ' / ' + link_to(simple_sanitize(project.name), project_path(project)) + link_to( + simple_sanitize(owner.name), user_path(owner) + ) + ' / ' + + link_to(simple_sanitize(project.name), + namespace_project_path(project.namespace, project)) end end end @@ -100,7 +108,10 @@ module ProjectsHelper content_tag 'span', class: starred ? 'turn-on' : 'turn-off' do - link_to toggle_star_project_path(@project), link_opts do + link_to( + toggle_star_namespace_project_path(@project.namespace, @project), + link_opts + ) do toggle_html + ' ' + count_html end end @@ -222,7 +233,12 @@ module ProjectsHelper def contribution_guide_url(project) if project && project.repository.contribution_guide - project_blob_path(project, tree_join(project.default_branch, project.repository.contribution_guide.name)) + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + project.repository.contribution_guide.name) + ) end end @@ -236,7 +252,7 @@ module ProjectsHelper def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } - project_wiki_path(proj, page, url_params) + namespace_project_wiki_path(proj.namespace, proj, page, url_params) end def project_status_css_class(status) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 65b9408cfa1..cb829037697 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -52,16 +52,16 @@ module SearchHelper ref = @ref || @project.repository.root_ref [ - { label: "#{prefix} - Files", url: project_tree_path(@project, ref) }, - { label: "#{prefix} - Commits", url: project_commits_path(@project, ref) }, - { label: "#{prefix} - Network", url: project_network_path(@project, ref) }, - { label: "#{prefix} - Graph", url: project_graph_path(@project, ref) }, - { label: "#{prefix} - Issues", url: project_issues_path(@project) }, - { label: "#{prefix} - Merge Requests", url: project_merge_requests_path(@project) }, - { label: "#{prefix} - Milestones", url: project_milestones_path(@project) }, - { label: "#{prefix} - Snippets", url: project_snippets_path(@project) }, - { label: "#{prefix} - Team", url: project_team_index_path(@project) }, - { label: "#{prefix} - Wiki", url: project_wikis_path(@project) }, + { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { label: "#{prefix} - Team", url: namespace_project_team_index_path(@project.namespace, @project) }, + { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] @@ -84,7 +84,7 @@ module SearchHelper sorted_by_stars.non_archived.limit(limit).map do |p| { label: "project: #{search_result_sanitize(p.name_with_namespace)}", - url: project_path(p) + url: namespace_project_path(p.namespace, p) } end end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index b0abc2cae33..906cb12cd48 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -11,7 +11,8 @@ module SnippetsHelper def reliable_snippet_path(snippet) if snippet.project_id? - project_snippet_path(snippet.project, snippet) + namespace_project_snippet_path(snippet.project.namespace, + snippet.project, snippet) else snippet_path(snippet) end diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 841e7fd17f6..525266fb3b5 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -5,19 +5,22 @@ module SubmoduleHelper def submodule_links(submodule_item, ref = nil) url = @repository.submodule_url_for(ref, submodule_item.path) - return url, nil unless url =~ /([^\/:]+\/[^\/]+\.git)\Z/ + return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/ - project = $1 + namespace = $1 + project = $2 project.chomp!('.git') - if self_url?(url, project) - return project_path(project), project_tree_path(project, submodule_item.id) + if self_url?(url, namespace, project) + return namespace_project_path(namespace, project), + namespace_project_tree_path(namespace, project, + submodule_item.id) elsif relative_self_url?(url) relative_self_links(url, submodule_item.id) elsif github_dot_com_url?(url) - standard_links('github.com', project, submodule_item.id) + standard_links('github.com', namespace, project, submodule_item.id) elsif gitlab_dot_com_url?(url) - standard_links('gitlab.com', project, submodule_item.id) + standard_links('gitlab.com', namespace, project, submodule_item.id) else return url, nil end @@ -33,9 +36,10 @@ module SubmoduleHelper url =~ /gitlab\.com[\/:][^\/]+\/[^\/]+\Z/ end - def self_url?(url, project) - return true if url == [ Gitlab.config.gitlab.url, '/', project, '.git' ].join('') - url == gitlab_shell.url_to_repo(project) + def self_url?(url, namespace, project) + return true if url == [ Gitlab.config.gitlab.url, '/', namespace, '/', + project, '.git' ].join('') + url == gitlab_shell.url_to_repo([namespace, '/', project].join('')) end def relative_self_url?(url) @@ -43,8 +47,8 @@ module SubmoduleHelper url =~ /^((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\Z/ || url =~ /^((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\Z/ end - def standard_links(host, project, commit) - base = [ 'https://', host, '/', project ].join('') + def standard_links(host, namespace, project, commit) + base = [ 'https://', host, '/', namespace, '/', project ].join('') return base, [ base, '/tree/', commit ].join('') end @@ -54,6 +58,7 @@ module SubmoduleHelper else base = [ @project.group.path, '/', url[/([^\/]*)\.git/, 1] ].join('') end - return project_path(base), project_tree_path(base, commit) + return namespace_project_path(base.namespace, base), + namespace_project_tree_path(base.namespace, base, commit) end end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 2142db29925..7a401a274d3 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -97,7 +97,8 @@ module TabHelper def branches_tab_class if current_controller?(:protected_branches) || current_controller?(:branches) || - current_page?(project_repository_path(@project)) + current_page?(namespace_project_repository_path(@project.namespace, + @project)) 'active' end end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index e5346235963..687bac3aa31 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -3,7 +3,7 @@ module Emails def new_issue_email(recipient_id, issue_id) @issue = Issue.find(issue_id) @project = @issue.project - @target_url = project_issue_url(@project, @issue) + @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_new_thread(@issue, from: sender(@issue.author_id), to: recipient(recipient_id), @@ -14,7 +14,7 @@ module Emails @issue = Issue.find(issue_id) @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @project = @issue.project - @target_url = project_issue_url(@project, @issue) + @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_answer_thread(@issue, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -25,7 +25,7 @@ module Emails @issue = Issue.find issue_id @project = @issue.project @updated_by = User.find updated_by_user_id - @target_url = project_issue_url(@project, @issue) + @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_answer_thread(@issue, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -37,7 +37,7 @@ module Emails @issue_status = status @project = @issue.project @updated_by = User.find updated_by_user_id - @target_url = project_issue_url(@project, @issue) + @target_url = namespace_project_issue_url(@project.namespace, @project, @issue) mail_answer_thread(@issue, from: sender(updated_by_user_id), to: recipient(recipient_id), diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 7f6c855c301..512a8f7ea6b 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -3,7 +3,9 @@ module Emails def new_merge_request_email(recipient_id, merge_request_id) @merge_request = MergeRequest.find(merge_request_id) @project = @merge_request.project - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) mail_new_thread(@merge_request, from: sender(@merge_request.author_id), to: recipient(recipient_id), @@ -14,7 +16,9 @@ module Emails @merge_request = MergeRequest.find(merge_request_id) @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @project = @merge_request.project - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -25,7 +29,9 @@ module Emails @merge_request = MergeRequest.find(merge_request_id) @updated_by = User.find updated_by_user_id @project = @merge_request.project - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -35,7 +41,9 @@ module Emails def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) @merge_request = MergeRequest.find(merge_request_id) @project = @merge_request.project - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), @@ -47,7 +55,9 @@ module Emails @mr_status = status @project = @merge_request.project @updated_by = User.find updated_by_user_id - @target_url = project_merge_request_url(@project, @merge_request) + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request) set_reference("merge_request_#{merge_request_id}") mail_answer_thread(@merge_request, from: sender(updated_by_user_id), diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index ef9af726a6c..ff251209e01 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -4,7 +4,9 @@ module Emails @note = Note.find(note_id) @commit = @note.noteable @project = @note.project - @target_url = project_commit_url(@project, @commit, anchor: "note_#{@note.id}") + @target_url = namespace_project_commit_url(@project.namespace, @project, + @commit, anchor: + "note_#{@note.id}") mail_answer_thread(@commit, from: sender(@note.author_id), to: recipient(recipient_id), @@ -15,7 +17,9 @@ module Emails @note = Note.find(note_id) @issue = @note.noteable @project = @note.project - @target_url = project_issue_url(@project, @issue, anchor: "note_#{@note.id}") + @target_url = namespace_project_issue_url(@project.namespace, @project, + @issue, anchor: + "note_#{@note.id}") mail_answer_thread(@issue, from: sender(@note.author_id), to: recipient(recipient_id), @@ -26,7 +30,10 @@ module Emails @note = Note.find(note_id) @merge_request = @note.noteable @project = @note.project - @target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{@note.id}") + @target_url = namespace_project_merge_request_url(@project.namespace, + @project, + @merge_request, anchor: + "note_#{@note.id}") mail_answer_thread(@merge_request, from: sender(@note.author_id), to: recipient(recipient_id), diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index dc2ebc969c1..4bc40b35f2d 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -3,7 +3,7 @@ module Emails def project_access_granted_email(user_project_id) @project_member = ProjectMember.find user_project_id @project = @project_member.project - @target_url = project_url(@project) + @target_url = namespace_project_url(@project.namespace, @project) mail(to: @project_member.user.email, subject: subject("Access to project was granted")) end @@ -11,7 +11,7 @@ module Emails def project_was_moved_email(project_id, user_id) @user = User.find user_id @project = Project.find project_id - @target_url = project_url(@project) + @target_url = namespace_project_url(@project.namespace, @project) mail(to: @user.notification_email, subject: subject("Project was moved")) end @@ -24,10 +24,14 @@ module Emails @diffs = compare.diffs @branch = branch if @commits.length > 1 - @target_url = project_compare_url(@project, from: @commits.first, to: @commits.last) + @target_url = namespace_project_compare_url(@project.namespace, + @project, + from: @commits.first, + to: @commits.last) @subject = "#{@commits.length} new commits pushed to repository" else - @target_url = project_commit_url(@project, @commits.first) + @target_url = namespace_project_commit_url(@project.namespace, + @project, @commits.first) @subject = @commits.first.title end diff --git a/app/models/project.rb b/app/models/project.rb index 56e1aa29040..91ab788083d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -285,7 +285,7 @@ class Project < ActiveRecord::Base end def to_param - namespace.path + '/' + path + path end def web_url diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index b1eab24df19..782cf42ce55 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -27,14 +27,14 @@ class GitlabIssueTrackerService < IssueTrackerService end def project_url - project_issues_path(project) + namespace_project_issues_path(project.namespace, project) end def new_issue_url - new_project_issue_path project_id: project + new_namespace_project_issue_path namespace_id: project.namespace, project_id: project end def issue_url(iid) - "#{Gitlab.config.gitlab.url}#{project_issue_path(project_id: project, id: iid)}" + "#{Gitlab.config.gitlab.url}#{namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid)}" end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index e39fe882cb1..3372cfc11d0 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -12,7 +12,7 @@ module Projects class TransferError < StandardError; end def execute - namespace_id = params[:namespace_id] + namespace_id = params[:new_namespace_id] namespace = Namespace.find_by(id: namespace_id) if allowed_transfer?(current_user, project, namespace) diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 32e0e4a6848..931b0c5c107 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -85,10 +85,10 @@ .light-well %h4 Projects .data - = link_to admin_projects_path do + = link_to admin_namespaces_projects_path do %h1= Project.count %hr - = link_to 'New Project', new_project_path, class: "btn btn-new" + = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 .light-well %h4 Users @@ -112,7 +112,7 @@ %hr - @projects.each do |project| %p - = link_to project.name_with_namespace, [:admin, project], class: 'str-truncated' + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated' %span.light.pull-right #{time_ago_with_tooltip(project.created_at)} diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index d356aff6365..bb7f1972925 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -41,7 +41,7 @@ - @projects.each do |project| %li %strong - = link_to project.name_with_namespace, [:admin, project] + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] %span.label.label-gray = repository_size(project) %span.pull-right.light diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 36a4a2fb4af..dffb4f0d82d 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,7 +1,7 @@ .row .col-md-3 .admin-filter - = form_tag admin_projects_path, method: :get, class: '' do + = form_tag admin_namespaces_projects_path, method: :get, class: '' do .form-group = label_tag :name, 'Name:' = text_field_tag :name, params[:name], class: "form-control" @@ -36,7 +36,7 @@ %hr = hidden_field_tag :sort, params[:sort] = button_tag "Search", class: "btn submit btn-primary" - = link_to "Reset", admin_projects_path, class: "btn btn-cancel" + = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" .col-md-9 .panel.panel-default @@ -53,15 +53,15 @@ %b.caret %ul.dropdown-menu %li - = link_to admin_projects_path(sort: sort_value_recently_created) do + = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do = sort_title_recently_created - = link_to admin_projects_path(sort: sort_value_oldest_created) do + = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do = sort_title_oldest_created - = link_to admin_projects_path(sort: sort_value_recently_updated) do + = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do = sort_title_recently_updated - = link_to admin_projects_path(sort: sort_value_oldest_updated) do + = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated - = link_to admin_projects_path(sort: sort_value_largest_repo) do + = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do = sort_title_largest_repo = link_to 'New Project', new_project_path, class: "btn btn-new" %ul.well-list @@ -70,12 +70,12 @@ .list-item-name %span{ class: visibility_level_color(project.visibility_level) } = visibility_level_icon(project.visibility_level) - = link_to project.name_with_namespace, [:admin, project] + = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] .pull-right %span.label.label-gray = repository_size(project) - = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Destroy', [project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-small btn-remove" + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-small" + = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-small btn-remove" - if @projects.blank? .nothing-here-block 0 projects matches = paginate @projects, theme: "gitlab" diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 6d536199851..3bcf1cc9ede 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -1,6 +1,6 @@ %h3.page-title Project: #{@project.name_with_namespace} - = link_to edit_project_path(@project), class: "btn pull-right" do + = link_to edit_namespace_project_path(@project.namespace, @project), class: "btn pull-right" do %i.fa.fa-pencil-square-o Edit %hr @@ -13,7 +13,7 @@ %li %span.light Name: %strong - = link_to @project.name, project_path(@project) + = link_to @project.name, namespace_project_path(@project.namespace, @project) %li %span.light Namespace: %strong @@ -79,11 +79,11 @@ .panel-heading Transfer project .panel-body - = form_for @project, url: transfer_admin_project_path(@project), method: :put, html: { class: 'form-horizontal' } do |f| + = form_for @project, url: transfer_admin_namespace_project_path(@project.namespace, @project), method: :put, html: { class: 'form-horizontal' } do |f| .form-group - = f.label :namespace_id, "Namespace", class: 'control-label' + = f.label :new_namespace_id, "Namespace", class: 'control-label' .col-sm-10 - = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large' + = namespace_select_tag :new_namespace_id, selected: params[:namespace_id], class: 'input-large' .form-group .col-sm-2 @@ -111,7 +111,7 @@ %small (#{@project.users.count}) .pull-right - = link_to project_team_index_path(@project), class: "btn btn-tiny" do + = link_to namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-tiny" do %i.fa.fa-pencil-square-o Manage Access %ul.well-list.team_members @@ -126,7 +126,7 @@ %span.light Owner - else %span.light= project_member.human_access - = link_to project_team_member_path(@project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-small btn-remove" do + = link_to namespace_project_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-small btn-remove" do %i.fa.fa-times .panel-footer = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 88e71aa170f..90267897503 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -206,7 +206,7 @@ - tm = project.team.find_tm(@user.id) %li.project_member .list-item-name - = link_to admin_project_path(project), class: dom_class(project) do + = link_to admin_namespace_project_path(project.namespace, project), class: dom_class(project) do = project.name_with_namespace - if tm @@ -217,7 +217,7 @@ %span.light= tm.human_access - if tm.respond_to? :project - = link_to project_team_member_path(project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from project' do + = link_to namespace_project_team_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from project' do %i.fa.fa-times #ssh-keys.tab-pane = render 'profiles/keys/key_table', admin: true diff --git a/app/views/dashboard/_project.html.haml b/app/views/dashboard/_project.html.haml index f0fb2c1881b..d638a161f40 100644 --- a/app/views/dashboard/_project.html.haml +++ b/app/views/dashboard/_project.html.haml @@ -1,6 +1,6 @@ -= link_to project_path(project), class: dom_class(project) do += link_to namespace_project_path(project.namespace, project), class: dom_class(project) do .dash-project-avatar - = project_icon(project.to_param, alt: '', class: 'avatar project-avatar s40') + = project_icon("#{project.namespace.to_param}/#{project.to_param}", alt: '', class: 'avatar project-avatar s40') .dash-project-access-icon = visibility_level_icon(project.visibility_level) %span.str-truncated diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index 0596738342f..252dbf78882 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -20,6 +20,6 @@ %span.light #{@projects_limit} of #{pluralize(@projects_count, 'project')} displayed. .pull-right - = link_to projects_dashboard_path do + = link_to namespace_projects_dashboard_path do Show all %i.fa.fa-angle-right diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml index 21e44fb1c60..1cea654dc1e 100644 --- a/app/views/dashboard/projects.html.haml +++ b/app/views/dashboard/projects.html.haml @@ -16,10 +16,10 @@ %li.my-project-row %h4.project-title .pull-left - = project_icon(project.to_param, alt: '', class: 'avatar project-avatar s60') + = project_icon("#{project.namespace.to_param}/#{project.to_param}", alt: '', class: 'avatar project-avatar s60') .project-access-icon = visibility_level_icon(project.visibility_level) - = link_to project_path(project), class: dom_class(project) do + = link_to namespace_project_path(project.namespace, project), class: dom_class(project) do = project.name_with_namespace - if project.forked_from_project @@ -27,11 +27,11 @@ %small %i.fa.fa-code-fork Forked from: - = link_to project.forked_from_project.name_with_namespace, project_path(project.forked_from_project) + = link_to project.forked_from_project.name_with_namespace, namespace_project_path(project.namespace, project.forked_from_project) - if current_user.can_leave_project?(project) .pull-right - = link_to leave_project_team_members_path(project), data: { confirm: "Leave project?"}, method: :delete, remote: true, class: "btn-tiny btn remove-row", title: 'Leave project' do + = link_to leave_namespace_project_team_members_path(project.namespace, project), data: { confirm: "Leave project?"}, method: :delete, remote: true, class: "btn-tiny btn remove-row", title: 'Leave project' do %i.fa.fa-sign-out Leave diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml index f0c34def145..c86ce9ae651 100644 --- a/app/views/events/_commit.html.haml +++ b/app/views/events/_commit.html.haml @@ -1,5 +1,5 @@ %li.commit .commit-row-title - = link_to truncate_sha(commit[:id]), project_commit_path(project, commit[:id]), class: "commit_short_id", alt: '' + = link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: ''   = gfm event_commit_title(commit[:message]), project diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index 4c9a39bcc27..cb40aa9970b 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -2,7 +2,7 @@ .event-last-push .event-last-push-text %span You pushed to - = link_to project_commits_path(event.project, event.ref_name) do + = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do %strong= event.ref_name at %strong= link_to_project event.project diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 2b63519edac..0ffd2aa0b98 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -2,7 +2,7 @@ - event.commits.first(15).each do |commit| %p %strong= commit[:author][:name] - = link_to "(##{truncate_sha(commit[:id])})", project_commit_path(event.project, id: commit[:id]) + = link_to "(##{truncate_sha(commit[:id])})", namespace_project_commit_path(event.project.namespace, event.project, id: commit[:id]) %i at = commit[:timestamp].to_time.to_s(:short) diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index a9d3adf41df..b3f32dab79c 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -2,7 +2,7 @@ %span.author_name= link_to_author event %span.event_label{class: event.action_name}= event_action_name(event) - if event.target - %strong= link_to "##{event.target_iid}", [event.project, event.target] + %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] - else %strong= gfm event.target_title at diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index b912b5e092f..092d246a94c 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -4,7 +4,7 @@ - if event.rm_ref? %strong= event.ref_name - else - = link_to project_commits_path(event.project, event.ref_name) do + = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do %strong= event.ref_name at = link_to_project event.project @@ -21,5 +21,5 @@ %li.commits-stat - if event.commits_count > 2 %span ... and #{event.commits_count - 2} more commits. - = link_to project_compare_path(event.project, from: event.commit_from, to: event.commit_to) do + = link_to namespace_project_compare_path(event.project.namespace, event.project, from: event.commit_from, to: event.commit_to) do %strong Compare → #{truncate_sha(event.commit_from)}...#{truncate_sha(event.commit_to)} diff --git a/app/views/explore/projects/_project.html.haml b/app/views/explore/projects/_project.html.haml index ffbddbae4d6..cdd6ede36a1 100644 --- a/app/views/explore/projects/_project.html.haml +++ b/app/views/explore/projects/_project.html.haml @@ -2,7 +2,7 @@ %h4.project-title .project-access-icon = visibility_level_icon(project.visibility_level) - = link_to project.name_with_namespace, project + = link_to project.name_with_namespace, [project.namespace.becomes(Namespace), project] - if current_page?(starred_explore_projects_path) %strong.pull-right @@ -16,11 +16,11 @@ .repo-info - unless project.empty_repo? - = link_to pluralize(project.repository.round_commit_count, 'commit'), project_commits_path(project, project.default_branch) + = link_to pluralize(project.repository.round_commit_count, 'commit'), namespace_project_commits_path(project.namespace, project, project.default_branch) · - = link_to pluralize(project.repository.branch_names.count, 'branch'), project_branches_path(project) + = link_to pluralize(project.repository.branch_names.count, 'branch'), namespace_project_branches_path(project.namespace, project) · - = link_to pluralize(project.repository.tag_names.count, 'tag'), project_tags_path(project) + = link_to pluralize(project.repository.tag_names.count, 'tag'), namespace_project_tags_path(project.namespace, project) - else %i.fa.fa-exclamation-triangle Empty repository diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index a2f1d28a275..5fe93f4e083 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -11,9 +11,9 @@ .nothing-here-block This group has no projects yet - projects.each do |project| %li.project-row - = link_to project_path(project), class: dom_class(project) do + = link_to namespace_project_path(project.namespace, project), class: dom_class(project) do .dash-project-avatar - = project_icon(project.to_param, alt: '', class: 'avatar s40') + = project_icon("#{project.namespace.to_param}/#{project.to_param}", alt: '', class: 'avatar s40') .dash-project-access-icon = visibility_level_icon(project.visibility_level) %span.str-truncated diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml index c95c2e89670..27d0c62df8c 100644 --- a/app/views/groups/milestones/_issue.html.haml +++ b/app/views/groups/milestones/_issue.html.haml @@ -2,9 +2,9 @@ %span.milestone-row - project = issue.project %strong #{project.name} · - = link_to [project, issue] do + = link_to [project.namespace.becomes(Namespace), project, issue] do %span.cgray ##{issue.iid} - = link_to_gfm issue.title, [project, issue], title: issue.title + = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title .pull-right.assignee-icon - if issue.assignee = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16" diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml index e0c903bfdb2..b2d2097dfab 100644 --- a/app/views/groups/milestones/_merge_request.html.haml +++ b/app/views/groups/milestones/_merge_request.html.haml @@ -2,9 +2,9 @@ %span.milestone-row - project = merge_request.project %strong #{project.name} · - = link_to [project, merge_request] do + = link_to [project.namespace.becomes(Namespace), project, merge_request] do %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [project, merge_request], title: merge_request.title + = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title .pull-right.assignee-icon - if merge_request.assignee = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 7bcac56c37b..e3606d167ad 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -28,7 +28,7 @@ - @group_milestone.milestones.each do |milestone| %tr %td - = link_to "#{milestone.project.name}", project_milestone_path(milestone.project, milestone) + = link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) %td = milestone.issues.opened.count %td diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 40c81e8cd5b..8c829654fb0 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -16,8 +16,8 @@ %span.label.label-gray = repository_size(project) .pull-right - = link_to 'Members', project_team_index_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-small" + = link_to 'Members', namespace_project_team_index_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-small" + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-small" = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-small btn-remove" - if @projects.blank? .nothing-here-block This group has no projects yet diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index a6900f4a04b..bece8061fb9 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -30,6 +30,6 @@ = auto_discovery_link_tag :atom, projects_url(:atom, private_token: current_user.private_token), title: "Dashboard feed" - if @project && !@project.new_record? - if current_controller?(:tree, :commits) - = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, format: :atom, private_token: current_user.private_token), title: "Recent commits to #{@project.name}:#{@ref}") + = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "Recent commits to #{@project.name}:#{@ref}") - if current_controller?(:issues) - = auto_discovery_link_tag(:atom, project_issues_url(@project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") + = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 353f7ce34f1..3c58f10e759 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -1,3 +1,3 @@ :javascript - GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_project_path(@project, type: @noteable.class, type_id: params[:id])}" + GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(@project.namespace, @project, type: @noteable.class, type_id: params[:id])}" GitLab.GfmAutoComplete.setup(); diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 74334b12e63..2f38d596c65 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -5,7 +5,7 @@ %span Overview = nav_link(controller: :projects) do - = link_to admin_projects_path, title: 'Projects' do + = link_to admin_namespaces_projects_path, title: 'Projects' do %i.fa.fa-cube %span Projects diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 8d572ddcd10..62a51047c3c 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,13 +1,13 @@ %ul.project-navigation.nav.nav-sidebar - if @project_settings_nav = nav_link do - = link_to project_path(@project), title: 'Back to project', class: "" do + = link_to namespace_project_path(@project.namespace, @project), title: 'Back to project', class: "" do %i.fa.fa-angle-left %span Back to project = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings', class: "stat-tab tab no-highlight" do + = link_to edit_namespace_project_path(@project.namespace, @project), title: 'Settings', class: "stat-tab tab no-highlight" do %i.fa.fa-cogs %span Settings @@ -17,34 +17,34 @@ - else = nav_link(path: 'projects#show', html_options: {class: "home"}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do + = link_to namespace_project_path(@project.namespace, @project), title: 'Project', class: 'shortcuts-project' do %i.fa.fa-dashboard %span Project - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do - = link_to project_tree_path(@project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree' do + = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree' do %i.fa.fa-files-o %span Files - if project_nav_tab? :commits = nav_link(controller: %w(commit commits compare repositories tags branches)) do - = link_to project_commits_path(@project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits' do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits' do %i.fa.fa-history %span Commits - if project_nav_tab? :network = nav_link(controller: %w(network)) do - = link_to project_network_path(@project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network' do + = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network' do %i.fa.fa-code-fork %span Network - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do - = link_to project_graph_path(@project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs' do + = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs' do %i.fa.fa-area-chart %span Graphs @@ -60,7 +60,7 @@ - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %i.fa.fa-tasks %span Merge Requests @@ -68,21 +68,21 @@ - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = link_to project_wiki_path(@project, :home), title: 'Wiki', class: 'shortcuts-wiki' do + = link_to namespace_project_wiki_path(@project.namespace, @project, :home), title: 'Wiki', class: 'shortcuts-wiki' do %i.fa.fa-book %span Wiki - if project_nav_tab? :snippets = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do %i.fa.fa-file-text-o %span Snippets - if project_nav_tab? :settings = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings', class: "stat-tab tab no-highlight" do + = link_to edit_namespace_project_path(@project.namespace, @project), title: 'Settings', class: "stat-tab tab no-highlight" do %i.fa.fa-cogs %span Settings diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index a722db2f32c..8cca80e5248 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -28,4 +28,4 @@ #{link_to "View it on GitLab", @target_url} = email_action @target_url - if @project - You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, project_url(@project)} project team. + You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} project team. diff --git a/app/views/notify/_reassigned_issuable_email.text.erb b/app/views/notify/_reassigned_issuable_email.text.erb index 817d030c362..855d37429d9 100644 --- a/app/views/notify/_reassigned_issuable_email.text.erb +++ b/app/views/notify/_reassigned_issuable_email.text.erb @@ -1,6 +1,6 @@ Reassigned <%= issuable.class.model_name.human.titleize %> <%= issuable.iid %> -<%= url_for([issuable.project, issuable, {only_path: false}]) %> +<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, {only_path: false}]) %> Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%> to <%= "#{issuable.assignee_id ? issuable.assignee_name : 'Unassigned'}" %> diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml index 49f160a0d5f..ac703b31edd 100644 --- a/app/views/notify/closed_issue_email.text.haml +++ b/app/views/notify/closed_issue_email.text.haml @@ -1,3 +1,3 @@ = "Issue was closed by #{@updated_by.name}" -Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)} +Issue ##{@issue.iid}: #{namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)} diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index d6b76e906c5..59db86b08bc 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,6 +1,6 @@ = "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}" -Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} +Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') diff --git a/app/views/notify/issue_status_changed_email.text.erb b/app/views/notify/issue_status_changed_email.text.erb index 4200881f7e8..e6ab3fcde77 100644 --- a/app/views/notify/issue_status_changed_email.text.erb +++ b/app/views/notify/issue_status_changed_email.text.erb @@ -1,4 +1,4 @@ Issue was <%= @issue_status %> by <%= @updated_by.name %> -Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> +Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %> diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index 8750bf86e2c..b96dd0fd8ab 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -1,6 +1,6 @@ = "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}" -Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} +Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index 360da60bc3f..9db75bdb19e 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -1,6 +1,6 @@ = "Merge Request ##{@merge_request.iid} was merged" -Merge Request Url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} +Merge Request Url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb index d36f54eb1ca..0cc62935498 100644 --- a/app/views/notify/new_issue_email.text.erb +++ b/app/views/notify/new_issue_email.text.erb @@ -1,5 +1,5 @@ New Issue was created. -Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> +Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %> Author: <%= @issue.author_name %> Asignee: <%= @issue.assignee_name %> diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index 16be4bb619f..f08039ad045 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -1,6 +1,6 @@ New Merge Request #<%= @merge_request.iid %> -<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> +<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %> <%= merge_path_description(@merge_request, 'to') %> Author: <%= @merge_request.author_name %> diff --git a/app/views/notify/note_commit_email.text.erb b/app/views/notify/note_commit_email.text.erb index aab8e5cfb6c..aaeaf5fdf73 100644 --- a/app/views/notify/note_commit_email.text.erb +++ b/app/views/notify/note_commit_email.text.erb @@ -1,6 +1,6 @@ New comment for Commit <%= @commit.short_id %> -<%= url_for(project_commit_url(@note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> +<%= url_for(namespace_project_commit_url(@note.project.namespace, @note.project, id: @commit.id, anchor: "note_#{@note.id}")) %> Author: <%= @note.author_name %> diff --git a/app/views/notify/note_issue_email.text.erb b/app/views/notify/note_issue_email.text.erb index 8a61f54a337..e33cbcd70f2 100644 --- a/app/views/notify/note_issue_email.text.erb +++ b/app/views/notify/note_issue_email.text.erb @@ -1,6 +1,6 @@ New comment for Issue <%= @issue.iid %> -<%= url_for(project_issue_url(@issue.project, @issue, anchor: "note_#{@note.id}")) %> +<%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue, anchor: "note_#{@note.id}")) %> Author: <%= @note.author_name %> diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb index 79e72ca16c6..1d1411992a6 100644 --- a/app/views/notify/note_merge_request_email.text.erb +++ b/app/views/notify/note_merge_request_email.text.erb @@ -1,6 +1,6 @@ New comment for Merge Request <%= @merge_request.iid %> -<%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> +<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %> <%= @note.author_name %> diff --git a/app/views/notify/project_access_granted_email.html.haml b/app/views/notify/project_access_granted_email.html.haml index 4596205f39b..dfc30a2d360 100644 --- a/app/views/notify/project_access_granted_email.html.haml +++ b/app/views/notify/project_access_granted_email.html.haml @@ -1,5 +1,5 @@ %p = "You have been granted #{@project_member.human_access} access to project" %p - = link_to project_url(@project) do + = link_to namespace_project_url(@project.namespace, @project) do = @project.name_with_namespace diff --git a/app/views/notify/project_access_granted_email.text.erb b/app/views/notify/project_access_granted_email.text.erb index de24feb802f..68eb1611ba7 100644 --- a/app/views/notify/project_access_granted_email.text.erb +++ b/app/views/notify/project_access_granted_email.text.erb @@ -1,4 +1,4 @@ You have been granted <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %> -<%= url_for(project_url(@project)) %> +<%= url_for(namespace_project_url(@project.namespace, @project)) %> diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml index fe248584e55..f53de2de287 100644 --- a/app/views/notify/project_was_moved_email.html.haml +++ b/app/views/notify/project_was_moved_email.html.haml @@ -2,7 +2,7 @@ Project was moved to another location %p The project is now located under - = link_to project_url(@project) do + = link_to namespace_project_url(@project.namespace, @project) do = @project.name_with_namespace %p To update the remote url in your local repository run (for ssh): diff --git a/app/views/notify/project_was_moved_email.text.erb b/app/views/notify/project_was_moved_email.text.erb index 664148fb3ba..b3f18b35a4d 100644 --- a/app/views/notify/project_was_moved_email.text.erb +++ b/app/views/notify/project_was_moved_email.text.erb @@ -1,7 +1,7 @@ Project was moved to another location The project is now located under -<%= project_url(@project) %> +<%= namespace_project_url(@project.namespace, @project) %> To update the remote url in your local repository run (for ssh): diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index b6fe445867c..a45d1dedcd1 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,11 +1,11 @@ -%h3 #{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, project_url(@project)} +%h3 #{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} %h4 Commits: %ul - @commits.each do |commit| %li - %strong #{link_to commit.short_id, project_commit_url(@project, commit)} + %strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)} %div %span by #{commit.author_name} %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 6f5f9eda2c5..fa355cb5269 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -1,9 +1,9 @@ -#{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, project_url(@project)} +#{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} \ Commits: - @commits.each do |commit| - #{link_to commit.short_id, project_commit_url(@project, commit)} by #{commit.author_name} + #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)} by #{commit.author_name} #{commit.safe_message} \- - - - - \ diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml index 6ff46970336..2d5120f283b 100644 --- a/app/views/projects/_dropdown.html.haml +++ b/app/views/projects/_dropdown.html.haml @@ -9,24 +9,24 @@ New issue - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) %li - = link_to new_project_merge_request_path(@project), title: "New Merge Request" do + = link_to new_namespace_project_merge_request_path(@project.namespace, @project), title: "New Merge Request" do New merge request - if @project.snippets_enabled && can?(current_user, :write_snippet, @project) %li - = link_to new_project_snippet_path(@project), title: "New Snippet" do + = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do New snippet - if can?(current_user, :admin_team_member, @project) %li - = link_to new_project_team_member_path(@project), title: "New project member" do + = link_to new_namespace_project_team_member_path(@project.namespace, @project), title: "New project member" do New project member - if can? current_user, :push_code, @project %li.divider %li - = link_to new_project_branch_path(@project) do + = link_to new_namespace_project_branch_path(@project.namespace, @project) do %i.fa.fa-code-fork Git branch %li - = link_to new_project_tag_path(@project) do + = link_to new_namespace_project_tag_path(@project.namespace, @project) do %i.fa.fa-tag Git tag diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 5697f9ea1af..60d461da664 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,28 +1,28 @@ - empty_repo = @project.empty_repo? .project-home-panel{:class => ("empty-project" if empty_repo)} .project-identicon-holder - = project_icon(@project.to_param, alt: '', class: 'avatar project-avatar') + = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar') .project-home-row .project-home-desc - if @project.description.present? = escaped_autolink(@project.description) - if can?(current_user, :admin_project, @project) – - = link_to 'Edit', edit_project_path + = link_to 'Edit', edit_namespace_project_path - elsif !@project.empty_repo? && @repository.readme - readme = @repository.readme – - = link_to project_blob_path(@project, tree_join(@repository.root_ref, readme.name)) do + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do = readme.name .star-fork-buttons - unless @project.empty_repo? .fork-buttons - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to project_path(current_user.fork_of(@project)), title: 'Go to my fork' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to my fork' do = link_to_toggle_fork - else - = link_to new_project_fork_path(@project), title: "Fork project" do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project" do = link_to_toggle_fork .star-buttons diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 9e2e214b3e8..52a31610e6c 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -53,7 +53,7 @@ %span.light No open milestones available.   - if can? current_user, :admin_milestone, issuable.project - = link_to 'Create new milestone', new_project_milestone_path(issuable.project), target: :blank + = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank .form-group = f.label :label_ids, class: 'control-label' do %i.fa.fa-tag @@ -66,7 +66,7 @@ %span.light No labels yet.   - if can? current_user, :admin_label, issuable.project - = link_to 'Create new label', new_project_label_path(issuable.project), target: :blank + = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank .form-actions - if !issuable.project.empty_repo? && contribution_guide_url(issuable.project) && !issuable.persisted? @@ -82,4 +82,4 @@ - cancel_project = issuable.source_project - else - cancel_project = issuable.project - = link_to 'Cancel', [cancel_project, issuable], class: 'btn btn-cancel' + = link_to 'Cancel', [cancel_project.namespace.becomes(Namespace), cancel_project, issuable], class: 'btn btn-cancel' diff --git a/app/views/projects/_issues_nav.html.haml b/app/views/projects/_issues_nav.html.haml index f4e3d9a1093..3f14616af2c 100644 --- a/app/views/projects/_issues_nav.html.haml +++ b/app/views/projects/_issues_nav.html.haml @@ -1,20 +1,20 @@ %ul.nav.nav-tabs - if project_nav_tab? :issues = nav_link(controller: :issues) do - = link_to project_issues_path(@project), class: "tab" do + = link_to namespace_project_issues_path(@project.namespace, @project), class: "tab" do %i.fa.fa-exclamation-circle Issues - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project), class: "tab" do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), class: "tab" do %i.fa.fa-tasks Merge Requests = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), class: "tab" do + = link_to namespace_project_milestones_path(@project.namespace, @project), class: "tab" do %i.fa.fa-clock-o Milestones = nav_link(controller: :labels) do - = link_to project_labels_path(@project), class: "tab" do + = link_to namespace_project_labels_path(@project.namespace, @project), class: "tab" do %i.fa.fa-tags Labels @@ -22,13 +22,13 @@ - if current_controller?(:issues) - if current_user %li.hidden-xs - = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }) do + = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }) do %i.fa.fa-rss %li.pull-right .pull-right .pull-left - = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do + = form_tag namespace_project_issues_path(@project.namespace, @project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do .append-right-10.hidden-xs.hidden-sm = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } = hidden_field_tag :state, params['state'] @@ -38,7 +38,7 @@ = hidden_field_tag :label_id, params['label_id'] - if can? current_user, :write_issue, @project - = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do %i.fa.fa-plus New Issue @@ -46,6 +46,6 @@ %li.pull-right .pull-right - if can? current_user, :write_merge_request, @project - = link_to new_project_merge_request_path(@project), class: "btn btn-new pull-left", title: "New Merge Request" do + = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new pull-left", title: "New Merge Request" do %i.fa.fa-plus New Merge Request diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index cb75149434f..a2c8ee1d116 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -4,7 +4,7 @@ Write %li = link_to '#md-preview-holder', class: 'js-md-preview-button', - data: { url: markdown_preview_project_path(@project) } do + data: { url: markdown_preview_namespace_project_path(@project.namespace, @project) } do Preview %div .md-write-holder diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml index 646e48a1e1d..1a18bb065ad 100644 --- a/app/views/projects/_settings_nav.html.haml +++ b/app/views/projects/_settings_nav.html.haml @@ -1,31 +1,31 @@ %ul.project-settings-nav.sidebar-subnav = nav_link(path: 'projects#edit') do - = link_to edit_project_path(@project), title: 'Project', class: "stat-tab tab " do + = link_to edit_namespace_project_path(@project.namespace, @project), title: 'Project', class: "stat-tab tab " do %i.fa.fa-pencil-square-o %span Project = nav_link(controller: [:team_members, :teams]) do - = link_to project_team_index_path(@project), title: 'Members', class: "team-tab tab" do + = link_to namespace_project_team_index_path(@project.namespace, @project), title: 'Members', class: "team-tab tab" do %i.fa.fa-users %span Members = nav_link(controller: :deploy_keys) do - = link_to project_deploy_keys_path(@project), title: 'Deploy Keys' do + = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do %i.fa.fa-key %span Deploy Keys = nav_link(controller: :hooks) do - = link_to project_hooks_path(@project), title: 'Web Hooks' do + = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do %i.fa.fa-link %span Web Hooks = nav_link(controller: :services) do - = link_to project_services_path(@project), title: 'Services' do + = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do %i.fa.fa-cogs %span Services = nav_link(controller: :protected_branches) do - = link_to project_protected_branches_path(@project), title: 'Protected Branches' do + = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do %i.fa.fa-lock %span Protected branches diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 51a2f20d1e2..5a33d18e631 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -15,11 +15,11 @@ %tr %td.blame-commit %span.commit - = link_to commit.short_id, project_commit_path(@project, commit), class: "commit_short_id" + = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "commit_short_id"   = commit_author_link(commit, avatar: true, size: 16)   - = link_to_gfm truncate(commit.title, length: 20), project_commit_path(@project, commit.id), class: "row_title" + = link_to_gfm truncate(commit.title, length: 20), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "row_title" %td.lines.blame-numbers %pre - (since...(since + lines.count)).each do |i| diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index f428ae41ef4..b5b29540bb6 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,19 +1,19 @@ .btn-group.tree-btn-group = edit_blob_link(@project, @ref, @path) - = link_to 'Raw', project_raw_path(@project, @id), + = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-small', target: '_blank' -# only show normal/blame view links for text files - if @blob.text? - - if current_page? project_blame_path(@project, @id) - = link_to 'Normal View', project_blob_path(@project, @id), + - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) + = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), class: 'btn btn-small' - else - = link_to 'Blame', project_blame_path(@project, @id), + = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id), class: 'btn btn-small' unless @blob.empty? - = link_to 'History', project_commits_path(@project, @id), + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn btn-small' - if @ref != @commit.sha - = link_to 'Permalink', project_blob_path(@project, + = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.sha, @path)), class: 'btn btn-small' - if allowed_tree_edit? diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 68f3b08b8c8..64cc3fad6cf 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -1,17 +1,17 @@ %ul.breadcrumb.repo-breadcrumb %li %i.fa.fa-angle-right - = link_to project_tree_path(@project, @ref) do + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = @project.path - tree_breadcrumbs(@tree, 6) do |title, path| %li - if path - if path.end_with?(@path) - = link_to project_blob_path(@project, path) do + = link_to namespace_project_blob_path(@project.namespace, @project, path) do %strong = truncate(title, length: 40) - else - = link_to truncate(title, length: 40), project_tree_path(@project, path) + = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - else = link_to title, '#' diff --git a/app/views/projects/blob/_download.html.haml b/app/views/projects/blob/_download.html.haml index c24eeea4931..f2c5e95ecf4 100644 --- a/app/views/projects/blob/_download.html.haml +++ b/app/views/projects/blob/_download.html.haml @@ -1,6 +1,6 @@ .file-content.blob_file.blob-no-preview .center - = link_to project_raw_path(@project, @id) do + = link_to namespace_project_raw_path(@project.namespace, @project, @id) do %h1.light %i.fa.fa-download %h4 diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index c5568315cb1..09559a4967b 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -9,7 +9,7 @@ %strong= @ref .modal-body - = form_tag project_blob_path(@project, @id), method: :delete, class: 'form-horizontal' do + = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal' do = render 'shared/commit_message_container', params: params, placeholder: 'Removed this file because...' .form-group diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index b150b639888..6884ad1f2f3 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -6,11 +6,11 @@ Edit file %li - = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do + = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do %i.fa.fa-eye = editing_preview_title(@blob.name) - = form_tag(project_update_blob_path(@project, @id), method: :put, class: "form-horizontal") do + = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: "form-horizontal") do = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index df6aedbe17d..45865d552ae 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,12 +1,12 @@ %h3.page-title New file .file-editor - = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'form-horizontal form-new-file') do + = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file') do = render 'projects/blob/editor', ref: @ref = render 'shared/commit_message_container', params: params, placeholder: 'Add new file' = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, - cancel_path: project_tree_path(@project, @id) + cancel_path: namespace_project_tree_path(@project.namespace, @project, @id) :javascript blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 8e58f3c247a..8de629b03e9 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -1,7 +1,7 @@ - commit = @repository.commit(branch.target) %li(class="js-branch-#{branch.name}") %h4 - = link_to project_tree_path(@project, branch.name) do + = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do %strong.str-truncated= branch.name - if branch.name == @repository.root_ref %span.label.label-info default @@ -13,12 +13,12 @@ - if can?(current_user, :download_code, @project) = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-small' - if branch.name != @repository.root_ref - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-small', method: :post, title: "Compare" do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-small', method: :post, title: "Compare" do %i.fa.fa-files-o Compare - if can_remove_branch?(@project, branch.name) - = link_to project_branch_path(@project, branch.name), class: 'btn btn-grouped btn-small btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-small btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do %i.fa.fa-trash-o - if commit diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index d2aefd815a1..f77d02a97fb 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -3,7 +3,7 @@ Branches .pull-right - if can? current_user, :push_code, @project - = link_to new_project_branch_path(@project), class: 'btn btn-create' do + = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do %i.fa.fa-add-sign New branch   @@ -17,11 +17,11 @@ %b.caret %ul.dropdown-menu %li - = link_to project_branches_path(sort: nil) do + = link_to namespace_project_branches_path(sort: nil) do Name - = link_to project_branches_path(sort: 'recently_updated') do + = link_to namespace_project_branches_path(sort: 'recently_updated') do = sort_title_recently_updated - = link_to project_branches_path(sort: 'last_updated') do + = link_to namespace_project_branches_path(sort: 'last_updated') do = sort_title_oldest_updated %hr - unless @branches.empty? diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 2719bcc33bc..e5fcb98c68c 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -5,7 +5,7 @@ %h3.page-title %i.fa.fa-code-fork New branch -= form_tag project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal" do += form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal" do .form-group = label_tag :branch_name, 'Name for new branch', class: 'control-label' .col-sm-10 @@ -16,7 +16,7 @@ = text_field_tag :ref, params[:ref], placeholder: 'existing branch name, tag or commit SHA', required: true, tabindex: 2, class: 'form-control' .form-actions = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 - = link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel' + = link_to 'Cancel', namespace_project_branches_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript disableButtonIfAnyEmptyField($("#new-branch-form"), ".form-control", ".btn-create"); diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index dd28a35d41d..7409f702c5d 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -10,15 +10,15 @@ Download as %span.caret %ul.dropdown-menu - %li= link_to "Email Patches", project_commit_path(@project, @commit, format: :patch) - %li= link_to "Plain Diff", project_commit_path(@project, @commit, format: :diff) - = link_to project_tree_path(@project, @commit), class: "btn btn-primary btn-grouped" do + %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) + %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) + = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-primary btn-grouped" do %span Browse Code » %div %p %span.light Commit - = link_to @commit.id, project_commit_path(@project, @commit) + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit) .commit-info-row %span.light Authored by %strong @@ -35,7 +35,7 @@ .commit-info-row %span.cgray= pluralize(@commit.parents.count, "parent") - @commit.parents.each do |parent| - = link_to parent.short_id, project_commit_path(@project, parent) + = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent) .commit-info-row.branches %i.fa.fa-spinner.fa-spin @@ -49,4 +49,4 @@ :coffeescript $ -> - $(".commit-info-row.branches").load("#{branches_project_commit_path(@project, @commit.id)}") \ No newline at end of file + $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}") diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index b01e806210c..82aac1fbd15 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -1,7 +1,7 @@ - if @branches.any? %span - branch = commit_default_branch(@project, @branches) - = link_to(project_tree_path(@project, branch)) do + = link_to(namespace_project_tree_path(@project.namespace, @project, branch)) do %span.label.label-gray %i.fa.fa-code-fork = branch @@ -13,4 +13,4 @@ - if @branches.any? = commit_branches_links(@project, @branches) - if @tags.any? - = commit_tags_links(@project, @tags) \ No newline at end of file + = commit_tags_links(@project, @tags) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1eb17f760dc..09c3f83fb3e 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,9 +1,9 @@ %li.commit.js-toggle-container .commit-row-title - = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"   %span.str-truncated - = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - if commit.description? %a.text-expander.js-toggle-button ... diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 0c9d906481b..83e4d24cf5f 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,15 +1,15 @@ %ul.nav.nav-tabs = nav_link(controller: [:commit, :commits]) do - = link_to 'Commits', project_commits_path(@project, @repository.root_ref) + = link_to 'Commits', namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) = nav_link(controller: :compare) do - = link_to 'Compare', project_compare_index_path(@project, from: @repository.root_ref, to: @ref || @repository.root_ref) + = link_to 'Compare', namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do + = link_to namespace_project_branches_path(@project.namespace, @project) do Branches %span.badge.js-totalbranch-count= @repository.branches.size = nav_link(controller: :tags) do - = link_to project_tags_path(@project) do + = link_to namespace_project_tags_path(@project.namespace, @project) do Tags %span.badge.js-totaltags-count= @repository.tags.length diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml index 574599aa2d2..c03bc3f9df9 100644 --- a/app/views/projects/commits/_inline_commit.html.haml +++ b/app/views/projects/commits/_inline_commit.html.haml @@ -1,8 +1,8 @@ %li.commit.inline-commit .commit-row-title - = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"   %span.str-truncated - = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" .pull-right #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder index 32c82edb248..9211de72b1b 100644 --- a/app/views/projects/commits/show.atom.builder +++ b/app/views/projects/commits/show.atom.builder @@ -1,15 +1,15 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "Recent commits to #{@project.name}:#{@ref}" - xml.link :href => project_commits_url(@project, @ref, format: :atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => project_commits_url(@project, @ref), :rel => "alternate", :type => "text/html" - xml.id project_commits_url(@project, @ref) + xml.link :href => namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom), :rel => "self", :type => "application/atom+xml" + xml.link :href => namespace_project_commits_url(@project.namespace, @project, @ref), :rel => "alternate", :type => "text/html" + xml.id namespace_project_commits_url(@project.namespace, @project, @ref) xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any? @commits.each do |commit| xml.entry do - xml.id project_commit_url(@project, :id => commit.id) - xml.link :href => project_commit_url(@project, :id => commit.id) + xml.id namespace_project_commit_url(@project.namespace, @project, :id => commit.id) + xml.link :href => namespace_project_commit_url(@project.namespace, @project, :id => commit.id) xml.title truncate(commit.title, :length => 80) xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(commit.author_email) diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index b80639763c8..7ea855e1a4e 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -5,7 +5,7 @@ - if current_user && current_user.private_token .commits-feed-holder.hidden-xs.hidden-sm - = link_to project_commits_path(@project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed", class: 'btn' do + = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Feed", class: 'btn' do %i.fa.fa-rss Commits feed diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index cb0a3747f7d..dfb1dded9ea 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -1,4 +1,4 @@ -= form_tag project_compare_index_path(@project), method: :post, class: 'form-inline' do += form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline' do .clearfix.append-bottom-20 - if params[:to] && params[:from] = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has_tooltip', title: 'Switch base of comparison'} diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index a0345dbd9c3..52da85cbdfa 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -1,19 +1,20 @@ %li .pull-right - if @available_keys.include?(deploy_key) - = link_to enable_project_deploy_key_path(@project, deploy_key), class: 'btn btn-small', method: :put do + = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-small', method: :put do %i.fa.fa-plus Enable - else - if deploy_key.projects.count > 1 - = link_to disable_project_deploy_key_path(@project, deploy_key), class: 'btn btn-small', method: :put do + = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-small', method: :put do %i.fa.fa-power-off Disable - else - = link_to 'Remove', project_deploy_key_path(@project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :delete, class: "btn btn-remove delete-key btn-small pull-right" + = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :delete, class: "btn btn-remove delete-key btn-small pull-right" - = link_to project_deploy_key_path(deploy_key.projects.include?(@project) ? @project : deploy_key.projects.first, deploy_key) do + = key_project = deploy_key.projects.include?(@project) ? @project : deploy_key.projects.first + = link_to namespace_project_deploy_key_path(key_project.namespace, key_project, deploy_key) do %i.fa.fa-key %strong= deploy_key.title diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 162ef05b367..91675b3738e 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,5 +1,5 @@ %div - = form_for [@project, @key], url: project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal' } do |f| -if @key.errors.any? .alert.alert-danger %ul @@ -19,5 +19,5 @@ .form-actions = f.submit 'Create', class: "btn-create btn" - = link_to "Cancel", project_deploy_keys_path(@project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_deploy_keys_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml index 6f475e0b395..c02a18146eb 100644 --- a/app/views/projects/deploy_keys/index.html.haml +++ b/app/views/projects/deploy_keys/index.html.haml @@ -1,7 +1,7 @@ %h3.page-title Deploy keys allow read-only access to the repository - = link_to new_project_deploy_key_path(@project), class: "btn btn-new pull-right", title: "New Deploy Key" do + = link_to new_namespace_project_deploy_key_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Deploy Key" do %i.fa.fa-plus New Deploy Key @@ -20,7 +20,7 @@ = render @enabled_keys - if @enabled_keys.blank? .light-well - .nothing-here-block Create a #{link_to 'new deploy key', new_project_deploy_key_path(@project)} or add an existing one + .nothing-here-block Create a #{link_to 'new deploy key', new_namespace_project_deploy_key_path(@project.namespace, @project)} or add an existing one .col-md-6.available-keys %h5 %strong Deploy keys diff --git a/app/views/projects/deploy_keys/show.html.haml b/app/views/projects/deploy_keys/show.html.haml index c66e6bc69c3..405b5bcd0d3 100644 --- a/app/views/projects/deploy_keys/show.html.haml +++ b/app/views/projects/deploy_keys/show.html.haml @@ -5,9 +5,9 @@ created on = @key.created_at.stamp("Aug 21, 2011") .back-link - = link_to project_deploy_keys_path(@project) do + = link_to namespace_project_deploy_keys_path(@project.namespace, @project) do ← To keys list %hr %pre= @key.key .pull-right - = link_to 'Remove', project_deploy_key_path(@project, @key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn-remove btn delete-key" + = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, @key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn-remove btn delete-key" diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 8d080f710d8..2569e91ccfa 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,6 +1,6 @@ - blob = project.repository.blob_for_diff(@commit, diff_file.diff) - return unless blob -- blob_diff_path = diff_project_blob_path(project, tree_join(@commit.id, diff_file.file_path)) +- blob_diff_path = namespace_project_blob_diff_path(project.namespace, project, tree_join(@commit.id, diff_file.file_path)) .diff-file{id: "diff-#{i}", data: {blob_diff_path: blob_diff_path }} .diff-header{id: "file-path-#{hexdigest(diff_file.new_path || diff_file.old_path)}"} - if diff_file.deleted_file diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 900646dd0a4..058b71b21f5 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -10,7 +10,7 @@ %div.two-up.view %span.wrap .frame.deleted - %a{href: project_blob_path(@project, tree_join(@commit.parent_id, diff.old_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))} %img{src: "data:#{old_file.mime_type};base64,#{Base64.encode64(old_file.data)}"} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" @@ -22,7 +22,7 @@ %span.meta-height %span.wrap .frame.added - %a{href: project_blob_path(@project, tree_join(@commit.id, diff.new_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))} %img{src: "data:#{file.mime_type};base64,#{Base64.encode64(file.data)}"} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size file.size}" diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 86ed6bbeaa2..de091038e30 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -7,11 +7,11 @@ - if current_controller?(:commit) or current_controller?(:merge_requests) - if current_controller?(:commit) - = link_to "Plain diff", project_commit_path(@project, @commit, format: :diff), class: "btn btn-warning btn-small" - = link_to "Email patch", project_commit_path(@project, @commit, format: :patch), class: "btn btn-warning btn-small" + = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-warning btn-small" + = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-small" - elsif @merge_request && @merge_request.persisted? - = link_to "Plain diff", project_merge_request_path(@project, @merge_request, format: :diff), class: "btn btn-warning btn-small" - = link_to "Email patch", project_merge_request_path(@project, @merge_request, format: :patch), class: "btn btn-warning btn-small" + = link_to "Plain diff", namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :diff), class: "btn btn-warning btn-small" + = link_to "Email patch", namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :patch), class: "btn btn-warning btn-small" %p To preserve performance only %strong #{allowed_diff_size} of #{diffs.size} diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 737cda411bc..8240c186616 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -6,7 +6,7 @@ Project settings %hr .panel-body - = form_for @project, remote: true, html: { multipart: true, class: "edit_project form-horizontal" }, authenticity_token: true do |f| + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal" }, authenticity_token: true do |f| %fieldset .form-group.project_name_holder @@ -78,7 +78,7 @@ .col-sm-2 .col-sm-10 - if @project.avatar? - = project_icon(@project.to_param, alt: '', class: 'avatar project-avatar s160') + = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') %p.light - if @project.avatar_in_git Project avatar in repository: #{ @project.avatar_in_git } @@ -96,7 +96,7 @@ .light The maximum file size allowed is 200KB. - if @project.avatar? %hr - = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" + = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" .form-actions = f.submit 'Save changes', class: "btn btn-save" @@ -116,7 +116,7 @@ The project can be committed to. %br %strong Once active this project shows up in the search and on the dashboard. - = link_to 'Unarchive', unarchive_project_path(@project), + = link_to 'Unarchive', unarchive_namespace_project_path(@project.namespace, @project), data: { confirm: "Are you sure that you want to unarchive this project?\nWhen this project is unarchived it is active and can be committed to again." }, method: :post, class: "btn btn-success" - else @@ -130,7 +130,7 @@ It is hidden from the dashboard and doesn't show up in searches. %br %strong Archived projects cannot be committed to! - = link_to 'Archive', archive_project_path(@project), + = link_to 'Archive', archive_namespace_project_path(@project.namespace, @project), data: { confirm: "Are you sure that you want to archive this project?\nAn archived project cannot be committed to." }, method: :post, class: "btn btn-warning" - else @@ -140,7 +140,7 @@ .panel-heading Rename repository .errors-holder .panel-body - = form_for(@project, html: { class: 'form-horizontal' }) do |f| + = form_for([@project.namespace.becomes(Namespace), @project], html: { class: 'form-horizontal' }) do |f| .form-group.project_name_holder = f.label :name, class: 'control-label' do Project name @@ -168,13 +168,13 @@ .panel-heading Transfer project .errors-holder .panel-body - = form_for(@project, url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| + = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'transfer-project form-horizontal' }) do |f| .form-group - = f.label :namespace_id, class: 'control-label' do + = label_tag :new_namespace_id, nil, class: 'control-label' do %span Namespace .col-sm-10 .form-group - = f.select :namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace' }, { class: 'select2' } + = select_tag :new_namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace', class: 'select2' } %ul %li Be careful. Changing the project's namespace can have unintended side effects. %li You can only transfer the project to namespaces you manage. @@ -188,7 +188,7 @@ .panel.panel-default.panel.panel-danger .panel-heading Remove project .panel-body - = form_tag(project_path(@project), method: :delete, html: { class: 'form-horizontal'}) do + = form_tag(namespace_project_path(@project.namespace, @project), method: :delete, html: { class: 'form-horizontal'}) do %p Removing the project will delete its repository and all related resources including issues, merge requests etc. %br diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index b925bcb7fac..49806ceaa96 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -9,7 +9,7 @@ The repository for this project is empty %h4 You can - = link_to project_new_blob_path(@project, 'master'), class: 'btn btn-new btn-lg' do + = link_to namespace_project_new_blob_path(@project.namespace, @project, 'master'), class: 'btn btn-new btn-lg' do add a file  or do a push via the command line. @@ -46,4 +46,4 @@ - if can? current_user, :remove_project, @project .prepend-top-20 - = link_to 'Remove project', @project, data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml index 76d3aa5bf00..8eb4f795971 100644 --- a/app/views/projects/forks/error.html.haml +++ b/app/views/projects/forks/error.html.haml @@ -15,6 +15,6 @@ = @forked_project.errors.full_messages.first %p - = link_to new_project_fork_path(@project), title: "Fork", class: "btn" do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork", class: "btn" do %i.fa.fa-code-fork Try to Fork again diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index 959d5f08d47..5a6c46f3208 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -18,7 +18,7 @@ = namespace.path - else .thumbnail.fork-thumbnail - = link_to project_fork_path(@project, namespace_id: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do + = link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do = image_tag namespace_icon(namespace, 200) .caption %h4=namespace.human_name diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 9f37a760e61..9383df13305 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -1,5 +1,5 @@ %ul.nav.nav-tabs = nav_link(action: :show) do - = link_to 'Contributors', project_graph_path + = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do - = link_to 'Commits', commits_project_graph_path + = link_to 'Commits', commits_namespace_project_graph_path diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 9a003c87f68..e70cf5c3884 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -7,7 +7,7 @@ %hr.clearfix -= form_for [@project, @hook], as: :hook, url: project_hooks_path(@project), html: { class: 'form-horizontal' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| -if @hook.errors.any? .alert.alert-danger - @hook.errors.full_messages.each do |msg| @@ -58,8 +58,8 @@ - @hooks.each do |hook| %li .pull-right - = link_to 'Test Hook', test_project_hook_path(@project, hook), class: "btn btn-small btn-grouped" - = link_to 'Remove', project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-small btn-grouped" + = link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-small btn-grouped" + = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-small btn-grouped" .clearfix %span.monospace= hook.url %p diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 6c3083e49f5..097374e1128 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -6,7 +6,7 @@ %hr -= form_for @project, url: project_import_path(@project), method: :post, html: { class: 'form-horizontal' } do |f| += form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f| .form-group.import-url-data = f.label :import_url, class: 'control-label' do %span Import existing git repo diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index e04e1985f1f..c7c8af2f2c0 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,9 +1,9 @@ - content_for :note_actions do - if can?(current_user, :modify_issue, @issue) - if @issue.closed? - = link_to 'Reopen Issue', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue' + = link_to 'Reopen Issue', namespace_project_issue_path(@project.namespace, @project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue' - else - = link_to 'Close Issue', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" + = link_to 'Close Issue', namespace_project_issue_path(@project.namespace, @project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" .row .col-md-9 .participants @@ -33,5 +33,5 @@ %h6 Labels .issue-show-labels - @issue.labels.each do |label| - = link_to project_issues_path(@project, label_name: label.name) do + = link_to namespace_project_issues_path(@project.namespace, @project, label_name: label.name) do %p= render_colored_label(label) diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 2a7b44955cd..679e84c3666 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -2,7 +2,7 @@ %h3.page-title= @issue.new_record? ? "New Issue" : "Edit Issue ##{@issue.iid}" %hr - = form_for [@project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| = render 'projects/issuable_form', f: f, issuable: @issue :javascript @@ -11,4 +11,4 @@ e.preventDefault(); }); - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_image_path_upload = "#{upload_image_namespace_project_path @project.namespace, @project}"; diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index dc6510be858..a5f9a5653e3 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,11 +1,11 @@ -%li{ id: dom_id(issue), class: issue_css_classes(issue), url: project_issue_path(issue.project, issue) } +%li{ id: dom_id(issue), class: issue_css_classes(issue), url: namespace_project_issue_path(issue.project.namespace, issue.project, issue) } - if controller.controller_name == 'issues' .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue", disabled: !can?(current_user, :modify_issue, issue) .issue-title %span.str-truncated - = link_to_gfm issue.title, project_issue_path(issue.project, issue), class: "row_title" + = link_to_gfm issue.title, namespace_project_issue_path(issue.project.namespace, issue.project, issue), class: "row_title" - if issue.closed? %small.pull-right CLOSED @@ -33,16 +33,16 @@ .issue-labels - issue.labels.each do |label| - = link_to project_issues_path(issue.project, label_name: label.name) do + = link_to namespace_project_issues_path(issue.project.namespace, issue.project, label_name: label.name) do = render_colored_label(label) .issue-actions - if can? current_user, :modify_issue, issue - if issue.closed? - = link_to 'Reopen', project_issue_path(issue.project, issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-small btn-grouped reopen_issue btn-reopen", remote: true + = link_to 'Reopen', namespace_project_issue_path(issue.project.namespace, issue.project, issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-small btn-grouped reopen_issue btn-reopen", remote: true - else - = link_to 'Close', project_issue_path(issue.project, issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-small btn-grouped close_issue btn-close", remote: true - = link_to edit_project_issue_path(issue.project, issue), class: "btn btn-small edit-issue-link btn-grouped" do + = link_to 'Close', namespace_project_issue_path(issue.project.namespace, issue.project, issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-small btn-grouped close_issue btn-close", remote: true + = link_to edit_namespace_project_issue_path(issue.project.namespace, issue.project, issue), class: "btn btn-small edit-issue-link btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 3daa18ba346..9804658bebe 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @issue], remote: true, html: {class: 'edit-issue inline-update'} do |f| += form_for [@project.namespace.becomes(Namespace), @project, @issue], remote: true, html: {class: 'edit-issue inline-update'} do |f| %div.prepend-top-20 %p Assignee: @@ -13,7 +13,7 @@ %p Milestone: - if issue.milestone - #{link_to @issue.milestone.title, project_milestone_path(@project, @issue.milestone)} + #{link_to @issue.milestone.title, namespace_project_milestone_path(@project.namespace, @project, @issue.milestone)} - else none - if can?(current_user, :modify_issue, @issue) diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 816851a8abe..73ce78133d5 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -5,7 +5,7 @@ .clearfix .issues_bulk_update.hide - = form_tag bulk_update_project_issues_path(@project), method: :post do + = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do = select_tag('update[status]', options_for_select([['Open', 'open'], ['Closed', 'closed']]), prompt: "Status") = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 61e651da932..126f2c07faa 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -1,9 +1,9 @@ xml.instruct! xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.title "#{@project.name} issues" - xml.link :href => project_issues_url(@project, :atom), :rel => "self", :type => "application/atom+xml" - xml.link :href => project_issues_url(@project), :rel => "alternate", :type => "text/html" - xml.id project_issues_url(@project) + xml.link :href => namespace_project_issues_url(@project.namespace, @project, :atom), :rel => "self", :type => "application/atom+xml" + xml.link :href => namespace_project_issues_url(@project.namespace, @project), :rel => "alternate", :type => "text/html" + xml.id namespace_project_issues_url(@project.namespace, @project) xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any? @issues.each do |issue| diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 75411c6d86f..ca38a4e765d 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -10,16 +10,16 @@ .pull-right - if can?(current_user, :write_issue, @project) - = link_to new_project_issue_path(@project), class: "btn btn-grouped new-issue-link", title: "New Issue", id: "new_issue_link" do + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-grouped new-issue-link", title: "New Issue", id: "new_issue_link" do %i.fa.fa-plus New Issue - if can?(current_user, :modify_issue, @issue) - if @issue.closed? - = link_to 'Reopen', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen" + = link_to 'Reopen', namespace_project_issue_path(@project.namespace, @project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen" - else - = link_to 'Close', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue" + = link_to 'Close', namespace_project_issue_path(@project.namespace, @project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue" - = link_to edit_project_issue_path(@project, @issue), class: "btn btn-grouped issuable-edit" do + = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: "btn btn-grouped issuable-edit" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index 7a5e0517556..82c0e653759 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -6,7 +6,7 @@ $('.context').html("#{escape_javascript(render partial: 'issue_context', locals: { issue: @issue })}"); $('.context').effect('highlight'); - if @issue.milestone - $('.milestone-nav-link').replaceWith("| Milestone #{escape_javascript(link_to @issue.milestone.title, project_milestone_path(@issue.project, @issue.milestone))}") + $('.milestone-nav-link').replaceWith("| Milestone #{escape_javascript(link_to @issue.milestone.title, namespace_project_milestone_path(@issue.project.namespace, @issue.project, @issue.milestone))}") - else $('.milestone-nav-link').html('') diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index c7380920b47..95912536e42 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @label], html: { class: 'form-horizontal label-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form' } do |f| -if @label.errors.any? .row .col-sm-10.col-sm-offset-2 @@ -29,5 +29,5 @@ .form-actions = f.submit 'Save', class: 'btn btn-save js-save-button' - = link_to "Cancel", project_labels_path(@project), class: 'btn btn-cancel' + = link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml index 03a8f0921b7..82829452862 100644 --- a/app/views/projects/labels/_label.html.haml +++ b/app/views/projects/labels/_label.html.haml @@ -2,9 +2,9 @@ = render_colored_label(label) .pull-right %strong.append-right-20 - = link_to project_issues_path(@project, label_name: label.name) do + = link_to namespace_project_issues_path(@project.namespace, @project, label_name: label.name) do = pluralize label.open_issues_count, 'open issue' - if can? current_user, :admin_label, @project - = link_to 'Edit', edit_project_label_path(@project, label), class: 'btn' - = link_to 'Remove', project_label_path(@project, label), class: 'btn btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} + = link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn' + = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml index 52435c5d892..e003d1dfe7f 100644 --- a/app/views/projects/labels/edit.html.haml +++ b/app/views/projects/labels/edit.html.haml @@ -2,7 +2,7 @@ Edit label %span.light #{@label.name} .back-link - = link_to project_labels_path(@project) do + = link_to namespace_project_labels_path(@project.namespace, @project) do ← To labels list %hr = render 'form' diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index c7c17c7797e..c53d75b1bb6 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,7 +1,7 @@ = render "projects/issues_nav" - if can? current_user, :admin_label, @project - = link_to new_project_label_path(@project), class: "pull-right btn btn-new" do + = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do New label %h3.page-title Labels @@ -14,4 +14,4 @@ = paginate @labels, theme: 'gitlab' - else .light-well - .nothing-here-block Create first label or #{link_to 'generate', generate_project_labels_path(@project), method: :post} default set of labels + .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml index 850da0b192b..0683ed5d4fb 100644 --- a/app/views/projects/labels/new.html.haml +++ b/app/views/projects/labels/new.html.haml @@ -1,6 +1,6 @@ %h3 New label .back-link - = link_to project_labels_path(@project) do + = link_to namespace_project_labels_path(@project.namespace, @project) do ← To labels list %hr = render 'form' diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index f1f66569a9f..e3d3e69a9d3 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -1,9 +1,9 @@ - content_for :note_actions do - if can?(current_user, :modify_merge_request, @merge_request) - if @merge_request.open? - = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" + = link_to 'Close', namespace_project_merge_request_path(@project.namespace, @project, @merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" - if @merge_request.closed? - = link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" + = link_to 'Reopen', namespace_project_merge_request_path(@project.namespace, @project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" .row .col-md-9 @@ -27,5 +27,5 @@ %h6 Labels .merge-request-show-labels - @merge_request.labels.each do |label| - = link_to project_merge_requests_path(@project, label_name: label.name) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project, label_name: label.name) do %p= render_colored_label(label) diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index d52e64666a0..893c7daf3cf 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form' } do |f| .merge-request-form-info = render 'projects/issuable_form', f: f, issuable: @merge_request @@ -9,4 +9,4 @@ e.preventDefault(); }); - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_image_path_upload = "#{upload_image_namespace_project_path @project.namespace, @project}"; diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml index 35a86e6511c..19e4dab874b 100644 --- a/app/views/projects/merge_requests/_head.html.haml +++ b/app/views/projects/merge_requests/_head.html.haml @@ -1,5 +1,5 @@ .top-tabs - = link_to project_merge_requests_path(@project), class: "tab #{'active' if current_page?(project_merge_requests_path(@project)) }" do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), class: "tab #{'active' if current_page?(namespace_project_merge_requests_path(@project.namespace, @project)) }" do %span Merge Requests diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 5afc87fb6b1..3567401817b 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,6 +1,6 @@ %li{ class: mr_css_classes(merge_request) } .merge-request-title - = link_to_gfm truncate(merge_request.title, length: 80), project_merge_request_path(merge_request.target_project, merge_request), class: "row_title" + = link_to_gfm truncate(merge_request.title, length: 80), namespace_project_merge_request_path(merge_request.target_project.namespace, merge_request.target_project, merge_request), class: "row_title" - if merge_request.merged? %small.pull-right %i.fa.fa-check @@ -38,5 +38,5 @@ .merge-request-labels - merge_request.labels.each do |label| - = link_to project_merge_requests_path(merge_request.project, label_name: label.name) do + = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, label_name: label.name) do = render_colored_label(label) diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 99726172154..17e76059fdb 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -1,7 +1,7 @@ %h3.page-title Compare branches for new Merge Request %hr -= form_for [@project, @merge_request], url: new_project_merge_request_path(@project), method: :get, html: { class: "merge-request-form form-inline" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline" } do |f| .hide.alert.alert-danger.mr-compare-errors .merge-request-branches.row .col-md-6 @@ -60,19 +60,19 @@ , target_branch = $("#merge_request_target_branch") , target_project = $("#merge_request_target_project_id"); - $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: source_branch.val() }); - $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() }); + $.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: source_branch.val() }); + $.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() }); target_project.on("change", function() { - $.get("#{update_branches_project_merge_requests_path(@source_project)}", {target_project_id: $(this).val() }); + $.get("#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: $(this).val() }); }); source_branch.on("change", function() { - $.get("#{branch_from_project_merge_requests_path(@source_project)}", {ref: $(this).val() }); + $.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: $(this).val() }); $(".mr-compare-errors").fadeOut(); $(".mr-compare-btn").enable(); }); target_branch.on("change", function() { - $.get("#{branch_to_project_merge_requests_path(@source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); + $.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: $(this).val() }); $(".mr-compare-errors").fadeOut(); $(".mr-compare-btn").enable(); }); diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index ac374532ffd..2a3fce0df30 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -7,9 +7,9 @@ %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} %span.pull-right - = link_to 'Change branches', new_project_merge_request_path(@project) + = link_to 'Change branches', new_namespace_project_merge_request_path(@project.namespace, @project) -= form_for [@project, @merge_request], html: { class: "merge-request-form form-horizontal gfm-form" } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: "merge-request-form form-horizontal gfm-form" } do |f| .merge-request-form-info .form-group = f.label :title, class: 'control-label' do @@ -54,7 +54,7 @@ %span.light No open milestones available.   - if can? current_user, :admin_milestone, @merge_request.target_project - = link_to 'Create new milestone', new_project_milestone_path(@merge_request.target_project), target: :blank + = link_to 'Create new milestone', new_namespace_project_milestone_path(@merge_request.target_project.namespace, @merge_request.target_project), target: :blank .form-group = f.label :label_ids, class: 'control-label' do %i.fa.fa-tag @@ -66,7 +66,7 @@ %span.light No labels yet.   - if can? current_user, :admin_label, @merge_request.target_project - = link_to 'Create new label', new_project_label_path(@merge_request.target_project), target: :blank + = link_to 'Create new label', new_namespace_project_label_path(@merge_request.target_project.namespace, @merge_request.target_project), target: :blank .form-actions - if contribution_guide_url(@target_project) @@ -113,7 +113,7 @@ e.preventDefault(); }); - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_image_path_upload = "#{upload_image_namespace_project_path @project.namespace, @project}"; :javascript var merge_request diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 8e31a7e3fe4..45c3a419d97 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,4 +1,4 @@ -.merge-request{'data-url' => project_merge_request_path(@project, @merge_request)} +.merge-request{'data-url' => namespace_project_merge_request_path(@project.namespace, @project, @merge_request)} = render "projects/merge_requests/show/mr_title" %hr = render "projects/merge_requests/show/mr_box" @@ -9,7 +9,7 @@ - if @merge_request.for_fork? %strong.label-branch< - if @merge_request.source_project - = link_to @merge_request.source_project_namespace, project_path(@merge_request.source_project) + = link_to @merge_request.source_project_namespace, namespace_project_path(@merge_request.source_project.namespace, @merge_request.source_project) - else \ #{@merge_request.source_project_namespace} \:#{@merge_request.source_branch} @@ -27,8 +27,8 @@ Download as %span.caret %ul.dropdown-menu - %li= link_to "Email Patches", project_merge_request_path(@project, @merge_request, format: :patch) - %li= link_to "Plain Diff", project_merge_request_path(@project, @merge_request, format: :diff) + %li= link_to "Email Patches", namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :patch) + %li= link_to "Plain Diff", namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :diff) = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/show/state_widget" @@ -36,17 +36,17 @@ - if @commits.present? %ul.nav.nav-tabs.merge-request-tabs %li.notes-tab{data: {action: 'notes'}} - = link_to project_merge_request_path(@project, @merge_request) do + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request) do %i.fa.fa-comments Discussion %span.badge= @merge_request.mr_and_commit_notes.count %li.commits-tab{data: {action: 'commits'}} - = link_to project_merge_request_path(@project, @merge_request), title: 'Commits' do + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), title: 'Commits' do %i.fa.fa-history Commits %span.badge= @commits.size %li.diffs-tab{data: {action: 'diffs'}} - = link_to diffs_project_merge_request_path(@project, @merge_request) do + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) do %i.fa.fa-list-alt Changes %span.badge= @merge_request.diffs.size @@ -67,9 +67,9 @@ var merge_request; merge_request = new MergeRequest({ - url_to_automerge_check: "#{automerge_check_project_merge_request_path(@project, @merge_request)}", + url_to_automerge_check: "#{automerge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{ci_status_project_merge_request_path(@project, @merge_request)}", + url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_enable: #{@project.ci_service ? "true" : "false"}, current_status: "#{@merge_request.merge_status_name}", action: "#{controller.action_name}" diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index 21718ca2acf..ddf21f75063 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update'} do |f| += form_for [@project.namespace.becomes(Namespace), @project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update'} do |f| %div.prepend-top-20 %p Assignee: @@ -14,7 +14,7 @@ Milestone: - if @merge_request.milestone %span.back-to-milestone - #{link_to @merge_request.milestone.title, project_milestone_path(@project, @merge_request.milestone)} + #{link_to @merge_request.milestone.title, namespace_project_milestone_path(@project.namespace, @project, @merge_request.milestone)} - else none - if can?(current_user, :modify_merge_request, @merge_request) diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index f8ee6973637..12ab184973c 100644 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -12,7 +12,7 @@ - if @show_merge_controls .automerge_widget.can_be_merged.hide .clearfix - = form_for [:automerge, @project, @merge_request], remote: true, method: :post do |f| + = form_for [:automerge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post do |f| .accept-merge-holder.clearfix.js-toggle-container .accept-action = f.submit "Accept Merge Request", class: "btn btn-create accept_merge_request" diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 0f20eba382c..4c230953cb3 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -14,9 +14,9 @@ .issue-btn-group.pull-right - if can?(current_user, :modify_merge_request, @merge_request) - if @merge_request.open? - = link_to 'Close', project_merge_request_path(@project, @merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request" - = link_to edit_project_merge_request_path(@project, @merge_request), class: "btn btn-grouped issuable-edit", id: "edit_merge_request" do + = link_to 'Close', namespace_project_merge_request_path(@project.namespace, @project, @merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request" + = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn btn-grouped issuable-edit", id: "edit_merge_request" do %i.fa.fa-pencil-square-o Edit - if @merge_request.closed? - = link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link", title: "Close merge request" + = link_to 'Reopen', namespace_project_merge_request_path(@project.namespace, @project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link", title: "Close merge request" diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 0f51a347f01..46132eed0b9 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,11 +1,11 @@ %h3.page-title= @milestone.new_record? ? "New Milestone" : "Edit Milestone ##{@milestone.iid}" .back-link - = link_to project_milestones_path(@project) do + = link_to namespace_project_milestones_path(@project.namespace, @project) do ← To milestones %hr -= form_for [@project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form'} do |f| += form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form'} do |f| -if @milestone.errors.any? .alert.alert-danger %ul @@ -38,10 +38,10 @@ .form-actions - if @milestone.new_record? = f.submit 'Create milestone', class: "btn-create btn" - = link_to "Cancel", project_milestones_path(@project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_milestones_path(@project.namespace, @project), class: "btn btn-cancel" -else = f.submit 'Save changes', class: "btn-save btn" - = link_to "Cancel", project_milestone_path(@project, @milestone), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-cancel" :javascript @@ -51,4 +51,4 @@ onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_image_path_upload = "#{upload_image_namespace_project_path @project.namespace, @project}"; diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml index b5ec0fc9882..36463371f4f 100644 --- a/app/views/projects/milestones/_issue.html.haml +++ b/app/views/projects/milestones/_issue.html.haml @@ -1,8 +1,8 @@ -%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => project_issue_path(@project, issue) } +%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => namespace_project_issue_path(@project.namespace, @project, issue) } %span.str-truncated - = link_to [@project, issue] do + = link_to [@project.namespace.becomes(Namespace), @project, issue] do %span.cgray ##{issue.iid} - = link_to_gfm issue.title, [@project, issue], title: issue.title + = link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title .pull-right.assignee-icon - if issue.assignee = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16" diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml index d54cb3f8e74..3180c1d91b9 100644 --- a/app/views/projects/milestones/_merge_request.html.haml +++ b/app/views/projects/milestones/_merge_request.html.haml @@ -1,5 +1,5 @@ -%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => project_merge_request_path(@project, merge_request) } +%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => namespace_project_merge_request_path(@project.namespace, @project, merge_request) } %span.str-truncated - = link_to [@project, merge_request] do + = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do %span.cgray ##{merge_request.iid} - = link_to_gfm merge_request.title, [@project, merge_request], title: merge_request.title + = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 1002b9513ff..d32b2ba271f 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -1,12 +1,12 @@ %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) } .pull-right - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-small edit-milestone-link btn-grouped" do + = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-small edit-milestone-link btn-grouped" do %i.fa.fa-pencil-square-o Edit - = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-small btn-close" + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-small btn-close" %h4 - = link_to_gfm truncate(milestone.title, length: 100), project_milestone_path(milestone.project, milestone) + = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - if milestone.expired? and not milestone.closed? %span.cred (Expired) %small @@ -16,10 +16,10 @@ - else %div %div - = link_to project_issues_path(milestone.project, milestone_id: milestone.id) do + = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do = pluralize milestone.issues.count, 'Issue'   - = link_to project_merge_requests_path(milestone.project, milestone_id: milestone.id) do + = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do = pluralize milestone.merge_requests.count, 'Merge Request'   %span.light #{milestone.percent_complete}% complete diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 04a1b9243d5..084c6d010da 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -3,7 +3,7 @@ %h3.page-title Milestones - if can? current_user, :admin_milestone, @project - = link_to new_project_milestone_path(@project), class: "pull-right btn btn-new", title: "New Milestone" do + = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do %i.fa.fa-plus New Milestone diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 031b5a31895..3107766e993 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -12,13 +12,13 @@ = @milestone.expires_at .pull-right - if can?(current_user, :admin_milestone, @project) - = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped" do + = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit - if @milestone.active? - = link_to 'Close Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped" - else - = link_to 'Reopen Milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" + = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped" %hr - if @milestone.issues.any? && @milestone.can_be_closed? @@ -63,10 +63,10 @@ %span.badge= @users.count .pull-right - = link_to new_project_issue_path(@project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do %i.fa.fa-plus New Issue - = link_to 'Browse Issues', project_issues_path(@milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped" + = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped" .tab-content .tab-pane.active#tab-issues diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 4a21b84fb85..c36bad1e94b 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,7 +1,7 @@ = render "head" .project-network .controls - = form_tag project_network_path(@project, @id), method: :get, class: 'form-inline network-form' do |f| + = form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f| = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha' = button_tag class: 'btn btn-success btn-search-sha' do %i.fa.fa-search @@ -18,8 +18,8 @@ disableButtonIfEmptyField('#extended_sha1', '.btn-search-sha') network_graph = new Network({ - url: '#{project_network_path(@project, @ref, @options.merge(format: :json))}', - commit_url: '#{project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")}', + url: '#{namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))}', + commit_url: '#{namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")}', ref: '#{@ref}', commit_id: '#{@commit.id}' }) diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml index dd576243510..e8fd90efd1f 100644 --- a/app/views/projects/no_repo.html.haml +++ b/app/views/projects/no_repo.html.haml @@ -9,14 +9,14 @@ %hr .no-repo-actions - = link_to project_repository_path(@project), method: :post, class: 'btn btn-primary' do + = link_to namespace_project_repository_path(@project.namespace, @project), method: :post, class: 'btn btn-primary' do Create empty bare repository %strong.prepend-left-10.append-right-10 or - = link_to new_project_import_path(@project), class: 'btn' do + = link_to new_namespace_project_import_path(@project.namespace, @project), class: 'btn' do Import repository - if can? current_user, :remove_project, @project .prepend-top-20 - = link_to 'Remove project', @project, data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + = link_to 'Remove project', namespace_project_path(@project.namespace, @project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 59e2b3f1b0b..9fda7aafd51 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,5 +1,5 @@ .note-edit-form - = form_for note, url: project_note_path(@project, note), method: :put, remote: true, authenticity_token: true do |f| + = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f| = render layout: 'projects/md_preview' do = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 3879a0f10da..28c11aa5548 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f| += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f| = note_target_fields = f.hidden_field :commit_id = f.hidden_field :line_code @@ -29,4 +29,4 @@ = f.file_field :attachment, class: "js-note-attachment-input hidden" :javascript - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_image_path_upload = "#{upload_image_namespace_project_path @project.namespace, @project}"; diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 88c7b7ccf1a..0be3ded6df3 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -17,7 +17,7 @@ %i.fa.fa-pencil-square-o Edit   - = link_to project_note_path(@project, note), title: "Remove comment", method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: "danger js-note-delete" do + = link_to namespace_project_note_path(@project.namespace, @project, note), title: "Remove comment", method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: "danger js-note-delete" do %i.fa.fa-trash-o.cred Remove - if note.system @@ -63,7 +63,7 @@ = link_to note.attachment.secure_url, target: "_blank" do %i.fa.fa-paperclip = note.attachment_identifier - = link_to delete_attachment_project_note_path(@project, note), + = link_to delete_attachment_namespace_project_note_path(@project.namespace, @project, note), title: "Delete this attachment", method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: "danger js-note-attachment-delete" do %i.fa.fa-trash-o.cred .clear diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 04ee17a40a0..813e37276bd 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -7,4 +7,4 @@ = render "projects/notes/form" :javascript - new Notes("#{project_notes_path(target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}) + new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}) diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml index 52c06ec172d..7c6f7243173 100644 --- a/app/views/projects/notes/discussions/_active.html.haml +++ b/app/views/projects/notes/discussions/_active.html.haml @@ -8,7 +8,7 @@ %div = link_to_member(@project, note.author, avatar: false) started a discussion - = link_to diffs_project_merge_request_path(note.project, note.noteable, anchor: note.line_code) do + = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do %strong on the diff .last-update.hide.js-toggle-content - last_note = discussion_notes.last diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml index 94f16a5f02e..62609cfc1c8 100644 --- a/app/views/projects/notes/discussions/_commit.html.haml +++ b/app/views/projects/notes/discussions/_commit.html.haml @@ -8,7 +8,7 @@ %div = link_to_member(@project, note.author, avatar: false) started a discussion on commit - = link_to(note.noteable.short_id, project_commit_path(note.project, note.noteable), class: 'monospace') + = link_to(note.noteable.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') .last-update.hide.js-toggle-content - last_note = discussion_notes.last last updated by diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index e422799f55c..5406b80dc16 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -11,10 +11,10 @@ %tbody - @branches.each do |branch| - - @url = project_protected_branch_path(@project, branch) + - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch) %tr %td - = link_to project_commits_path(@project, branch.name) do + = link_to namespace_project_commits_path(@project.namespace, @project, branch.name) do %strong= branch.name - if @project.root_ref?(branch.name) %span.label.label-info default @@ -22,7 +22,7 @@ = check_box_tag "developers_can_push", branch.id, branch.developers_can_push, "data-url" => @url %td - if commit = branch.commit - = link_to project_commit_path(@project, commit.id), class: 'commit_short_id' do + = link_to namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id' do = commit.short_id · #{time_ago_with_tooltip(commit.committed_date)} @@ -31,4 +31,4 @@ %td .pull-right - if can? current_user, :admin_project, @project - = link_to 'Unprotect', [@project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-small" + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-small" diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 2164c874c74..dc20e96732e 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -11,7 +11,7 @@ %p Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"} - if can? current_user, :admin_project, @project - = form_for [@project, @protected_branch], html: { class: 'form-horizontal' } do |f| + = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'form-horizontal' } do |f| -if @protected_branch.errors.any? .alert.alert-danger %ul diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 948a21aa816..49ce6c0888e 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -11,9 +11,9 @@ - if @logs.present? :plain var current_url = location.href.replace(/\/?$/, '/'); - var log_url = '#{project_tree_url(@project, tree_join(@ref, @path || '/'))}'.replace(/\/?$/, '/'); + var log_url = '#{namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))}'.replace(/\/?$/, '/'); if(current_url == log_url) { // Load 10 more commit log for each file in tree // if we still on the same page - ajaxGet('#{logs_file_project_ref_path(@project, @ref, @path || '/', offset: (@offset + @limit))}'); + ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '/', offset: (@offset + @limit))}'); } diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index ce69adeb48c..26669fb00a9 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,7 +3,7 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do %i.fa.fa-download %span Download zip %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } @@ -12,26 +12,26 @@ Select Archive Format %ul.dropdown-menu{ role: 'menu' } %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do %i.fa.fa-download %span Download zip %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do %i.fa.fa-download %span Download tar.gz %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do %i.fa.fa-download %span Download tar.bz2 %li - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar'), rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do %i.fa.fa-download %span Download tar - else %span.btn-group{class: btn_class} - = link_to archive_project_repository_path(@project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do %i.fa.fa-download %span zip - = link_to archive_project_repository_path(@project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do %i.fa.fa-download %span tar.gz diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml index c77ffff43fe..f3526ad0747 100644 --- a/app/views/projects/repositories/_feed.html.haml +++ b/app/views/projects/repositories/_feed.html.haml @@ -1,7 +1,7 @@ - commit = update %tr %td - = link_to project_commits_path(@project, commit.head.name) do + = link_to namespace_project_commits_path(@project.namespace, @project, commit.head.name) do %strong = commit.head.name - if @project.root_ref?(commit.head.name) @@ -9,7 +9,7 @@ %td %div - = link_to project_commits_path(@project, commit.id) do + = link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do %code= commit.short_id = image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: '' = gfm escape_once(truncate(commit.title, length: 40)) diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index ba270880881..8db6d67e06b 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -5,12 +5,12 @@ %p= @service.description .back-link - = link_to project_services_path(@project) do + = link_to namespace_project_services_path(@project.namespace, @project) do ← to services %hr -= form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| += form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f| - if @service.errors.any? .alert.alert-danger %ul @@ -53,4 +53,4 @@ = f.submit 'Save', class: 'btn btn-save'   - if @service.valid? && @service.activated? && @service.can_test? - = link_to 'Test settings', test_project_service_path(@project, @service.to_param), class: 'btn' + = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: 'btn' diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml index 4604c0afd8d..d615d128653 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/index.html.haml @@ -13,7 +13,7 @@ %td = boolean_to_icon service.activated? %td - = link_to edit_project_service_path(@project, service.to_param) do + = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do %strong= service.title %td = service.description diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 435b2648404..abebbc7049d 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -15,9 +15,9 @@ Readme .project-home-links - unless @project.empty_repo? - = link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), project_commits_path(@project, @ref || @repository.root_ref) - = link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), project_branches_path(@project) - = link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), project_tags_path(@project) + = link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) + = link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), namespace_project_branches_path(@project.namespace, @project) + = link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), namespace_project_tags_path(@project.namespace, @project) %span.light.prepend-left-20= repository_size .tab-content @@ -42,15 +42,15 @@ %i.fa.fa-code-fork.project-fork-icon Forked from: %br - = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) + = link_to @project.forked_from_project.name_with_namespace, namespace_project_path(@project.namespace, @project.forked_from_project) - unless @project.empty_repo? - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do Compare code - if @repository.version - version = @repository.version - = link_to project_blob_path(@project, tree_join(@repository.root_ref, version.name)), class: 'btn btn-block' do + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, version.name)), class: 'btn btn-block' do Version: %span.count = @repository.blob_by_oid(version.id).data @@ -78,7 +78,7 @@ - if readme .tab-pane#tab-readme %article.readme-holder#README - = link_to project_blob_path(@project, tree_join(@repository.root_ref, readme.name)) do + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do %h4.readme-file-title %i.fa.fa-file = readme.name diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index f6a5bf9e4ff..2d4d5d030ab 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -1,4 +1,4 @@ %h3.page-title Edit snippet %hr -= render "shared/snippets/form", url: project_snippet_path(@project, @snippet) += render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet) diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index e60f9a44322..e2d8ec673a1 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,7 +1,7 @@ %h3.page-title Snippets - if can? current_user, :write_project_snippet, @project - = link_to new_project_snippet_path(@project), class: "btn btn-new pull-right", title: "New Snippet" do + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Snippet" do Add new snippet %p.light diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index 10f684b6316..bb659dba0cf 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -1,4 +1,4 @@ %h3.page-title New snippet %hr -= render "shared/snippets/form", url: project_snippets_path(@project, @snippet) += render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet) diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index ada0d30c496..345848fa6d1 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -2,7 +2,7 @@ = @snippet.title .pull-right - = link_to new_project_snippet_path(@project), class: "btn btn-new", title: "New Snippet" do + = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do Add new snippet %hr @@ -17,7 +17,7 @@ = @snippet.author_name .back-link - = link_to project_snippets_path(@project) do + = link_to namespace_project_snippets_path(@project.namespace, @project) do ← project snippets .file-holder @@ -28,10 +28,10 @@ .options .btn-group - if can?(current_user, :modify_project_snippet, @snippet) - = link_to "edit", edit_project_snippet_path(@project, @snippet), class: "btn btn-small", title: 'Edit Snippet' - = link_to "raw", raw_project_snippet_path(@project, @snippet), class: "btn btn-small", target: "_blank" + = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-small", title: 'Edit Snippet' + = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-small", target: "_blank" - if can?(current_user, :admin_project_snippet, @snippet) - = link_to "remove", project_snippet_path(@project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-small btn-remove", title: 'Delete Snippet' + = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-small btn-remove", title: 'Delete Snippet' = render 'shared/snippets/blob' %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 4ab102ba96c..8da07222cba 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -1,7 +1,7 @@ - commit = @repository.commit(tag.target) %li %h4 - = link_to project_commits_path(@project, tag.name), class: "" do + = link_to namespace_project_commits_path(@project.namespace, @project, tag.name), class: "" do %i.fa.fa-tag = tag.name - if tag.message.present? @@ -11,7 +11,7 @@ - if can? current_user, :download_code, @project = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-small' - if can?(current_user, :admin_project, @project) - = link_to project_tag_path(@project, tag.name), class: 'btn btn-small btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-small btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do %i.fa.fa-trash-o - if commit diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index ac74e3b6d36..f1bc2bc9a2b 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -4,7 +4,7 @@ Git Tags - if can? current_user, :push_code, @project .pull-right - = link_to new_project_tag_path(@project), class: 'btn btn-create new-tag-btn' do + = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do %i.fa.fa-add-sign New tag diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 289c52a2e3f..655044438d5 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -5,7 +5,7 @@ %h3.page-title %i.fa.fa-code-fork New tag -= form_tag project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal" do += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal" do .form-group = label_tag :tag_name, 'Name for new tag', class: 'control-label' .col-sm-10 @@ -22,7 +22,7 @@ .light (Optional) Entering a message will create an annotated tag. .form-actions = button_tag 'Create tag', class: 'btn btn-create', tabindex: 3 - = link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel' + = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript disableButtonIfAnyEmptyField($("#new-tag-form"), ".form-control", ".btn-create"); diff --git a/app/views/projects/team_members/_form.html.haml b/app/views/projects/team_members/_form.html.haml index ddf8cb76f78..166b6362a07 100644 --- a/app/views/projects/team_members/_form.html.haml +++ b/app/views/projects/team_members/_form.html.haml @@ -1,7 +1,7 @@ %h3.page-title New project member(s) -= form_for @user_project_relation, as: :project_member, url: project_team_members_path(@project), html: { class: "form-horizontal users-project-form" } do |f| += form_for @user_project_relation, as: :project_member, url: namespace_project_team_members_path(@project.namespace, @project), html: { class: "form-horizontal users-project-form" } do |f| -if @user_project_relation.errors.any? .alert.alert-danger %ul @@ -26,4 +26,4 @@ .form-actions = f.submit 'Add users', class: "btn btn-create" - = link_to "Cancel", project_team_index_path(@project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml index 7a9c0939ba0..61c50af31bf 100644 --- a/app/views/projects/team_members/_team_member.html.haml +++ b/app/views/projects/team_members/_team_member.html.haml @@ -4,10 +4,10 @@ - if current_user_can_admin_project - unless @project.personal? && user == current_user .pull-left - = form_for(member, as: :project_member, url: project_team_member_path(@project, member.user)) do |f| + = form_for(member, as: :project_member, url: namespace_project_team_member_path(@project.namespace, @project, member.user)) do |f| = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: "trigger-submit"   - = link_to project_team_member_path(@project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from team' do + = link_to namespace_project_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from team' do %i.fa.fa-minus.fa-inverse = image_tag avatar_icon(user.email, 32), class: "avatar s32" %p diff --git a/app/views/projects/team_members/import.html.haml b/app/views/projects/team_members/import.html.haml index d1f46c61b2e..9e31d47117e 100644 --- a/app/views/projects/team_members/import.html.haml +++ b/app/views/projects/team_members/import.html.haml @@ -3,12 +3,12 @@ %p.light Only project members will be imported. Group members will be skipped. %hr -= form_tag apply_import_project_team_members_path(@project), method: 'post', class: 'form-horizontal' do += form_tag apply_import_namespace_project_team_members_path(@project.namespace, @project), method: 'post', class: 'form-horizontal' do .form-group = label_tag :source_project_id, "Project", class: 'control-label' .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions = button_tag 'Import project members', class: "btn btn-create" - = link_to "Cancel", project_team_index_path(@project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/team_members/index.html.haml b/app/views/projects/team_members/index.html.haml index ecb7c689e8a..fcc879a58df 100644 --- a/app/views/projects/team_members/index.html.haml +++ b/app/views/projects/team_members/index.html.haml @@ -3,9 +3,9 @@ - if can? current_user, :admin_team_member, @project %span.pull-right - = link_to new_project_team_member_path(@project), class: "btn btn-new btn-grouped", title: "New project member" do + = link_to new_namespace_project_team_member_path(@project.namespace, @project), class: "btn btn-new btn-grouped", title: "New project member" do New project member - = link_to import_project_team_members_path(@project), class: "btn btn-grouped", title: "Import members from another project" do + = link_to import_namespace_project_team_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do Import members %p.light diff --git a/app/views/projects/transfer.js.haml b/app/views/projects/transfer.js.haml index 6d083c5c516..17b9fecfeb1 100644 --- a/app/views/projects/transfer.js.haml +++ b/app/views/projects/transfer.js.haml @@ -1,2 +1,2 @@ :plain - location.href = "#{edit_project_path(@project)}"; + location.href = "#{edit_namespace_project_path(@project.namespace, @project)}"; diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index 393ef0e24bd..b253fe896e3 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -2,7 +2,7 @@ %td.tree-item-file-name = tree_icon(type) %span.str-truncated - = link_to blob_item.name, project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)) + = link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)) %td.tree_time_ago.cgray = render 'spinner' %td.hidden-xs.tree_commit diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index f902440b3f1..d304690d162 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -1,16 +1,16 @@ %ul.breadcrumb.repo-breadcrumb %li - = link_to project_tree_path(@project, @ref) do + = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do = @project.path - tree_breadcrumbs(tree, 6) do |title, path| %li - if path - = link_to truncate(title, length: 40), project_tree_path(@project, path) + = link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path) - else = link_to title, '#' - if current_user && can_push_branch?(@project, @ref) %li - = link_to project_new_blob_path(@project, @id), title: 'New file', id: 'new-file-link' do + = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'New file', id: 'new-file-link' do %small %i.fa.fa-plus @@ -27,15 +27,15 @@ %i.fa.fa-angle-right   %small.light - = link_to @commit.short_id, project_commit_path(@project, @commit) + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit) – = truncate(@commit.title, length: 50) - = link_to 'History', project_commits_path(@project, @id), class: 'pull-right' + = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right' - if @path.present? %tr.tree-item %td.tree-item-file-name - = link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10' + = link_to "..", namespace_project_tree_path(@project.namespace, @project, up_dir_path), class: 'prepend-left-10' %td %td.hidden-xs diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index bd50dd4d9a2..50521264a61 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,3 +1,3 @@ %span.str-truncated %span.tree_author= commit_author_link(commit, avatar: true, size: 16) - = link_to_gfm commit.title, project_commit_path(@project, commit.id), class: "tree-commit-link" + = link_to_gfm commit.title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link" diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 5adbf93ff8f..94342bc9b2b 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -3,7 +3,7 @@ = tree_icon(type) %span.str-truncated - path = flatten_tree(tree_item) - = link_to path, project_tree_path(@project, tree_join(@id || @commit.id, path)) + = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)) %td.tree_time_ago.cgray = render 'spinner' %td.hidden-xs.tree_commit diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index cbb21f2b9fb..4f3f4cab8d5 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -1,6 +1,6 @@ - if @project.valid? :plain - location.href = "#{edit_project_path(@project)}"; + location.href = "#{edit_namespace_project_path(@project.namespace, @project)}"; - else :plain $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 111484c8316..1e1400058e1 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form' } do |f| -if @page.errors.any? #error_explanation .alert.alert-danger @@ -37,11 +37,11 @@ .form-actions - if @page && @page.persisted? = f.submit 'Save changes', class: "btn-save btn" - = link_to "Cancel", project_wiki_path(@project, @page), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel" - else = f.submit 'Create page', class: "btn-create btn" - = link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel" :javascript - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_image_path_upload = "#{upload_image_namespace_project_path @project.namespace, @project}"; diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 30410bc95e0..633214a4e86 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,8 +1,8 @@ %span.pull-right - if (@page && @page.persisted?) - = link_to history_project_wiki_path(@project, @page), class: "btn btn-grouped" do + = link_to history_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do Page History - if can?(current_user, :write_wiki, @project) - = link_to edit_project_wiki_path(@project, @page), class: "btn btn-grouped" do + = link_to edit_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml index 90539fde583..693c3facb32 100644 --- a/app/views/projects/wikis/_nav.html.haml +++ b/app/views/projects/wikis/_nav.html.haml @@ -1,12 +1,12 @@ %ul.nav.nav-tabs = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do - = link_to 'Home', project_wiki_path(@project, :home) + = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) = nav_link(path: 'wikis#pages') do - = link_to 'Pages', pages_project_wikis_path(@project) + = link_to 'Pages', pages_namespace_project_wikis_path(@project.namespace, @project) = nav_link(path: 'wikis#git_access') do - = link_to git_access_project_wikis_path(@project) do + = link_to git_access_namespace_project_wikis_path(@project.namespace, @project) do %i.fa.fa-download Git Access diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 1ce292a02df..6834969de8b 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -7,7 +7,7 @@ .modal-body = label_tag :new_wiki_path do %span Page slug - = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project) + = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project) %p.hint Please don't use spaces. .modal-footer diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 5347caf000a..5567f1af22a 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -9,5 +9,5 @@ .pull-right - if @page.persisted? && can?(current_user, :admin_wiki, @project) - = link_to project_wiki_path(@project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-small btn-remove" do + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { 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/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index 9c9a9933dcf..91291f753f7 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,7 +1,7 @@ = render 'nav' %h3.page-title %span.light History for - = link_to @page.title, project_wiki_path(@project, @page) + = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page) %table.table %thead diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 264b48ec36c..ee233d9086f 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -5,7 +5,7 @@ - @wiki_pages.each do |wiki_page| %li %h4 - = link_to wiki_page.title, project_wiki_path(@project, wiki_page) + = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page) %small (#{wiki_page.format}) .pull-right %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)} diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index ede4fef9e24..a6263e93f67 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -5,7 +5,7 @@ - if @page.historical? .warning_message This is an old version of this page. - You can view the #{link_to "most recent version", project_wiki_path(@project, @page)} or browse the #{link_to "history", history_project_wiki_path(@project, @page)}. + You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", history_namespace_project_wiki_path(@project.namespace, @project, @page)}. %hr diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 58bcff9dbe3..796dd752a4c 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -2,7 +2,7 @@ #{@search_results.total_count} results found - unless @show_snippets - if @project - for #{link_to @project.name_with_namespace, @project} + for #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]} - elsif @group for #{link_to @group.name, @group} diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index dae641dab4f..84e9be82c44 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,7 +1,7 @@ .blob-result .file-holder .file-title - = link_to project_blob_path(@project, tree_join(blob.ref, blob.filename), :anchor => "L" + blob.startline.to_s) do + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename), :anchor => "L" + blob.startline.to_s) do %i.fa.fa-file %strong = blob.filename diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index 7868f958261..ce8ddff9556 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -1,6 +1,6 @@ .search-result-row %h4 - = link_to [issue.project, issue] do + = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do %span.term.str-truncated= issue.title .pull-right ##{issue.iid} - if issue.description.present? diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 56b185283bd..2efa616d664 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -1,6 +1,6 @@ .search-result-row %h4 - = link_to [merge_request.target_project, merge_request] do + = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title .pull-right ##{merge_request.iid} - if merge_request.description.present? diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index a44a4542df5..5fcba2b7e93 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -9,7 +9,7 @@ = link_to project do = project.name_with_namespace · - = link_to project_commit_path(project, note.commit_id, anchor: dom_id(note)) do + = link_to namespace_project_commit_path(project.namespace, project, note.commit_id, anchor: dom_id(note)) do Commit #{truncate_sha(note.commit_id)} - else = link_to project do @@ -17,7 +17,7 @@ · %span #{note.noteable_type.titleize} ##{note.noteable.iid} · - = link_to [project, note.noteable, anchor: dom_id(note)] do + = link_to [project.namespace.becomes(Namespace), project, note.noteable, anchor: dom_id(note)] do = note.noteable.title .note-search-result diff --git a/app/views/search/results/_project.html.haml b/app/views/search/results/_project.html.haml index 301b65eca29..195cf06c8ea 100644 --- a/app/views/search/results/_project.html.haml +++ b/app/views/search/results/_project.html.haml @@ -1,6 +1,6 @@ .search-result-row %h4 - = link_to project do + = link_to [project.namespace.becomes(Namespace), project] do %span.term= project.name_with_namespace - if project.description.present? %span.light.term= project.description diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index c7bc596eb14..f9c5810e3d0 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -1,7 +1,7 @@ .blob-result .file-holder .file-title - = link_to project_wiki_path(@project, wiki_blob.filename) do + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.filename) do %i.fa.fa-file %strong = wiki_blob.filename diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml index cd97481bb6c..0e094d8844b 100644 --- a/app/views/shared/_issuable_filter.html.haml +++ b/app/views/shared/_issuable_filter.html.haml @@ -104,7 +104,7 @@ = render_colored_label(label) - else %li - = link_to generate_project_labels_path(@project, redirect: request.original_url), method: :post do + = link_to generate_namespace_project_labels_path(@project.namespace, @project, redirect: request.original_url), method: :post do %i.fa.fa-plus-circle Create default labels diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml index e976f897dc9..0dbb6a04393 100644 --- a/app/views/shared/_issues.html.haml +++ b/app/views/shared/_issues.html.haml @@ -4,7 +4,7 @@ - project = group[0] .panel-heading = link_to_project project - = link_to 'show all', project_issues_path(project), class: 'pull-right' + = link_to 'show all', namespace_project_issues_path(project.namespace, project), class: 'pull-right' %ul.well-list.issues-list - group[1].each do |issue| diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index 39a1ee38f8e..c02c5af008a 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -4,7 +4,7 @@ - project = group[0] .panel-heading = link_to_project project - = link_to 'show all', project_merge_requests_path(project), class: 'pull-right' + = link_to 'show all', namespace_project_merge_requests_path(project.namespace, project), class: 'pull-right' %ul.well-list.mr-list - group[1].each do |merge_request| = render 'projects/merge_requests/merge_request', merge_request: merge_request diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 4d9534f49b1..eb2e1919e19 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,4 +1,4 @@ -= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do += form_tag switch_namespace_project_refs_path(@project.namespace, @project), method: :get, class: "project-refs-form" do = select_tag "ref", grouped_options_refs, class: "project-refs-select select2 select2-sm" = hidden_field_tag :destination, destination - if defined?(path) diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index f729f129e45..4e0663ea208 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -30,7 +30,7 @@ = f.submit 'Save', class: "btn-save btn" - if @snippet.respond_to?(:project) - = link_to "Cancel", project_snippets_path(@project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" diff --git a/config/routes.rb b/config/routes.rb index 65786d83566..934653a2245 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -125,9 +125,17 @@ Gitlab::Application.routes.draw do resource :logs, only: [:show] resource :background_jobs, controller: 'background_jobs', only: [:show] - resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, only: [:index, :show] do - member do - put :transfer + resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do + root to: 'projects#index', as: :projects + + resources(:projects, path: '/', + constraints: { id: /[a-zA-Z.0-9_\-]+/ }, + only: [:index, :show]) do + root to: 'projects#show' + + member do + put :transfer + end end end @@ -212,167 +220,203 @@ Gitlab::Application.routes.draw do devise_scope :user do get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error end + + root to: "dashboard#show" + # # Project Area # - resources :projects, constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/ }, except: [:new, :create, :index], path: '/' do - member do - put :transfer - post :archive - post :unarchive - post :upload_image - post :toggle_star - post :markdown_preview - get :autocomplete_sources - end - - scope module: :projects do - # Blob routes: - get '/new/:id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' - post '/create/:id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' - get '/edit/:id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob' - put '/update/:id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob' - post '/preview/:id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' - - resources :blob, only: [:show, :destroy], constraints: { id: /.+/, format: false } do - get :diff, on: :member + resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do + resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+/ }, except: + [:new, :create, :index], path: "/") do + member do + put :transfer + post :archive + post :unarchive + post :upload_image + post :toggle_star + post :markdown_preview + get :autocomplete_sources end - resources :raw, only: [:show], constraints: { id: /.+/ } - resources :tree, only: [:show], constraints: { id: /.+/, format: /(html|js)/ } - resource :avatar, only: [:show, :destroy] - - resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do - get :branches, on: :member - end + scope module: :projects do + # Blob routes: + get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob' + post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob' + get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob' + put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob' + post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' + + scope do + get('/blob/*id/diff', to: 'blob#diff', + constraints: { id: /.+/, format: false }, + as: :blob_diff) + get('/blob/*id', to: 'blob#show', + constraints: { id: /.+/, format: false }, as: :blob) + delete('/blob/*id', to: 'blob#destroy', + constraints: { id: /.+/, format: false }) + end - resources :commits, only: [:show], constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } - resources :compare, only: [:index, :create] - resources :blame, only: [:show], constraints: { id: /.+/ } - resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } - resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do - member do - get :commits + scope do + get( + '/raw/*id', + to: 'raw#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :raw + ) end - end - get '/compare/:from...:to' => 'compare#show', :as => 'compare', - :constraints => { from: /.+/, to: /.+/ } + scope do + get( + '/tree/*id', + to: 'tree#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :tree + ) + end + resource :avatar, only: [:show, :destroy] - resources :snippets, constraints: { id: /\d+/ } do - member do - get 'raw' + resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do + get :branches, on: :member end - end - resources :wikis, only: [:show, :edit, :destroy, :create], constraints: { id: /[a-zA-Z.0-9_\-\/]+/ } do - collection do - get :pages - put ':id' => 'wikis#update' - get :git_access + resources :commits, only: [:show], constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } + resources :compare, only: [:index, :create] + + scope do + get( + '/blame/*id', + to: 'blame#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :blame + ) end - member do - get 'history' + resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } + resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do + member do + get :commits + end end - end - resource :fork, only: [:new, :create] - resource :import, only: [:new, :create, :show] + get '/compare/:from...:to' => 'compare#show', :as => 'compare', + :constraints => { from: /.+/, to: /.+/ } - resource :repository, only: [:show, :create] do - member do - get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } + resources :snippets, constraints: { id: /\d+/ } do + member do + get 'raw' + end end - end - resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do - member do - get :test - end - end + resources :wikis, only: [:show, :edit, :destroy, :create], constraints: { id: /[a-zA-Z.0-9_\-\/]+/ } do + collection do + get :pages + put ':id' => 'wikis#update' + get :git_access + end - resources :deploy_keys, constraints: { id: /\d+/ } do - member do - put :enable - put :disable + member do + get 'history' + end end - end - resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } - resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resource :repository, only: [:show, :create] do + member do + get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } + end + end - resources :refs, only: [] do - collection do - get 'switch' + resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do + member do + get :test + end end - member do - # tree viewer logs - get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } - get 'logs_tree/:path' => 'refs#logs_tree', as: :logs_file, constraints: { - id: Gitlab::Regex.git_reference_regex, - path: /.*/ - } + resources :deploy_keys, constraints: { id: /\d+/ } do + member do + put :enable + put :disable + end end - end - resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do - member do - get :diffs - post :automerge - get :automerge_check - get :ci_status + resource :fork, only: [:new, :create] + resource :import, only: [:new, :create, :show] + + resources :refs, only: [] do + collection do + get 'switch' + end + + member do + # tree viewer logs + get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } + get 'logs_tree/:path' => 'refs#logs_tree', as: :logs_file, constraints: { + id: Gitlab::Regex.git_reference_regex, + path: /.*/ + } + end end - collection do - get :branch_from - get :branch_to - get :update_branches + resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do + member do + get :diffs + post :automerge + get :automerge_check + get :ci_status + end + + collection do + get :branch_from + get :branch_to + get :update_branches + end end - end - resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do - member do - get :test + resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + + resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do + member do + get :test + end end - end - resources :team, controller: 'team_members', only: [:index] - resources :milestones, except: [:destroy], constraints: { id: /\d+/ } do - member do - put :sort_issues - put :sort_merge_requests + resources :team, controller: 'team_members', only: [:index] + resources :milestones, except: [:destroy], constraints: { id: /\d+/ } do + member do + put :sort_issues + put :sort_merge_requests + end end - end - resources :labels, constraints: { id: /\d+/ } do - collection do - post :generate + resources :labels, constraints: { id: /\d+/ } do + collection do + post :generate + end end - end - resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do - collection do - post :bulk_update + resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do + collection do + post :bulk_update + end end - end - resources :team_members, except: [:index, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do - collection do - delete :leave + resources :team_members, except: [:index, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do + collection do + delete :leave - # Used for import team - # from another project - get :import - post :apply_import + # Used for import team + # from another project + get :import + post :apply_import + end end - end - resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do - member do - delete :delete_attachment + resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do + member do + delete :delete_attachment + end end end @@ -380,6 +424,4 @@ Gitlab::Application.routes.draw do end get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } - - root to: 'dashboard#show' end diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index 2fd6385fe7b..9be4d39d2d5 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -15,17 +15,17 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps step 'I should see project details' do project = Project.first - current_path.should == admin_project_path(project) + current_path.should == admin_namespace_project_path(project.namespace, project) page.should have_content(project.name_with_namespace) page.should have_content(project.creator.name) end step 'I visit admin project page' do - visit admin_project_path(project) + visit admin_namespace_project_path(project.namespace, project) end step 'I transfer project to group \'Web\'' do - find(:xpath, "//input[@id='namespace_id']").set group.id + find(:xpath, "//input[@id='new_namespace_id']").set group.id click_button 'Transfer' end diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 1826ead1d51..f9b1f18562a 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -21,7 +21,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'I see prefilled new Merge Request page' do - current_path.should == new_project_merge_request_path(@project) + current_path.should == new_namespace_project_merge_request_path(@project.namespace, @project) find("#merge_request_target_project_id").value.should == @project.id.to_s find("#merge_request_source_branch").value.should == "fix" find("#merge_request_target_branch").value.should == "master" diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb index 8172f7922cc..26b71406bd8 100644 --- a/features/steps/explore/projects.rb +++ b/features/steps/explore/projects.rb @@ -65,7 +65,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps title: "New feature", project: public_project ) - visit project_issues_path(public_project) + visit namespace_project_issues_path(public_project.namespace, public_project) end @@ -84,7 +84,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps title: "New internal feature", project: internal_project ) - visit project_issues_path(internal_project) + visit namespace_project_issues_path(internal_project.namespace, internal_project) end @@ -95,7 +95,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps end step 'I visit "Community" merge requests page' do - visit project_merge_requests_path(public_project) + visit namespace_project_merge_requests_path(public_project.namespace, public_project) end step 'project "Community" has "Bug fix" open merge request' do @@ -112,7 +112,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps end step 'I visit "Internal" merge requests page' do - visit project_merge_requests_path(internal_project) + visit namespace_project_merge_requests_path(internal_project.namespace, internal_project) end step 'project "Internal" has "Feature implemented" open merge request' do diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 610e7fd3a48..f44afb8cbe6 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -194,8 +194,8 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'I should see group milestone with all issues and MRs assigned to that milestone' do page.should have_content('Milestone GL-113') page.should have_content('Progress: 0 closed – 4 open') - page.should have_link(@issue1.title, href: project_issue_path(@project1, @issue1)) - page.should have_link(@mr3.title, href: project_merge_request_path(@project3, @mr3)) + page.should have_link(@issue1.title, href: namespace_project_issue_path(@project1.namespace, @project1, @issue1)) + page.should have_link(@mr3.title, href: namespace_project_merge_request_path(@project3.namespace, @project3, @mr3)) end protected diff --git a/features/steps/project/archived.rb b/features/steps/project/archived.rb index afbf4d5950d..37ad0c77655 100644 --- a/features/steps/project/archived.rb +++ b/features/steps/project/archived.rb @@ -15,7 +15,7 @@ class Spinach::Features::ProjectArchived < Spinach::FeatureSteps When 'I visit project "Forum" page' do project = Project.find_by(name: "Forum") - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I should not see "Archived"' do diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index d515ee1ac11..b2dccf868b0 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -24,7 +24,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I click on commit link' do - visit project_commit_path(@project, sample_commit.id) + visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id) end step 'I see commit info' do @@ -58,7 +58,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps step 'I visit big commit page' do Commit::DIFF_SAFE_FILES = 20 - visit project_commit_path(@project, sample_big_commit.id) + visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id) end step 'I see big commit warning' do @@ -68,7 +68,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps end step 'I visit a commit with an image that changed' do - visit project_commit_path(@project, sample_image_commit.id) + visit namespace_project_commit_path(@project.namespace, @project, sample_image_commit.id) end step 'The diff links to both the previous and current image' do diff --git a/features/steps/project/commits/user_lookup.rb b/features/steps/project/commits/user_lookup.rb index 0622fef43bb..63ff84c82ef 100644 --- a/features/steps/project/commits/user_lookup.rb +++ b/features/steps/project/commits/user_lookup.rb @@ -4,11 +4,11 @@ class Spinach::Features::ProjectCommitsUserLookup < Spinach::FeatureSteps include SharedPaths step 'I click on commit link' do - visit project_commit_path(@project, sample_commit.id) + visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id) end step 'I click on another commit link' do - visit project_commit_path(@project, sample_commit.parent_id) + visit namespace_project_commit_path(@project.namespace, @project, sample_commit.parent_id) end step 'I have user with primary email' do diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb index 6b07b62f16f..6b85cf74f5f 100644 --- a/features/steps/project/create.rb +++ b/features/steps/project/create.rb @@ -9,7 +9,7 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps step 'I should see project page' do page.should have_content "Empty" - current_path.should == project_path(Project.last) + current_path.should == namespace_project_path(Project.last.namespace, Project.last) end step 'I should see empty project instuctions' do diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index 914da31322f..4bf5cb5fa40 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -24,7 +24,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I should be on deploy keys page' do - current_path.should == project_deploy_keys_path(@project) + current_path.should == namespace_project_deploy_keys_path(@project.namespace, @project) end step 'I should see newly created deploy key' do diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index a5484ad3a00..63ad90e1241 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -23,7 +23,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps step 'I should see merge request "Merge Request On Forked Project"' do @project.merge_requests.size.should >= 1 @merge_request = @project.merge_requests.last - current_path.should == project_merge_request_path(@project, @merge_request) + current_path.should == namespace_project_merge_request_path(@project.namespace, @project, @merge_request) @merge_request.title.should == "Merge Request On Forked Project" @merge_request.source_project.should == @forked_project @merge_request.source_branch.should == "fix" @@ -64,7 +64,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I see prefilled new Merge Request page for the forked project' do - current_path.should == new_project_merge_request_path(@forked_project) + current_path.should == new_namespace_project_merge_request_path(@forked_project.namespace, @forked_project) find("#merge_request_source_project_id").value.should == @forked_project.id.to_s find("#merge_request_target_project_id").value.should == @project.id.to_s find("#merge_request_source_branch").value.should have_content "new_design" @@ -86,7 +86,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps page.should have_content "An Edited Forked Merge Request" @project.merge_requests.size.should >= 1 @merge_request = @project.merge_requests.last - current_path.should == project_merge_request_path(@project, @merge_request) + current_path.should == namespace_project_merge_request_path(@project.namespace, @project, @merge_request) @merge_request.source_project.should == @forked_project @merge_request.source_branch.should == "fix" @merge_request.target_branch.should == "master" @@ -106,7 +106,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I see the edit page prefilled for "Merge Request On Forked Project"' do - current_path.should == edit_project_merge_request_path(@project, @merge_request) + current_path.should == edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) page.should have_content "Edit merge request ##{@merge_request.id}" find("#merge_request_title").value.should == "Merge Request On Forked Project" end diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index ba460ac8097..bc07c3d413c 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -8,12 +8,12 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps When 'I visit project "Shop" graph page' do project = Project.find_by(name: "Shop") - visit project_graph_path(project, "master") + visit namespace_project_graph_path(project.namespace, project, "master") end step 'I visit project "Shop" commits graph page' do project = Project.find_by(name: "Shop") - visit commits_project_graph_path(project, "master") + visit commits_namespace_project_graph_path(project.namespace, project, "master") end step 'page should have commits graphs' do diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index f4b8d372be8..4b135202593 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -29,7 +29,7 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps end step 'I should see newly created hook' do - current_path.should == project_hooks_path(current_project) + current_path.should == namespace_project_hooks_path(current_project.namespace, current_project) page.should have_content(@url) end @@ -44,7 +44,7 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps end step 'hook should be triggered' do - current_path.should == project_hooks_path(current_project) + current_path.should == namespace_project_hooks_path(current_project.namespace, current_project) page.should have_selector '.flash-notice', text: 'Hook successfully executed.' end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index c0ae5208541..6d72c93ad13 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -168,7 +168,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps When 'I visit empty project page' do project = Project.find_by(name: 'Empty Project') - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I see empty project details with ssh clone info' do @@ -180,7 +180,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps When "I visit empty project's issues page" do project = Project.find_by(name: 'Empty Project') - visit project_issues_path(project) + visit namespace_project_issues_path(project.namespace, project) end step 'I leave a comment with code block' do diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb index 3e3e90824b4..6ce34c500c6 100644 --- a/features/steps/project/issues/labels.rb +++ b/features/steps/project/issues/labels.rb @@ -4,7 +4,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps include SharedPaths step 'I visit \'bug\' label edit page' do - visit edit_project_label_path(project, bug_label) + visit edit_namespace_project_label_path(project.namespace, project, bug_label) end step 'I remove label \'bug\'' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 6f421de1aba..e477444023d 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -101,11 +101,11 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I switch to the diff tab' do - visit diffs_project_merge_request_path(project, merge_request) + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) end step 'I switch to the merge request\'s comments tab' do - visit project_merge_request_path(project, merge_request) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) end step 'I click on the commit in the merge request' do diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb index 14fdc72b8b6..a15688ace6a 100644 --- a/features/steps/project/network_graph.rb +++ b/features/steps/project/network_graph.rb @@ -12,7 +12,7 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps Network::Graph.stub(max_count: 10) project = Project.find_by(name: "Shop") - visit project_network_path(project, "master") + visit namespace_project_network_path(project.namespace, project, "master") end step 'page should select "master" in select box' do diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index e2badccbcf4..57c6e39c801 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -13,7 +13,7 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I visit project "Community" page' do project = Project.find_by(name: 'Community') - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I should see project "Community" home page' do @@ -25,12 +25,12 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps step 'I visit project "Enterprise" page' do project = Project.find_by(name: 'Enterprise') - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I visit project "CommunityDoesNotExist" page' do project = Project.find_by(name: 'Community') - visit project_path(project) + 'DoesNotExist' + visit namespace_project_path(project.namespace, project) + 'DoesNotExist' end step 'I click on "Sign In"' do diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 957a16d06a8..3307117e69a 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -4,7 +4,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps include SharedPaths step 'I visit project "Shop" services page' do - visit project_services_path(@project) + visit namespace_project_services_path(@project.namespace, @project) end step 'I should see list of available services' do diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 4a39bfdbb79..343aeb53b11 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -86,7 +86,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps end step 'I visit snippet page "Snippet one"' do - visit project_snippet_path(project, project_snippet) + visit namespace_project_snippet_path(project.namespace, project, project_snippet) end def project_snippet diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 1fe01e55aa4..98d8a60e1a5 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -11,7 +11,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see files from repository for "6d39438"' do - current_path.should == project_tree_path(@project, "6d39438") + current_path.should == namespace_project_tree_path(@project.namespace, @project, "6d39438") page.should have_content ".gitignore" page.should have_content "LICENSE" end @@ -141,21 +141,24 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I am redirected to the files URL' do - current_path.should == project_tree_path(@project, 'master') + current_path.should == namespace_project_tree_path(@project.namespace, @project, 'master') end step 'I am redirected to the ".gitignore"' do - expect(current_path).to eq(project_blob_path(@project, 'master/.gitignore')) + expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'master/.gitignore')) end step 'I am redirected to the permalink URL' do - expect(current_path).to eq(project_blob_path( - @project, @project.repository.commit.sha + '/.gitignore')) + expect(current_path).to( + eq(namespace_project_blob_path(@project.namespace, @project, + @project.repository.commit.sha + + '/.gitignore')) + ) end step 'I am redirected to the new file' do - expect(current_path).to eq(project_blob_path( - @project, 'master/' + new_file_name)) + expect(current_path).to eq(namespace_project_blob_path( + @project.namespace, @project, 'master/' + new_file_name)) end step "I don't see the permalink link" do diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index 53578ee5970..7961fdedad8 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -13,7 +13,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see files from repository in markdown' do - current_path.should == project_tree_path(@project, "markdown") + current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown") page.should have_content "README.md" page.should have_content "CHANGELOG" end @@ -33,7 +33,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct document rendered' do - current_path.should == project_blob_path(@project, "markdown/doc/api/README.md") + current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") page.should have_content "All API requests require authentication" end @@ -42,7 +42,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct directory rendered' do - current_path.should == project_tree_path(@project, "markdown/doc/raketasks") + current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown/doc/raketasks") page.should have_content "backup_restore.md" page.should have_content "maintenance.md" end @@ -52,7 +52,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct doc/api directory rendered' do - current_path.should == project_tree_path(@project, "markdown/doc/api") + current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") page.should have_content "README.md" page.should have_content "users.md" end @@ -62,7 +62,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see correct maintenance file rendered' do - current_path.should == project_blob_path(@project, "markdown/doc/raketasks/maintenance.md") + current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/raketasks/maintenance.md") page.should have_content "bundle exec rake gitlab:env:info RAILS_ENV=production" end @@ -93,7 +93,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see correct file rendered' do - current_path.should == project_blob_path(@project, "markdown/doc/api/README.md") + current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") page.should have_content "Contents" page.should have_link "Users" page.should have_link "Rake tasks" @@ -104,7 +104,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see the correct document file' do - current_path.should == project_blob_path(@project, "markdown/doc/api/users.md") + current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") page.should have_content "Get a list of users." end @@ -115,100 +115,100 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps # Markdown branch When 'I visit markdown branch' do - visit project_tree_path(@project, "markdown") + visit namespace_project_tree_path(@project.namespace, @project, "markdown") end When 'I visit markdown branch "README.md" blob' do - visit project_blob_path(@project, "markdown/README.md") + visit namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") end When 'I visit markdown branch "d" tree' do - visit project_tree_path(@project, "markdown/d") + visit namespace_project_tree_path(@project.namespace, @project, "markdown/d") end When 'I visit markdown branch "d/README.md" blob' do - visit project_blob_path(@project, "markdown/d/README.md") + visit namespace_project_blob_path(@project.namespace, @project, "markdown/d/README.md") end step 'I should see files from repository in markdown branch' do - current_path.should == project_tree_path(@project, "markdown") + current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown") page.should have_content "README.md" page.should have_content "CHANGELOG" end step 'I see correct file rendered in markdown branch' do - current_path.should == project_blob_path(@project, "markdown/doc/api/README.md") + current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") page.should have_content "Contents" page.should have_link "Users" page.should have_link "Rake tasks" end step 'I should see correct document rendered for markdown branch' do - current_path.should == project_blob_path(@project, "markdown/doc/api/README.md") + current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") page.should have_content "All API requests require authentication" end step 'I should see correct directory rendered for markdown branch' do - current_path.should == project_tree_path(@project, "markdown/doc/raketasks") + current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown/doc/raketasks") page.should have_content "backup_restore.md" page.should have_content "maintenance.md" end step 'I should see the users document file in markdown branch' do - current_path.should == project_blob_path(@project, "markdown/doc/api/users.md") + current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") page.should have_content "Get a list of users." end # Expected link contents step 'The link with text "empty" should have url "tree/markdown"' do - find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown") + find('a', text: /^empty$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown") end step 'The link with text "empty" should have url "blob/markdown/README.md"' do - find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + find('a', text: /^empty$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") end step 'The link with text "empty" should have url "tree/markdown/d"' do - find('a', text: /^empty$/)['href'] == current_host + project_tree_path(@project, "markdown/d") + find('a', text: /^empty$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown/d") end step 'The link with text "empty" should have '\ 'url "blob/markdown/d/README.md"' do - find('a', text: /^empty$/)['href'] == current_host + project_blob_path(@project, "markdown/d/README.md") + find('a', text: /^empty$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/d/README.md") end step 'The link with text "ID" should have url "tree/markdownID"' do - find('a', text: /^#id$/)['href'] == current_host + project_tree_path(@project, "markdown") + '#id' + find('a', text: /^#id$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown") + '#id' end step 'The link with text "/ID" should have url "tree/markdownID"' do - find('a', text: /^\/#id$/)['href'] == current_host + project_tree_path(@project, "markdown") + '#id' + find('a', text: /^\/#id$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown") + '#id' end step 'The link with text "README.mdID" '\ 'should have url "blob/markdown/README.mdID"' do - find('a', text: /^README.md#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id' + find('a', text: /^README.md#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id' end step 'The link with text "d/README.mdID" should have '\ 'url "blob/markdown/d/README.mdID"' do - find('a', text: /^d\/README.md#id$/)['href'] == current_host + project_blob_path(@project, "d/markdown/README.md") + '#id' + find('a', text: /^d\/README.md#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "d/markdown/README.md") + '#id' end step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do - find('a', text: /^#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id' + find('a', text: /^#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id' end step 'The link with text "/ID" should have url "blob/markdown/README.mdID"' do - find('a', text: /^\/#id$/)['href'] == current_host + project_blob_path(@project, "markdown/README.md") + '#id' + find('a', text: /^\/#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id' end # Wiki step 'I go to wiki page' do click_link "Wiki" - current_path.should == project_wiki_path(@project, "home") + current_path.should == namespace_project_wiki_path(@project.namespace, @project, "home") end step 'I add various links to the wiki page' do @@ -218,7 +218,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'Wiki page should have added links' do - current_path.should == project_wiki_path(@project, "home") + current_path.should == namespace_project_wiki_path(@project.namespace, @project, "home") page.should have_content "test GitLab API doc Rake tasks" end @@ -237,13 +237,13 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see new wiki page named test' do - current_path.should == project_wiki_path(@project, "test") + current_path.should == namespace_project_wiki_path(@project.namespace, @project, "test") page.should have_content "Editing" end When 'I go back to wiki page home' do - visit project_wiki_path(@project, "home") - current_path.should == project_wiki_path(@project, "home") + visit namespace_project_wiki_path(@project.namespace, @project, "home") + current_path.should == namespace_project_wiki_path(@project.namespace, @project, "home") end step 'I click on GitLab API doc link' do @@ -251,7 +251,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see Gitlab API document' do - current_path.should == project_wiki_path(@project, "api") + current_path.should == namespace_project_wiki_path(@project.namespace, @project, "api") page.should have_content "Editing" end @@ -260,13 +260,13 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I see Rake tasks directory' do - current_path.should == project_wiki_path(@project, "raketasks") + current_path.should == namespace_project_wiki_path(@project.namespace, @project, "raketasks") page.should have_content "Editing" end step 'I go directory which contains README file' do - visit project_tree_path(@project, "markdown/doc/api") - current_path.should == project_tree_path(@project, "markdown/doc/api") + visit namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") + current_path.should == namespace_project_tree_path(@project.namespace, @project, "markdown/doc/api") end step 'I click on a relative link in README' do @@ -274,7 +274,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'I should see the correct markdown' do - current_path.should == project_blob_path(@project, "markdown/doc/api/users.md") + current_path.should == namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") page.should have_content "List users" end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index aa00818c602..cd7d5eac243 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -11,7 +11,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should be redirected back to the Edit Home Wiki page' do - current_path.should == project_wiki_path(project, :home) + current_path.should == namespace_project_wiki_path(project.namespace, project, :home) end step 'I create the Wiki Home page' do @@ -33,7 +33,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I browse to that Wiki page' do - visit project_wiki_path(project, @page) + visit namespace_project_wiki_path(project.namespace, project, @page) end step 'I click on the Edit button' do @@ -50,7 +50,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I should be redirected back to that Wiki page' do - current_path.should == project_wiki_path(project, @page) + current_path.should == namespace_project_wiki_path(project.namespace, project, @page) end step 'That page has two revisions' do @@ -90,7 +90,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I browse to wiki page with images' do - visit project_wiki_path(project, @wiki_page) + visit namespace_project_wiki_path(project.namespace, project, @wiki_page) end step 'I click on existing image link' do diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index cef48c179b2..835b644e6c7 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -136,7 +136,7 @@ module SharedPaths end step 'I visit admin projects page' do - visit admin_projects_path + visit admin_namespaces_projects_path end step 'I visit admin users page' do @@ -180,59 +180,59 @@ module SharedPaths # ---------------------------------------- step "I visit my project's home page" do - visit project_path(@project) + visit namespace_project_path(@project.namespace, @project) end step "I visit my project's settings page" do - visit edit_project_path(@project) + visit edit_namespace_project_path(@project.namespace, @project) end step "I visit my project's files page" do - visit project_tree_path(@project, root_ref) + visit namespace_project_tree_path(@project.namespace, @project, root_ref) end step 'I visit a binary file in the repo' do - visit project_blob_path(@project, File.join( + visit namespace_project_blob_path(@project.namespace, @project, File.join( root_ref, 'files/images/logo-black.png')) end step "I visit my project's commits page" do - visit project_commits_path(@project, root_ref, {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, root_ref, {limit: 5}) end step "I visit my project's commits page for a specific path" do - visit project_commits_path(@project, root_ref + "/app/models/project.rb", {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, root_ref + "/app/models/project.rb", {limit: 5}) end step 'I visit my project\'s commits stats page' do - visit stats_project_repository_path(@project) + visit stats_namespace_project_repository_path(@project.namespace, @project) end step "I visit my project's network page" do # Stub Graph max_size to speed up test (10 commits vs. 650) Network::Graph.stub(max_count: 10) - visit project_network_path(@project, root_ref) + visit namespace_project_network_path(@project.namespace, @project, root_ref) end step "I visit my project's issues page" do - visit project_issues_path(@project) + visit namespace_project_issues_path(@project.namespace, @project) end step "I visit my project's merge requests page" do - visit project_merge_requests_path(@project) + visit namespace_project_merge_requests_path(@project.namespace, @project) end step "I visit my project's wiki page" do - visit project_wiki_path(@project, :home) + visit namespace_project_wiki_path(@project.namespace, @project, :home) end step 'I visit project hooks page' do - visit project_hooks_path(@project) + visit namespace_project_hooks_path(@project.namespace, @project) end step 'I visit project deploy keys page' do - visit project_deploy_keys_path(@project) + visit namespace_project_deploy_keys_path(@project.namespace, @project) end # ---------------------------------------- @@ -240,153 +240,153 @@ module SharedPaths # ---------------------------------------- step 'I visit project "Shop" page' do - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I visit project "Forked Shop" merge requests page' do - visit project_merge_requests_path(@forked_project) + visit namespace_project_merge_requests_path(@forked_project.namespace, @forked_project) end step 'I visit edit project "Shop" page' do - visit edit_project_path(project) + visit edit_namespace_project_path(project.namespace, project) end step 'I visit project branches page' do - visit project_branches_path(@project) + visit namespace_project_branches_path(@project.namespace, @project) end step 'I visit project protected branches page' do - visit project_protected_branches_path(@project) + visit namespace_project_protected_branches_path(@project.namespace, @project) end step 'I visit compare refs page' do - visit project_compare_index_path(@project) + visit namespace_project_compare_index_path(@project.namespace, @project) end step 'I visit project commits page' do - visit project_commits_path(@project, root_ref, {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, root_ref, {limit: 5}) end step 'I visit project commits page for stable branch' do - visit project_commits_path(@project, 'stable', {limit: 5}) + visit namespace_project_commits_path(@project.namespace, @project, 'stable', {limit: 5}) end step 'I visit project source page' do - visit project_tree_path(@project, root_ref) + visit namespace_project_tree_path(@project.namespace, @project, root_ref) end step 'I visit blob file from repo' do - visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path)) + visit namespace_project_blob_path(@project.namespace, @project, File.join(sample_commit.id, sample_blob.path)) end step 'I visit ".gitignore" file in repo' do - visit project_blob_path(@project, File.join(root_ref, '.gitignore')) + visit namespace_project_blob_path(@project.namespace, @project, File.join(root_ref, '.gitignore')) end step 'I am on the new file page' do - current_path.should eq(project_create_blob_path(@project, root_ref)) + current_path.should eq(namespace_project_create_blob_path(@project.namespace, @project, root_ref)) end step 'I am on the ".gitignore" edit file page' do - current_path.should eq(project_edit_blob_path( - @project, File.join(root_ref, '.gitignore'))) + current_path.should eq(namespace_project_edit_blob_path( + @project.namespace, @project, File.join(root_ref, '.gitignore'))) end step 'I visit project source page for "6d39438"' do - visit project_tree_path(@project, "6d39438") + visit namespace_project_tree_path(@project.namespace, @project, "6d39438") end step 'I visit project source page for' \ ' "6d394385cf567f80a8fd85055db1ab4c5295806f"' do - visit project_tree_path(@project, + visit namespace_project_tree_path(@project.namespace, @project, '6d394385cf567f80a8fd85055db1ab4c5295806f') end step 'I visit project tags page' do - visit project_tags_path(@project) + visit namespace_project_tags_path(@project.namespace, @project) end step 'I visit project commit page' do - visit project_commit_path(@project, sample_commit.id) + visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id) end step 'I visit project "Shop" issues page' do - visit project_issues_path(project) + visit namespace_project_issues_path(project.namespace, project) end step 'I visit issue page "Release 0.4"' do issue = Issue.find_by(title: "Release 0.4") - visit project_issue_path(issue.project, issue) + visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) end step 'I visit issue page "Tasks-open"' do issue = Issue.find_by(title: 'Tasks-open') - visit project_issue_path(issue.project, issue) + visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) end step 'I visit issue page "Tasks-closed"' do issue = Issue.find_by(title: 'Tasks-closed') - visit project_issue_path(issue.project, issue) + visit namespace_project_issue_path(issue.project.namespace, issue.project, issue) end step 'I visit project "Shop" labels page' do project = Project.find_by(name: 'Shop') - visit project_labels_path(project) + visit namespace_project_labels_path(project.namespace, project) end step 'I visit project "Forum" labels page' do project = Project.find_by(name: 'Forum') - visit project_labels_path(project) + visit namespace_project_labels_path(project.namespace, project) end step 'I visit project "Shop" new label page' do project = Project.find_by(name: 'Shop') - visit new_project_label_path(project) + visit new_namespace_project_label_path(project.namespace, project) end step 'I visit project "Forum" new label page' do project = Project.find_by(name: 'Forum') - visit new_project_label_path(project) + visit new_namespace_project_label_path(project.namespace, project) end step 'I visit merge request page "Bug NS-04"' do mr = MergeRequest.find_by(title: "Bug NS-04") - visit project_merge_request_path(mr.target_project, mr) + visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) end step 'I visit merge request page "Bug NS-05"' do mr = MergeRequest.find_by(title: "Bug NS-05") - visit project_merge_request_path(mr.target_project, mr) + visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) end step 'I visit merge request page "MR-task-open"' do mr = MergeRequest.find_by(title: 'MR-task-open') - visit project_merge_request_path(mr.target_project, mr) + visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) end step 'I visit merge request page "MR-task-closed"' do mr = MergeRequest.find_by(title: 'MR-task-closed') - visit project_merge_request_path(mr.target_project, mr) + visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) end step 'I visit project "Shop" merge requests page' do - visit project_merge_requests_path(project) + visit namespace_project_merge_requests_path(project.namespace, project) end step 'I visit forked project "Shop" merge requests page' do - visit project_merge_requests_path(project) + visit namespace_project_merge_requests_path(project.namespace, project) end step 'I visit project "Shop" milestones page' do - visit project_milestones_path(project) + visit namespace_project_milestones_path(project.namespace, project) end step 'I visit project "Shop" team page' do - visit project_team_index_path(project) + visit namespace_project_team_index_path(project.namespace, project) end step 'I visit project wiki page' do - visit project_wiki_path(@project, :home) + visit namespace_project_wiki_path(@project.namespace, @project, :home) end # ---------------------------------------- @@ -395,22 +395,22 @@ module SharedPaths step 'I visit project "Community" page' do project = Project.find_by(name: "Community") - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I visit project "Community" source page' do project = Project.find_by(name: 'Community') - visit project_tree_path(project, root_ref) + visit namespace_project_tree_path(project.namespace, project, root_ref) end step 'I visit project "Internal" page' do project = Project.find_by(name: "Internal") - visit project_path(project) + visit namespace_project_path(project.namespace, project) end step 'I visit project "Enterprise" page' do project = Project.find_by(name: "Enterprise") - visit project_path(project) + visit namespace_project_path(project.namespace, project) end # ---------------------------------------- @@ -419,7 +419,7 @@ module SharedPaths step "I visit empty project page" do project = Project.find_by(name: "Empty Public Project") - visit project_path(project) + visit namespace_project_path(project.namespace, project) end # ---------------------------------------- @@ -447,7 +447,7 @@ module SharedPaths # ---------------------------------------- step 'I visit project "Shop" snippets page' do - visit project_snippets_path(project) + visit namespace_project_snippets_path(project.namespace, project) end step 'I visit snippets page' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index cf0be256231..41f71ae29cb 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -29,7 +29,8 @@ module SharedProject end step 'I visit my empty project page' do - visit project_path(Project.find_by(name: 'Empty Project')) + project = Project.find_by(name: 'Empty Project') + visit namespace_project_path(project.namespace, project) end step 'project "Shop" has push event' do @@ -64,7 +65,7 @@ module SharedProject end step 'I should see project settings' do - current_path.should == edit_project_path(@project) + current_path.should == edit_namespace_project_path(@project.namespace, @project) page.should have_content("Project name") page.should have_content("Features:") end diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 19215cfb7e6..6e4ed01e079 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -102,7 +102,8 @@ module ExtractsPath raise InvalidPathError unless @commit @hex_path = Digest::SHA1.hexdigest(@path) - @logs_path = logs_file_project_ref_path(@project, @ref, @path) + @logs_path = logs_file_namespace_project_ref_path(@project.namespace, + @project, @ref, @path) rescue RuntimeError, NoMethodError, InvalidPathError not_found! diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index fb0218a2778..a1fd794aed2 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -202,7 +202,7 @@ module Gitlab ) if identifier == "all" - link_to("@all", project_url(project), options) + link_to("@all", namespace_project_url(project.namespace, project), options) elsif namespace = Namespace.find_by(path: identifier) url = if namespace.type == "Group" @@ -222,7 +222,7 @@ module Gitlab ) link_to( render_colored_label(label), - project_issues_path(project, label_name: label.name), + namespace_project_issues_path(project.namespace, project, label_name: label.name), options ) end @@ -255,7 +255,8 @@ module Gitlab title: "Merge Request: #{merge_request.title}", class: "gfm gfm-merge_request #{html_options[:class]}" ) - url = project_merge_request_url(project, merge_request) + url = namespace_project_merge_request_url(project.namespace, project, + merge_request) link_to("#{prefix_text}!#{identifier}", url, options) end end @@ -266,8 +267,11 @@ module Gitlab title: "Snippet: #{snippet.title}", class: "gfm gfm-snippet #{html_options[:class]}" ) - link_to("$#{identifier}", project_snippet_url(project, snippet), - options) + link_to( + "$#{identifier}", + namespace_project_snippet_url(project.namespace, project, snippet), + options + ) end end @@ -280,7 +284,7 @@ module Gitlab prefix_text = "#{prefix_text}@" if prefix_text link_to( "#{prefix_text}#{identifier}", - project_commit_url(project, commit), + namespace_project_commit_url(project.namespace, project, commit), options ) end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 877488d8471..e7153cc3225 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -17,9 +17,10 @@ module Gitlab def issue_url(id) issue = Issue.find(id) - project_issue_url(id: issue.iid, - project_id: issue.project, - host: Gitlab.config.gitlab['url']) + namespace_project_issue_url(namespace_id: issue.project.namespace, + id: issue.iid, + project_id: issue.project, + host: Gitlab.config.gitlab['url']) end end end diff --git a/spec/controllers/blob_controller_spec.rb b/spec/controllers/blob_controller_spec.rb index ea52e4d212a..a1102f28340 100644 --- a/spec/controllers/blob_controller_spec.rb +++ b/spec/controllers/blob_controller_spec.rb @@ -17,7 +17,10 @@ describe Projects::BlobController do describe "GET show" do render_views - before { get :show, project_id: project.to_param, id: id } + before do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: id) + end context "valid branch, valid file" do let(:id) { 'master/README.md' } @@ -39,7 +42,8 @@ describe Projects::BlobController do render_views before do - get :show, project_id: project.to_param, id: id + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: id) controller.instance_variable_set(:@blob, nil) end diff --git a/spec/controllers/branches_controller_spec.rb b/spec/controllers/branches_controller_spec.rb index 0c39d016440..51397382cfb 100644 --- a/spec/controllers/branches_controller_spec.rb +++ b/spec/controllers/branches_controller_spec.rb @@ -19,6 +19,7 @@ describe Projects::BranchesController do before { post :create, + namespace_id: project.namespace.to_param, project_id: project.to_param, branch_name: branch, ref: ref diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index f0e39e674fd..3394a1f863f 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -13,7 +13,8 @@ describe Projects::CommitController do describe "#show" do shared_examples "export as" do |format| it "should generally work" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response).to be_success end @@ -21,11 +22,13 @@ describe Projects::CommitController do it "should generate it" do expect_any_instance_of(Commit).to receive(:"to_#{format}") - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) end it "should render it" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response.body).to eq(commit.send(:"to_#{format}")) end @@ -34,7 +37,8 @@ describe Projects::CommitController do allow_any_instance_of(Commit).to receive(:"to_#{format}"). and_return('HTML entities &<>" ') - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response.body).to_not include('&') expect(response.body).to_not include('>') @@ -48,7 +52,8 @@ describe Projects::CommitController do let(:format) { :diff } it "should really only be a git diff" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response.body).to start_with("diff --git") end @@ -59,13 +64,15 @@ describe Projects::CommitController do let(:format) { :patch } it "should really be a git email patch" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response.body).to start_with("From #{commit.id}") end it "should contain a git diff" do - get :show, project_id: project.to_param, id: commit.id, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id, format: format) expect(response.body).to match(/^diff --git/) end @@ -74,7 +81,8 @@ describe Projects::CommitController do describe "#branches" do it "contains branch and tags information" do - get :branches, project_id: project.to_param, id: commit.id + get(:branches, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: commit.id) expect(assigns(:branches)).to include("master", "feature_conflict") expect(assigns(:tags)).to include("v1.1.0") diff --git a/spec/controllers/commits_controller_spec.rb b/spec/controllers/commits_controller_spec.rb index c3de01a84f2..2184b35152e 100644 --- a/spec/controllers/commits_controller_spec.rb +++ b/spec/controllers/commits_controller_spec.rb @@ -12,7 +12,8 @@ describe Projects::CommitsController do describe "GET show" do context "as atom feed" do it "should render as atom" do - get :show, project_id: project.to_param, id: "master", format: "atom" + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: "master", format: "atom") expect(response).to be_success expect(response.content_type).to eq('application/atom+xml') end diff --git a/spec/controllers/merge_requests_controller_spec.rb b/spec/controllers/merge_requests_controller_spec.rb index eedaf17941a..d6f56ed33d6 100644 --- a/spec/controllers/merge_requests_controller_spec.rb +++ b/spec/controllers/merge_requests_controller_spec.rb @@ -13,7 +13,8 @@ describe Projects::MergeRequestsController do describe "#show" do shared_examples "export merge as" do |format| it "should generally work" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) expect(response).to be_success end @@ -21,11 +22,13 @@ describe Projects::MergeRequestsController do it "should generate it" do expect_any_instance_of(MergeRequest).to receive(:"to_#{format}") - get :show, project_id: project.to_param, id: merge_request.iid, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) end it "should render it" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s) end @@ -34,7 +37,8 @@ describe Projects::MergeRequestsController do allow_any_instance_of(MergeRequest).to receive(:"to_#{format}"). and_return('HTML entities &<>" ') - get :show, project_id: project.to_param, id: merge_request.iid, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) expect(response.body).to_not include('&') expect(response.body).to_not include('>') @@ -48,7 +52,8 @@ describe Projects::MergeRequestsController do let(:format) { :diff } it "should really only be a git diff" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) expect(response.body).to start_with("diff --git") end @@ -59,13 +64,15 @@ describe Projects::MergeRequestsController do let(:format) { :patch } it "should really be a git email patch with commit" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) expect(response.body[0..100]).to start_with("From #{merge_request.commits.last.id}") end it "should contain git diffs" do - get :show, project_id: project.to_param, id: merge_request.iid, format: format + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: merge_request.iid, format: format) expect(response.body).to match(/^diff --git/) end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index ef786ccd324..06c703ecf7a 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -15,14 +15,16 @@ describe ProjectsController do context "without params['markdown_img']" do it "returns an error" do - post :upload_image, id: project.to_param, format: :json + post(:upload_image, namespace_id: project.namespace.to_param, + id: project.to_param, format: :json) expect(response.status).to eq(422) end end context "with invalid file" do before do - post :upload_image, id: project.to_param, markdown_img: txt, format: :json + post(:upload_image, namespace_id: project.namespace.to_param, + id: project.to_param, markdown_img: txt, format: :json) end it "returns an error" do @@ -32,7 +34,8 @@ describe ProjectsController do context "with valid file" do before do - post :upload_image, id: project.to_param, markdown_img: jpg, format: :json + post(:upload_image, namespace_id: project.namespace.to_param, + id: project.to_param, markdown_img: jpg, format: :json) end it "returns a content with original filename and new link." do @@ -46,16 +49,20 @@ describe ProjectsController do it "toggles star if user is signed in" do sign_in(user) expect(user.starred?(public_project)).to be_falsey - post :toggle_star, id: public_project.to_param + post(:toggle_star, namespace_id: public_project.namespace.to_param, + id: public_project.to_param) expect(user.starred?(public_project)).to be_truthy - post :toggle_star, id: public_project.to_param + post(:toggle_star, namespace_id: public_project.namespace.to_param, + id: public_project.to_param) expect(user.starred?(public_project)).to be_falsey end it "does nothing if user is not signed in" do - post :toggle_star, id: public_project.to_param + post(:toggle_star, namespace_id: project.namespace.to_param, + id: public_project.to_param) expect(user.starred?(public_project)).to be_falsey - post :toggle_star, id: public_project.to_param + post(:toggle_star, namespace_id: project.namespace.to_param, + id: public_project.to_param) expect(user.starred?(public_project)).to be_falsey end end diff --git a/spec/controllers/tree_controller_spec.rb b/spec/controllers/tree_controller_spec.rb index 805e0a8795b..7b219819bbc 100644 --- a/spec/controllers/tree_controller_spec.rb +++ b/spec/controllers/tree_controller_spec.rb @@ -18,7 +18,10 @@ describe Projects::TreeController do # Make sure any errors accessing the tree in our views bubble up to this spec render_views - before { get :show, project_id: project.to_param, id: id } + before do + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: id) + end context "valid branch, no path" do let(:id) { 'master' } @@ -45,7 +48,8 @@ describe Projects::TreeController do render_views before do - get :show, project_id: project.to_param, id: id + get(:show, namespace_id: project.namespace.to_param, + project_id: project.to_param, id: id) end context 'redirect to blob' do diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index eae3d102334..101d955d693 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -8,11 +8,11 @@ describe "Admin::Projects", feature: true do describe "GET /admin/projects" do before do - visit admin_projects_path + visit admin_namespaces_projects_path end it "should be ok" do - expect(current_path).to eq(admin_projects_path) + expect(current_path).to eq(admin_namespaces_projects_path) end it "should have projects list" do @@ -22,7 +22,7 @@ describe "Admin::Projects", feature: true do describe "GET /admin/projects/:id" do before do - visit admin_projects_path + visit admin_namespaces_projects_path click_link "#{@project.name}" end diff --git a/spec/features/admin/security_spec.rb b/spec/features/admin/security_spec.rb index 2bcd3d8d010..175fa9d4647 100644 --- a/spec/features/admin/security_spec.rb +++ b/spec/features/admin/security_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe "Admin::Projects", feature: true do describe "GET /admin/projects" do - subject { admin_projects_path } + subject { admin_namespaces_projects_path } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for :user } diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 43163e4113e..baa7814e96a 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -11,7 +11,7 @@ describe 'Issues Feed', feature: true do context 'when authenticated' do it 'should render atom feed' do login_with user - visit project_issues_path(project, :atom) + visit namespace_project_issues_path(project.namespace, project, :atom) expect(response_headers['Content-Type']). to have_content('application/atom+xml') @@ -23,8 +23,8 @@ describe 'Issues Feed', feature: true do context 'when authenticated via private token' do it 'should render atom feed' do - visit project_issues_path(project, :atom, - private_token: user.private_token) + visit namespace_project_issues_path(project.namespace, project, :atom, + private_token: user.private_token) expect(response_headers['Content-Type']). to have_content('application/atom+xml') diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb index 73a9f78708a..fca1a06eb88 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -23,25 +23,25 @@ describe "GitLab Flavored Markdown", feature: true do describe "for commits" do it "should render title in commits#index" do - visit project_commits_path(project, 'master', limit: 1) + visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1) expect(page).to have_link("##{issue.iid}") end it "should render title in commits#show" do - visit project_commit_path(project, commit) + visit namespace_project_commit_path(project.namespace, project, commit) expect(page).to have_link("##{issue.iid}") end it "should render description in commits#show" do - visit project_commit_path(project, commit) + visit namespace_project_commit_path(project.namespace, project, commit) expect(page).to have_link("@#{fred.username}") end it "should render title in repositories#branches" do - visit project_branches_path(project) + visit namespace_project_branches_path(project.namespace, project) expect(page).to have_link("##{issue.iid}") end @@ -62,19 +62,19 @@ describe "GitLab Flavored Markdown", feature: true do end it "should render subject in issues#index" do - visit project_issues_path(project) + visit namespace_project_issues_path(project.namespace, project) expect(page).to have_link("##{@other_issue.iid}") end it "should render subject in issues#show" do - visit project_issue_path(project, @issue) + visit namespace_project_issue_path(project.namespace, project, @issue) expect(page).to have_link("##{@other_issue.iid}") end it "should render details in issues#show" do - visit project_issue_path(project, @issue) + visit namespace_project_issue_path(project.namespace, project, @issue) expect(page).to have_link("@#{fred.username}") end @@ -87,13 +87,13 @@ describe "GitLab Flavored Markdown", feature: true do end it "should render title in merge_requests#index" do - visit project_merge_requests_path(project) + visit namespace_project_merge_requests_path(project.namespace, project) expect(page).to have_link("##{issue.iid}") end it "should render title in merge_requests#show" do - visit project_merge_request_path(project, @merge_request) + visit namespace_project_merge_request_path(project.namespace, project, @merge_request) expect(page).to have_link("##{issue.iid}") end @@ -109,19 +109,19 @@ describe "GitLab Flavored Markdown", feature: true do end it "should render title in milestones#index" do - visit project_milestones_path(project) + visit namespace_project_milestones_path(project.namespace, project) expect(page).to have_link("##{issue.iid}") end it "should render title in milestones#show" do - visit project_milestone_path(project, @milestone) + visit namespace_project_milestone_path(project.namespace, project, @milestone) expect(page).to have_link("##{issue.iid}") end it "should render description in milestones#show" do - visit project_milestone_path(project, @milestone) + visit namespace_project_milestone_path(project.namespace, project, @milestone) expect(page).to have_link("@#{fred.username}") end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index f54155439cb..a2db57ad908 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -21,7 +21,7 @@ describe 'Issues', feature: true do end before do - visit project_issues_path(project) + visit namespace_project_issues_path(project.namespace, project) click_link "Edit" end @@ -61,7 +61,7 @@ describe 'Issues', feature: true do end it 'allows user to select unasigned', js: true do - visit edit_project_issue_path(project, issue) + visit edit_namespace_project_issue_path(project.namespace, project, issue) expect(page).to have_content "Assign to #{@user.name}" @@ -95,7 +95,7 @@ describe 'Issues', feature: true do let(:issue) { @issue } it 'should allow filtering by issues with no specified milestone' do - visit project_issues_path(project, milestone_id: '0') + visit namespace_project_issues_path(project.namespace, project, milestone_id: '0') expect(page).not_to have_content 'foobar' expect(page).to have_content 'barbaz' @@ -103,7 +103,7 @@ describe 'Issues', feature: true do end it 'should allow filtering by a specified milestone' do - visit project_issues_path(project, milestone_id: issue.milestone.id) + visit namespace_project_issues_path(project.namespace, project, milestone_id: issue.milestone.id) expect(page).to have_content 'foobar' expect(page).not_to have_content 'barbaz' @@ -111,7 +111,7 @@ describe 'Issues', feature: true do end it 'should allow filtering by issues with no specified assignee' do - visit project_issues_path(project, assignee_id: '0') + visit namespace_project_issues_path(project.namespace, project, assignee_id: '0') expect(page).to have_content 'foobar' expect(page).not_to have_content 'barbaz' @@ -119,7 +119,7 @@ describe 'Issues', feature: true do end it 'should allow filtering by a specified assignee' do - visit project_issues_path(project, assignee_id: @user.id) + visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id) expect(page).not_to have_content 'foobar' expect(page).to have_content 'barbaz' @@ -140,14 +140,14 @@ describe 'Issues', feature: true do let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') } it 'sorts by newest' do - visit project_issues_path(project, sort: sort_value_recently_created) + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created) expect(first_issue).to include('foo') expect(last_issue).to include('baz') end it 'sorts by oldest' do - visit project_issues_path(project, sort: sort_value_oldest_created) + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created) expect(first_issue).to include('baz') expect(last_issue).to include('foo') @@ -156,7 +156,7 @@ describe 'Issues', feature: true do it 'sorts by most recently updated' do baz.updated_at = Time.now + 100 baz.save - visit project_issues_path(project, sort: sort_value_recently_updated) + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_updated) expect(first_issue).to include('baz') end @@ -164,7 +164,7 @@ describe 'Issues', feature: true do it 'sorts by least recently updated' do baz.updated_at = Time.now - 100 baz.save - visit project_issues_path(project, sort: sort_value_oldest_updated) + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_updated) expect(first_issue).to include('baz') end @@ -178,13 +178,13 @@ describe 'Issues', feature: true do end it 'sorts by recently due milestone' do - visit project_issues_path(project, sort: sort_value_milestone_soon) + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_milestone_soon) expect(first_issue).to include('foo') end it 'sorts by least recently due milestone' do - visit project_issues_path(project, sort: sort_value_milestone_later) + visit namespace_project_issues_path(project.namespace, project, sort: sort_value_milestone_later) expect(first_issue).to include('bar') end @@ -201,9 +201,9 @@ describe 'Issues', feature: true do end it 'sorts with a filter applied' do - visit project_issues_path(project, - sort: sort_value_oldest_created, - assignee_id: user2.id) + visit namespace_project_issues_path(project.namespace, project, + sort: sort_value_oldest_created, + assignee_id: user2.id) expect(first_issue).to include('bar') expect(last_issue).to include('foo') @@ -218,7 +218,7 @@ describe 'Issues', feature: true do context 'by autorized user' do it 'with dropdown menu' do - visit project_issue_path(project, issue) + visit namespace_project_issue_path(project.namespace, project, issue) find('.edit-issue.inline-update #issue_assignee_id'). set project.team.members.first.id @@ -244,7 +244,7 @@ describe 'Issues', feature: true do logout login_with guest - visit project_issue_path(project, issue) + visit namespace_project_issue_path(project.namespace, project, issue) expect(page).to have_content issue.assignee.name end end @@ -257,7 +257,7 @@ describe 'Issues', feature: true do context 'by authorized user' do it 'with dropdown menu' do - visit project_issue_path(project, issue) + visit namespace_project_issue_path(project.namespace, project, issue) find('.edit-issue.inline-update'). select(milestone.title, from: 'issue_milestone_id') @@ -282,7 +282,7 @@ describe 'Issues', feature: true do logout login_with guest - visit project_issue_path(project, issue) + visit namespace_project_issue_path(project.namespace, project, issue) expect(page).to have_content milestone.title end end @@ -295,8 +295,8 @@ describe 'Issues', feature: true do issue.save end - it 'allows user to remove assignee', js: true do - visit project_issue_path(project, issue) + it 'allows user to remove assignee', :js => true do + visit namespace_project_issue_path(project.namespace, project, issue) expect(page).to have_content "Assignee: #{user2.name}" first('#s2id_issue_assignee_id').click diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 7790d0ecd73..5c8b1f5be36 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -12,7 +12,7 @@ describe 'Comments' do before do login_as :admin - visit project_merge_request_path(project, merge_request) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) end subject { page } @@ -136,7 +136,7 @@ describe 'Comments' do before do login_as :admin - visit diffs_project_merge_request_path(project, merge_request) + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) end subject { page } diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index d291621935b..cae11be7cdd 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -7,7 +7,7 @@ describe "Projects", feature: true, js: true do before do @project = create(:project, namespace: @user.namespace) @project.team << [@user, :master] - visit edit_project_path(@project) + visit edit_namespace_project_path(@project.namespace, @project) end it "should remove project" do diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 81f94e33569..322697bced8 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -25,7 +25,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path" do - subject { project_path(project) } + subject { namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -36,7 +36,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/tree/master" do - subject { project_tree_path(project, project.repository.root_ref) } + subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -47,7 +47,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/commits/master" do - subject { project_commits_path(project, project.repository.root_ref, limit: 1) } + subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -58,7 +58,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/commit/:sha" do - subject { project_commit_path(project, project.repository.commit) } + subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -69,7 +69,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/compare" do - subject { project_compare_index_path(project) } + subject { namespace_project_compare_index_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -80,7 +80,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/team" do - subject { project_team_index_path(project) } + subject { namespace_project_team_index_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -94,7 +94,7 @@ describe "Internal Project Access", feature: true do before do commit = project.repository.commit path = '.gitignore' - @blob_path = project_blob_path(project, File.join(commit.id, path)) + @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path)) end it { expect(@blob_path).to be_allowed_for master } @@ -106,7 +106,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/edit" do - subject { edit_project_path(project) } + subject { edit_namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -117,7 +117,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/deploy_keys" do - subject { project_deploy_keys_path(project) } + subject { namespace_project_deploy_keys_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -128,7 +128,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/issues" do - subject { project_issues_path(project) } + subject { namespace_project_issues_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -139,7 +139,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/snippets" do - subject { project_snippets_path(project) } + subject { namespace_project_snippets_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -150,7 +150,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/snippets/new" do - subject { new_project_snippet_path(project) } + subject { new_namespace_project_snippet_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -161,7 +161,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/merge_requests" do - subject { project_merge_requests_path(project) } + subject { namespace_project_merge_requests_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -172,7 +172,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/merge_requests/new" do - subject { new_project_merge_request_path(project) } + subject { new_namespace_project_merge_request_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -183,7 +183,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/branches" do - subject { project_branches_path(project) } + subject { namespace_project_branches_path(project.namespace, project) } before do # Speed increase @@ -199,7 +199,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/tags" do - subject { project_tags_path(project) } + subject { namespace_project_tags_path(project.namespace, project) } before do # Speed increase @@ -215,7 +215,7 @@ describe "Internal Project Access", feature: true do end describe "GET /:project_path/hooks" do - subject { project_hooks_path(project) } + subject { namespace_project_hooks_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index fd21e722611..ea146c3f0e4 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -25,7 +25,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path" do - subject { project_path(project) } + subject { namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -36,7 +36,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/tree/master" do - subject { project_tree_path(project, project.repository.root_ref) } + subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -47,7 +47,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/commits/master" do - subject { project_commits_path(project, project.repository.root_ref, limit: 1) } + subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -58,7 +58,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/commit/:sha" do - subject { project_commit_path(project, project.repository.commit) } + subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -69,7 +69,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/compare" do - subject { project_compare_index_path(project) } + subject { namespace_project_compare_index_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -80,7 +80,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/team" do - subject { project_team_index_path(project) } + subject { namespace_project_team_index_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -94,7 +94,7 @@ describe "Private Project Access", feature: true do before do commit = project.repository.commit path = '.gitignore' - @blob_path = project_blob_path(project, File.join(commit.id, path)) + @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path)) end it { expect(@blob_path).to be_allowed_for master } @@ -106,7 +106,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/edit" do - subject { edit_project_path(project) } + subject { edit_namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -117,7 +117,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/deploy_keys" do - subject { project_deploy_keys_path(project) } + subject { namespace_project_deploy_keys_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -128,7 +128,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/issues" do - subject { project_issues_path(project) } + subject { namespace_project_issues_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -139,7 +139,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/snippets" do - subject { project_snippets_path(project) } + subject { namespace_project_snippets_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -150,7 +150,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/merge_requests" do - subject { project_merge_requests_path(project) } + subject { namespace_project_merge_requests_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -161,7 +161,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/branches" do - subject { project_branches_path(project) } + subject { namespace_project_branches_path(project.namespace, project) } before do # Speed increase @@ -177,7 +177,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/tags" do - subject { project_tags_path(project) } + subject { namespace_project_tags_path(project.namespace, project) } before do # Speed increase @@ -193,7 +193,7 @@ describe "Private Project Access", feature: true do end describe "GET /:project_path/hooks" do - subject { project_hooks_path(project) } + subject { namespace_project_hooks_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index ddc1c3be7df..8ee9199ff29 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -30,7 +30,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path" do - subject { project_path(project) } + subject { namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -41,7 +41,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/tree/master" do - subject { project_tree_path(project, project.repository.root_ref) } + subject { namespace_project_tree_path(project.namespace, project, project.repository.root_ref) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -52,7 +52,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/commits/master" do - subject { project_commits_path(project, project.repository.root_ref, limit: 1) } + subject { namespace_project_commits_path(project.namespace, project, project.repository.root_ref, limit: 1) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -63,7 +63,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/commit/:sha" do - subject { project_commit_path(project, project.repository.commit) } + subject { namespace_project_commit_path(project.namespace, project, project.repository.commit) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -74,7 +74,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/compare" do - subject { project_compare_index_path(project) } + subject { namespace_project_compare_index_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -85,7 +85,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/team" do - subject { project_team_index_path(project) } + subject { namespace_project_team_index_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -99,7 +99,7 @@ describe "Public Project Access", feature: true do before do commit = project.repository.commit path = '.gitignore' - @blob_path = project_blob_path(project, File.join(commit.id, path)) + @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path)) end it { expect(@blob_path).to be_allowed_for master } @@ -111,7 +111,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/edit" do - subject { edit_project_path(project) } + subject { edit_namespace_project_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -122,7 +122,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/deploy_keys" do - subject { project_deploy_keys_path(project) } + subject { namespace_project_deploy_keys_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -133,7 +133,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/issues" do - subject { project_issues_path(project) } + subject { namespace_project_issues_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -144,7 +144,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/snippets" do - subject { project_snippets_path(project) } + subject { namespace_project_snippets_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -155,7 +155,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/snippets/new" do - subject { new_project_snippet_path(project) } + subject { new_namespace_project_snippet_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -166,7 +166,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/merge_requests" do - subject { project_merge_requests_path(project) } + subject { namespace_project_merge_requests_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_allowed_for reporter } @@ -177,7 +177,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/merge_requests/new" do - subject { new_project_merge_request_path(project) } + subject { new_namespace_project_merge_request_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } @@ -188,7 +188,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/branches" do - subject { project_branches_path(project) } + subject { namespace_project_branches_path(project.namespace, project) } before do # Speed increase @@ -204,7 +204,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/tags" do - subject { project_tags_path(project) } + subject { namespace_project_tags_path(project.namespace, project) } before do # Speed increase @@ -220,7 +220,7 @@ describe "Public Project Access", feature: true do end describe "GET /:project_path/hooks" do - subject { project_hooks_path(project) } + subject { namespace_project_hooks_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 61d6c906ad0..9d99b6e33cb 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -64,7 +64,7 @@ describe ApplicationHelper do project = create(:project) project.avatar = File.open(avatar_file_path) project.save! - expect(project_icon(project.to_param).to_s).to eq( + expect(project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to eq( "\"Gitlab" ) end @@ -75,8 +75,8 @@ describe ApplicationHelper do allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) - expect(project_icon(project.to_param).to_s).to match( - image_tag(project_avatar_path(project))) + expect(project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match( + image_tag(namespace_project_avatar_path(project.namespace, project))) end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 317a559f83c..2caeb0dd856 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -54,7 +54,7 @@ describe GitlabMarkdownHelper do end describe "referencing a commit" do - let(:expected) { project_commit_path(project, commit) } + let(:expected) { namespace_project_commit_path(project.namespace, project, commit) } it "should link using a full id" do actual = "Reverts #{commit.id}" @@ -146,7 +146,7 @@ describe GitlabMarkdownHelper do # Currently limited to Snippets, Issues and MergeRequests shared_examples 'referenced object' do let(:actual) { "Reference to #{reference}" } - let(:expected) { polymorphic_path([project, object]) } + let(:expected) { polymorphic_path([project.namespace, project, object]) } it "should link using a valid id" do expect(gfm(actual)).to match(expected) @@ -199,9 +199,9 @@ describe GitlabMarkdownHelper do let(:actual) { "Reference to #{full_reference}" } let(:expected) do if object.is_a?(Commit) - project_commit_path(@other_project, object) + namespace_project_commit_path(@other_project.namespace, @other_project, object) else - polymorphic_path([@other_project, object]) + polymorphic_path([@other_project.namespace, @other_project, object]) end end @@ -353,7 +353,7 @@ describe GitlabMarkdownHelper do let(:object) { snippet } let(:reference) { "$#{snippet.id}" } let(:actual) { "Reference to #{reference}" } - let(:expected) { project_snippet_path(project, object) } + let(:expected) { namespace_project_snippet_path(project.namespace, project, object) } it "should link using a valid id" do expect(gfm(actual)).to match(expected) @@ -395,17 +395,17 @@ describe GitlabMarkdownHelper do let(:actual) { "!#{merge_request.iid} -> #{commit.id} -> ##{issue.iid}" } it "should link to the merge request" do - expected = project_merge_request_path(project, merge_request) + expected = namespace_project_merge_request_path(project.namespace, project, merge_request) expect(gfm(actual)).to match(expected) end it "should link to the commit" do - expected = project_commit_path(project, commit) + expected = namespace_project_commit_path(project.namespace, project, commit) expect(gfm(actual)).to match(expected) end it "should link to the issue" do - expected = project_issue_path(project, issue) + expected = namespace_project_issue_path(project.namespace, project, issue) expect(gfm(actual)).to match(expected) end end @@ -458,7 +458,7 @@ describe GitlabMarkdownHelper do end describe "#link_to_gfm" do - let(:commit_path) { project_commit_path(project, commit) } + let(:commit_path) { namespace_project_commit_path(project.namespace, project, commit) } let(:issues) { create_list(:issue, 2, project: project) } it "should handle references nested in links with all the text" do @@ -474,7 +474,7 @@ describe GitlabMarkdownHelper do # First issue link expect(groups[1]). - to match(/href="#{project_issue_url(project, issues[0])}"/) + to match(/href="#{namespace_project_issue_url(project.namespace, project, issues[0])}"/) expect(groups[1]).to match(/##{issues[0].iid}$/) # Internal commit link @@ -483,7 +483,7 @@ describe GitlabMarkdownHelper do # Second issue link expect(groups[3]). - to match(/href="#{project_issue_url(project, issues[1])}"/) + to match(/href="#{namespace_project_issue_url(project.namespace, project, issues[1])}"/) expect(groups[3]).to match(/##{issues[1].iid}$/) # Trailing commit link @@ -506,7 +506,7 @@ describe GitlabMarkdownHelper do describe "#markdown" do it "should handle references in paragraphs" do actual = "\n\nLorem ipsum dolor sit amet. #{commit.id} Nam pulvinar sapien eget.\n" - expected = project_commit_path(project, commit) + expected = namespace_project_commit_path(project.namespace, project, commit) expect(markdown(actual)).to match(expected) end @@ -603,7 +603,7 @@ describe GitlabMarkdownHelper do end it "should leave ref-like href of 'manual' links untouched" do - expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("

    why not inspect !#{merge_request.iid}

    \n") + expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("

    why not inspect !#{merge_request.iid}

    \n") end it "should leave ref-like src of images untouched" do @@ -611,7 +611,7 @@ describe GitlabMarkdownHelper do end it "should generate absolute urls for refs" do - expect(markdown("##{issue.iid}")).to include(project_issue_url(project, issue)) + expect(markdown("##{issue.iid}")).to include(namespace_project_issue_url(project.namespace, project, issue)) end it "should generate absolute urls for emoji" do diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 7a8fd25e02d..54dd8d4aa64 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -29,7 +29,7 @@ describe IssuesHelper do project_url.gsub(':project_id', ext_project.id.to_s) .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) end - let(:int_expected) { polymorphic_path([project]) } + let(:int_expected) { polymorphic_path([@project.namespace, project]) } it "should return internal path if used internal tracker" do @project = project @@ -67,7 +67,7 @@ describe IssuesHelper do .gsub(':project_id', ext_project.id.to_s) .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) end - let(:int_expected) { polymorphic_path([project, issue]) } + let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) } it "should return internal path if used internal tracker" do @project = project @@ -104,7 +104,7 @@ describe IssuesHelper do issues_url.gsub(':project_id', ext_project.id.to_s) .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) end - let(:int_expected) { new_project_issue_path(project) } + let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) } it "should return internal path if used internal tracker" do @project = project diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index 3d80dc9d0a4..aef1108e333 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -19,28 +19,28 @@ describe SubmoduleHelper do Gitlab.config.gitlab_shell.stub(ssh_port: 22) # set this just to be sure Gitlab.config.gitlab_shell.stub(ssh_path_prefix: Settings.send(:build_gitlab_shell_ssh_path_prefix)) stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end it 'should detect ssh on non-standard port' do Gitlab.config.gitlab_shell.stub(ssh_port: 2222) Gitlab.config.gitlab_shell.stub(ssh_path_prefix: Settings.send(:build_gitlab_shell_ssh_path_prefix)) stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end it 'should detect http on standard port' do Gitlab.config.gitlab.stub(port: 80) Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end it 'should detect http on non-standard port' do Gitlab.config.gitlab.stub(port: 3000) Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end it 'should work with relative_url_root' do @@ -48,7 +48,7 @@ describe SubmoduleHelper do Gitlab.config.gitlab.stub(relative_url_root: '/gitlab/root') Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) stub_url([ 'http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git' ].join('')) - expect(submodule_links(submodule_item)).to eq([ project_path('gitlab-org/gitlab-ce'), project_tree_path('gitlab-org/gitlab-ce', 'hash') ]) + expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ]) end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index eb47bee8336..716430340b6 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::UrlBuilder do it 'returns the issue url' do issue = create(:issue) url = Gitlab::UrlBuilder.new(:issue).build(issue.id) - expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.to_param}/issues/#{issue.iid}" + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 64367ed9d80..3b09c618f2a 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -194,7 +194,7 @@ describe Notify do end it 'contains a link to the new issue' do - is_expected.to have_body_text /#{project_issue_path project, issue}/ + is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end end @@ -231,7 +231,7 @@ describe Notify do end it 'contains a link to the issue' do - is_expected.to have_body_text /#{project_issue_path project, issue}/ + is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end end @@ -260,7 +260,7 @@ describe Notify do end it 'contains a link to the issue' do - is_expected.to have_body_text /#{project_issue_path project, issue}/ + is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end end @@ -282,7 +282,7 @@ describe Notify do end it 'contains a link to the new merge request' do - is_expected.to have_body_text /#{project_merge_request_path(project, merge_request)}/ + is_expected.to have_body_text /#{namespace_project_merge_request_path(project.namespace, project, merge_request)}/ end it 'contains the source branch for the merge request' do @@ -331,7 +331,7 @@ describe Notify do end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/ + is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end end @@ -360,7 +360,7 @@ describe Notify do end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/ + is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end end @@ -385,7 +385,7 @@ describe Notify do end it 'contains a link to the merge request' do - is_expected.to have_body_text /#{project_merge_request_path project, merge_request}/ + is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ end end end @@ -477,7 +477,7 @@ describe Notify do describe 'on a merge request' do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let(:note_on_merge_request_path) { project_merge_request_path(project, merge_request, anchor: "note_#{note.id}") } + let(:note_on_merge_request_path) { namespace_project_merge_request_path(project.namespace, project, merge_request, anchor: "note_#{note.id}") } before(:each) { allow(note).to receive(:noteable).and_return(merge_request) } subject { Notify.note_merge_request_email(recipient.id, note.id) } @@ -496,7 +496,7 @@ describe Notify do describe 'on an issue' do let(:issue) { create(:issue, project: project) } - let(:note_on_issue_path) { project_issue_path(project, issue, anchor: "note_#{note.id}") } + let(:note_on_issue_path) { namespace_project_issue_path(project.namespace, project, issue, anchor: "note_#{note.id}") } before(:each) { allow(note).to receive(:noteable).and_return(issue) } subject { Notify.note_issue_email(recipient.id, note.id) } @@ -568,7 +568,7 @@ describe Notify do let(:user) { create(:user) } let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits) } - let(:diff_path) { project_compare_path(project, from: commits.first, to: commits.last) } + let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: commits.first, to: commits.last) } subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) } @@ -604,7 +604,7 @@ describe Notify do let(:user) { create(:user) } let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits) } - let(:diff_path) { project_commit_path(project, commits.first) } + let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ad7a0f0a1e3..a9df6f137b7 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -168,7 +168,7 @@ describe Project do @project = create(:project, name: 'gitlabhq', namespace: @group) end - it { expect(@project.to_param).to eq('gitlab/gitlabhq') } + it { expect(@project.to_param).to eq('gitlabhq') } end end diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 92542df52fd..bf8abcfb00f 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -71,7 +71,7 @@ describe Admin::ProjectsController, "routing" do end it "to #show" do - expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', id: 'gitlab') + expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab') end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 6b587345597..4308a765b56 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -25,31 +25,31 @@ shared_examples 'RESTful project resources' do let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } it 'to #index' do - expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", project_id: 'gitlab/gitlabhq') if actions.include?(:index) + expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index) end it 'to #create' do - expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", project_id: 'gitlab/gitlabhq') if actions.include?(:create) + expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create) end it 'to #new' do - expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", project_id: 'gitlab/gitlabhq') if actions.include?(:new) + expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new) end it 'to #edit' do - expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:edit) + expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit) end it 'to #show' do - expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:show) + expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show) end it 'to #update' do - expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:update) + expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update) end it 'to #destroy' do - expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:destroy) + expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) end end @@ -71,28 +71,28 @@ describe ProjectsController, 'routing' do end it 'to #edit' do - expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq') end it 'to #autocomplete_sources' do - expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq') end it 'to #show' do - expect(get('/gitlab/gitlabhq')).to route_to('projects#show', id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') end it 'to #update' do - expect(put('/gitlab/gitlabhq')).to route_to('projects#update', id: 'gitlab/gitlabhq') + expect(put('/gitlab/gitlabhq')).to route_to('projects#update', namespace_id: 'gitlab', id: 'gitlabhq') end it 'to #destroy' do - expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', id: 'gitlab/gitlabhq') + expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq') end it 'to #markdown_preview' do expect(post('/gitlab/gitlabhq/markdown_preview')).to( - route_to('projects#markdown_preview', id: 'gitlab/gitlabhq') + route_to('projects#markdown_preview', namespace_id: 'gitlab', id: 'gitlabhq') ) end end @@ -105,11 +105,11 @@ end # DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy describe Projects::WikisController, 'routing' do it 'to #pages' do - expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/wikis/pages')).to route_to('projects/wikis#pages', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #history' do - expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it_behaves_like 'RESTful project resources' do @@ -124,43 +124,43 @@ end # edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit describe Projects::RepositoriesController, 'routing' do it 'to #archive' do - expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/repository/archive')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #archive format:zip' do - expect(get('/gitlab/gitlabhq/repository/archive.zip')).to route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'zip') + expect(get('/gitlab/gitlabhq/repository/archive.zip')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'zip') end it 'to #archive format:tar.bz2' do - expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'tar.bz2') + expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2') end it 'to #show' do - expect(get('/gitlab/gitlabhq/repository')).to route_to('projects/repositories#show', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/repository')).to route_to('projects/repositories#show', namespace_id: 'gitlab', project_id: 'gitlabhq') end end describe Projects::BranchesController, 'routing' do it 'to #branches' do - expect(get('/gitlab/gitlabhq/branches')).to route_to('projects/branches#index', project_id: 'gitlab/gitlabhq') - expect(delete('/gitlab/gitlabhq/branches/feature%2345')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') - expect(delete('/gitlab/gitlabhq/branches/feature%2B45')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') - expect(delete('/gitlab/gitlabhq/branches/feature@45')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') - expect(delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') - expect(delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') - expect(delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') + expect(get('/gitlab/gitlabhq/branches')).to route_to('projects/branches#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(delete('/gitlab/gitlabhq/branches/feature%2345')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') + expect(delete('/gitlab/gitlabhq/branches/feature%2B45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') + expect(delete('/gitlab/gitlabhq/branches/feature@45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') + expect(delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz') end end describe Projects::TagsController, 'routing' do it 'to #tags' do - expect(get('/gitlab/gitlabhq/tags')).to route_to('projects/tags#index', project_id: 'gitlab/gitlabhq') - expect(delete('/gitlab/gitlabhq/tags/feature%2345')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') - expect(delete('/gitlab/gitlabhq/tags/feature%2B45')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') - expect(delete('/gitlab/gitlabhq/tags/feature@45')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') - expect(delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') - expect(delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') - expect(delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') + expect(get('/gitlab/gitlabhq/tags')).to route_to('projects/tags#index', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(delete('/gitlab/gitlabhq/tags/feature%2345')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') + expect(delete('/gitlab/gitlabhq/tags/feature%2B45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') + expect(delete('/gitlab/gitlabhq/tags/feature@45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') + expect(delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz') + expect(delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz') end end @@ -193,19 +193,19 @@ end # logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree describe Projects::RefsController, 'routing' do it 'to #switch' do - expect(get('/gitlab/gitlabhq/refs/switch')).to route_to('projects/refs#switch', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/refs/switch')).to route_to('projects/refs#switch', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #logs_tree' do - expect(get('/gitlab/gitlabhq/refs/stable/logs_tree')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable') - expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45') - expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45') - expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45') - expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'foo/bar/baz') - expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45', path: 'foo/bar/baz') - expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45', path: 'foo/bar/baz') - expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45', path: 'foo/bar/baz') - expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'files.scss') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable') + expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45') + expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45') + expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45', path: 'foo/bar/baz') + expect(get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss')).to route_to('projects/refs#logs_tree', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'stable', path: 'files.scss') end end @@ -223,31 +223,31 @@ end # DELETE /:project_id/merge_requests/:id(.:format) projects/merge_requests#destroy describe Projects::MergeRequestsController, 'routing' do it 'to #diffs' do - expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/merge_requests/1/diffs')).to route_to('projects/merge_requests#diffs', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it 'to #automerge' do expect(post('/gitlab/gitlabhq/merge_requests/1/automerge')).to route_to( 'projects/merge_requests#automerge', - project_id: 'gitlab/gitlabhq', id: '1' + namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1' ) end it 'to #automerge_check' do - expect(get('/gitlab/gitlabhq/merge_requests/1/automerge_check')).to route_to('projects/merge_requests#automerge_check', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/merge_requests/1/automerge_check')).to route_to('projects/merge_requests#automerge_check', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it 'to #branch_from' do - expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #branch_to' do - expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/merge_requests/branch_to')).to route_to('projects/merge_requests#branch_to', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #show' do - expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'diff') - expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'patch') + expect(get('/gitlab/gitlabhq/merge_requests/1.diff')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'diff') + expect(get('/gitlab/gitlabhq/merge_requests/1.patch')).to route_to('projects/merge_requests#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: 'patch') end it_behaves_like 'RESTful project resources' do @@ -266,35 +266,35 @@ end # DELETE /:project_id/snippets/:id(.:format) snippets#destroy describe SnippetsController, 'routing' do it 'to #raw' do - expect(get('/gitlab/gitlabhq/snippets/1/raw')).to route_to('projects/snippets#raw', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/snippets/1/raw')).to route_to('projects/snippets#raw', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it 'to #index' do - expect(get('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#index', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#index', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #create' do - expect(post('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#create', project_id: 'gitlab/gitlabhq') + expect(post('/gitlab/gitlabhq/snippets')).to route_to('projects/snippets#create', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #new' do - expect(get('/gitlab/gitlabhq/snippets/new')).to route_to('projects/snippets#new', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/snippets/new')).to route_to('projects/snippets#new', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #edit' do - expect(get('/gitlab/gitlabhq/snippets/1/edit')).to route_to('projects/snippets#edit', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/snippets/1/edit')).to route_to('projects/snippets#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it 'to #show' do - expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it 'to #update' do - expect(put('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#update', project_id: 'gitlab/gitlabhq', id: '1') + expect(put('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it 'to #destroy' do - expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', project_id: 'gitlab/gitlabhq', id: '1') + expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end end @@ -304,7 +304,7 @@ end # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy describe Projects::HooksController, 'routing' do it 'to #test' do - expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', project_id: 'gitlab/gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it_behaves_like 'RESTful project resources' do @@ -316,10 +316,10 @@ end # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /[[:alnum:]]{6,40}/, project_id: /[^\/]+/} describe Projects::CommitController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/commit/4246fb')).to route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb') - expect(get('/gitlab/gitlabhq/commit/4246fb.diff')).to route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'diff') - expect(get('/gitlab/gitlabhq/commit/4246fb.patch')).to route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'patch') - expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') + expect(get('/gitlab/gitlabhq/commit/4246fb')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb') + expect(get('/gitlab/gitlabhq/commit/4246fb.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'diff') + expect(get('/gitlab/gitlabhq/commit/4246fb.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'patch') + expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') end end @@ -334,7 +334,7 @@ describe Projects::CommitsController, 'routing' do end it 'to #show' do - expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', project_id: 'gitlab/gitlabhq', id: 'master', format: 'atom') + expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'atom') end end @@ -369,7 +369,7 @@ end # project_labels GET /:project_id/labels(.:format) labels#index describe Projects::LabelsController, 'routing' do it 'to #index' do - expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq') end end @@ -385,7 +385,7 @@ end # DELETE /:project_id/issues/:id(.:format) issues#destroy describe Projects::IssuesController, 'routing' do it 'to #bulk_update' do - expect(post('/gitlab/gitlabhq/issues/bulk_update')).to route_to('projects/issues#bulk_update', project_id: 'gitlab/gitlabhq') + expect(post('/gitlab/gitlabhq/issues/bulk_update')).to route_to('projects/issues#bulk_update', namespace_id: 'gitlab', project_id: 'gitlabhq') end it_behaves_like 'RESTful project resources' do @@ -407,26 +407,26 @@ end # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/} describe Projects::BlameController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') + expect(get('/gitlab/gitlabhq/blame/master/app/models/project.rb')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/blame/master/files.scss')).to route_to('projects/blame#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') end end # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/} describe Projects::BlobController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - expect(get('/gitlab/gitlabhq/blob/master/app/models/compare.rb')).to route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/compare.rb') - expect(get('/gitlab/gitlabhq/blob/master/app/models/diff.js')).to route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/diff.js') - expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') + expect(get('/gitlab/gitlabhq/blob/master/app/models/project.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/blob/master/app/models/compare.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/compare.rb') + expect(get('/gitlab/gitlabhq/blob/master/app/models/diff.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/diff.js') + expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') end end # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/} describe Projects::TreeController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') + expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') + expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') end end @@ -434,14 +434,14 @@ describe Projects::BlobController, 'routing' do it 'to #edit' do expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to( route_to('projects/blob#edit', - project_id: 'gitlab/gitlabhq', + namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')) end it 'to #preview' do expect(post('/gitlab/gitlabhq/preview/master/app/models/project.rb')).to( route_to('projects/blob#preview', - project_id: 'gitlab/gitlabhq', + namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')) end end @@ -451,39 +451,39 @@ end # project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/} describe Projects::CompareController, 'routing' do it 'to #index' do - expect(get('/gitlab/gitlabhq/compare')).to route_to('projects/compare#index', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/compare')).to route_to('projects/compare#index', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #compare' do - expect(post('/gitlab/gitlabhq/compare')).to route_to('projects/compare#create', project_id: 'gitlab/gitlabhq') + expect(post('/gitlab/gitlabhq/compare')).to route_to('projects/compare#create', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #show' do - expect(get('/gitlab/gitlabhq/compare/master...stable')).to route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'master', to: 'stable') - expect(get('/gitlab/gitlabhq/compare/issue/1234...stable')).to route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'issue/1234', to: 'stable') + expect(get('/gitlab/gitlabhq/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable') + expect(get('/gitlab/gitlabhq/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable') end end describe Projects::NetworkController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master') - expect(get('/gitlab/gitlabhq/network/master.json')).to route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master', format: 'json') + expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + expect(get('/gitlab/gitlabhq/network/master.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') end end describe Projects::GraphsController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master') + expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') end end describe Projects::ForksController, 'routing' do it 'to #new' do - expect(get('/gitlab/gitlabhq/fork/new')).to route_to('projects/forks#new', project_id: 'gitlab/gitlabhq') + expect(get('/gitlab/gitlabhq/fork/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #create' do - expect(post('/gitlab/gitlabhq/fork')).to route_to('projects/forks#create', project_id: 'gitlab/gitlabhq') + expect(post('/gitlab/gitlabhq/fork')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') end end @@ -491,6 +491,6 @@ end describe Projects::AvatarsController, 'routing' do it 'to #destroy' do expect(delete('/gitlab/gitlabhq/avatar')).to route_to( - 'projects/avatars#destroy', project_id: 'gitlab/gitlabhq') + 'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq') end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 9d0e41e4e8a..9924935094e 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -79,7 +79,17 @@ describe GitPushService do it { is_expected.to include(id: @commit.id) } it { is_expected.to include(message: @commit.safe_message) } it { is_expected.to include(timestamp: @commit.date.xmlschema) } - it { is_expected.to include(url: "#{Gitlab.config.gitlab.url}/#{project.to_param}/commit/#{@commit.id}") } + it do + is_expected.to include( + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + @commit.id + ].join('/') + ) + end context "with a author" do subject { @push_data[:commits].first[:author] } diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 46fb5f5fae5..5650626fb18 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -8,7 +8,7 @@ describe Projects::TransferService do context 'namespace -> namespace' do before do group.add_owner(user) - @result = transfer_project(project, user, namespace_id: group.id) + @result = transfer_project(project, user, new_namespace_id: group.id) end it { expect(@result).to be_truthy } @@ -17,7 +17,7 @@ describe Projects::TransferService do context 'namespace -> no namespace' do before do - @result = transfer_project(project, user, namespace_id: nil) + @result = transfer_project(project, user, new_namespace_id: nil) end it { expect(@result).not_to be_nil } # { result.should be_false } passes on nil @@ -27,7 +27,7 @@ describe Projects::TransferService do context 'namespace -> not allowed namespace' do before do - @result = transfer_project(project, user, namespace_id: group.id) + @result = transfer_project(project, user, new_namespace_id: group.id) end it { expect(@result).not_to be_nil } # { result.should be_false } passes on nil -- GitLab From c217860ae74381d75a4932fabb4417ac08b014b4 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Sat, 14 Feb 2015 20:19:13 +0100 Subject: [PATCH 0897/1609] Update charlock_holmes to 0.7.3 Version 0.6.9.4 fails to install on Gentoo Linux. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3283da40f8d..4418d80c0a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,7 +74,7 @@ GEM json (>= 1.7) celluloid (0.16.0) timers (~> 4.0.0) - charlock_holmes (0.6.9.4) + charlock_holmes (0.7.3) cliver (0.3.2) coderay (1.1.0) coercible (1.0.0) -- GitLab From ee804e2d94018f633c6e400b443ef514b5d7a10f Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Sat, 14 Feb 2015 12:13:31 -0800 Subject: [PATCH 0898/1609] Better English in the performance diff message. --- app/views/projects/diffs/_warning.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 86ed6bbeaa2..cdcd3d21167 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -15,5 +15,5 @@ %p To preserve performance only %strong #{allowed_diff_size} of #{diffs.size} - files displayed. + files are displayed. -- GitLab From 49d509935284c2f07c8f5cb53a31d5787c6ef7ab Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 14 Feb 2015 13:26:08 -0700 Subject: [PATCH 0899/1609] Avoid duplicate application rows Iterate over authorized applications instead of tokens to avoid multiple rows for the same authorized app. --- app/controllers/profiles_controller.rb | 3 +++ app/views/profiles/applications.html.haml | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index f7584c03411..e3e36505f0b 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -16,6 +16,9 @@ class ProfilesController < ApplicationController def applications @applications = current_user.oauth_applications @authorized_tokens = current_user.oauth_authorized_tokens + @authorized_apps = @authorized_tokens.map do |token| + token.application + end.uniq end def update diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index cb24e4a3dde..4b5817e10bf 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -36,12 +36,12 @@ %th Scope %th %tbody - - @authorized_tokens.each do |token| - - application = token.application - %tr{:id => "application_#{application.id}"} - %td= application.name + - @authorized_apps.each do |app| + - token = app.authorized_tokens.order('created_at desc').first + %tr{:id => "application_#{app.id}"} + %td= app.name %td= token.created_at %td= token.scopes - %td= render 'doorkeeper/authorized_applications/delete_form', application: application + %td= render 'doorkeeper/authorized_applications/delete_form', application: app - else %p.light You dont have any authorized applications -- GitLab From 1da7781cb313d4df2514a1bfc6ad125e9539371c Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 14 Feb 2015 20:11:45 -0700 Subject: [PATCH 0900/1609] Add items to "quick help" Modify the quick help list to add links to the pricing and feature comparison pages of about.gitlab.com. --- app/views/help/index.html.haml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 7b8193abfdf..64494e3e6b5 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -42,3 +42,9 @@ %li Use = link_to 'shortcuts', '#', onclick: 'Shortcuts.showHelp(event)' + %li + Get a + = link_to 'support subscription', 'https://about.gitlab.com/pricing/' + %li + = link_to 'Compare', 'https://about.gitlab.com/features/#compare' + GitLab editions -- GitLab From 655e9c6b4278102e78ffb7de13ca7b0b0f454c2a Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 14 Feb 2015 20:45:16 -0700 Subject: [PATCH 0901/1609] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 0bf8de6bd17..563e3bc0e5b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,6 +53,7 @@ v 7.8.0 (unreleased) - Show assignees in merge request index page (Kelvin Mutuma) - Link head panel titles to relevant root page. - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). + - Add quick help links to the GitLab pricing and feature comparison pages. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch -- GitLab From 020ec31eb5f09380e8cdcc877ffbdf27b0afb2d7 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 14 Feb 2015 20:47:57 -0700 Subject: [PATCH 0902/1609] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 0bf8de6bd17..74b24168a92 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,6 +53,7 @@ v 7.8.0 (unreleased) - Show assignees in merge request index page (Kelvin Mutuma) - Link head panel titles to relevant root page. - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). + - Fix duplicate authorized applications in user profile. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch -- GitLab From ebd00cc7f0da544f03e04464031085b66306a43f Mon Sep 17 00:00:00 2001 From: Zhang Sen Date: Sun, 15 Feb 2015 18:24:00 +0800 Subject: [PATCH 0903/1609] Fix wrong word in document of google oauth --- doc/integration/google.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/integration/google.md b/doc/integration/google.md index 7a78aff8ea4..51d740489da 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -45,9 +45,9 @@ To enable the Google OAuth2 OmniAuth provider you must register your application args: { access_type: 'offline', approval_prompt: '' } } ``` -1. Change 'YOUR APP ID' to the client ID from the GitHub application page from step 7. +1. Change 'YOUR APP ID' to the client ID from the Google Developer page from step 10. -1. Change 'YOUR APP SECRET' to the client secret from the GitHub application page from step 7. +1. Change 'YOUR APP SECRET' to the client secret from the Google Developer page from step 10. 1. Save the configuration file. -- GitLab From 3ab07b8aae8dae43cfa3aae1306c59ea264a8594 Mon Sep 17 00:00:00 2001 From: Bugagazavr Date: Sun, 15 Feb 2015 17:01:27 +0300 Subject: [PATCH 0904/1609] Update API branches documentation [ci skip] --- doc/api/branches.md | 131 +++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 80 deletions(-) diff --git a/doc/api/branches.md b/doc/api/branches.md index 319f0b47386..6a9c10c8520 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -15,27 +15,20 @@ Parameters: ```json [ { - "name": "master", "commit": { + "author_email": "john@example.com", + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "parents": [ - { - "id": "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - } - ], - "tree": "46e82de44b1061621357f24c05515327f2795a95", "message": "add projects API", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "John Smith", - "email": "john@example.com" - }, - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00" + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": true } ] @@ -56,27 +49,20 @@ Parameters: ```json { - "name": "master", "commit": { + "author_email": "john@example.com", + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "parents": [ - { - "id": "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - } - ], - "tree": "46e82de44b1061621357f24c05515327f2795a95", "message": "add projects API", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "John Smith", - "email": "john@example.com" - }, - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00" + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": true } ``` @@ -97,27 +83,20 @@ Parameters: ```json { - "name": "master", "commit": { + "author_email": "john@example.com", + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "parents": [ - { - "id": "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - } - ], - "tree": "46e82de44b1061621357f24c05515327f2795a95", "message": "add projects API", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "John Smith", - "email": "john@example.com" - }, - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00" + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": true } ``` @@ -138,27 +117,20 @@ Parameters: ```json { - "name": "master", "commit": { + "author_email": "john@example.com", + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "parents": [ - { - "id": "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - } - ], - "tree": "46e82de44b1061621357f24c05515327f2795a95", "message": "add projects API", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "John Smith", - "email": "john@example.com" - }, - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00" + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": false } ``` @@ -177,21 +149,20 @@ Parameters: ```json { - "name": "my-new-branch", "commit": { - "id": "8848c0e90327a0b70f1865b843fb2fbfb9345e57", - "message": "Merge pull request #54 from brightbox/use_fog_brightbox_module\n\nUpdate to use fog-brightbox module", - "parent_ids": [ - "fff449e0bf453576f16c91d6544f00a2664009d8", - "f93a93626fec20fd659f4ed3ab2e64019b6169ae" - ], - "authored_date": "2014-02-20T19:54:55+02:00", - "author_name": "john smith", "author_email": "john@example.com", - "committed_date": "2014-02-20T19:54:55+02:00", - "committer_name": "john smith", - "committer_email": "john@example.com" + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", + "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", + "message": "add projects API", + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8" + ] }, + "name": "master", "protected": false } ``` -- GitLab From 3d87cf8dc18eb8a37fc6996939c52a1ce508126d Mon Sep 17 00:00:00 2001 From: yglukhov Date: Tue, 27 Jan 2015 14:58:31 +0200 Subject: [PATCH 0905/1609] Make MR and issue numbers clickable links in MR and issue lists. --- app/views/projects/issues/_issue.html.haml | 2 +- app/views/projects/merge_requests/_merge_request.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index dc6510be858..9ccda38ade9 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -11,7 +11,7 @@ CLOSED .issue-info - %span.light= "##{issue.iid}" + = link_to "##{issue.iid}", project_issue_path(issue.project, issue), class: "light" - if issue.assignee assigned to #{link_to_member(@project, issue.assignee)} - if issue.votes_count > 0 diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 5afc87fb6b1..4e294064cb6 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -14,7 +14,7 @@ %i.fa.fa-angle-right.light = merge_request.target_branch .merge-request-info - %span.light= "##{merge_request.iid}" + = link_to "##{merge_request.iid}", project_merge_request_path(merge_request.target_project, merge_request), class: "light" - if merge_request.assignee assigned to #{link_to_member(merge_request.source_project, merge_request.assignee)} - else -- GitLab From 9e0467c38efb1a279b87cf17df5fb1b9b85aa0fe Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 15 Feb 2015 15:43:18 -0700 Subject: [PATCH 0906/1609] Fix application client count Display the number of unique users with an access token instead of the total number of access tokens per application in the admin area. --- CHANGELOG | 2 +- app/views/admin/applications/index.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 74b24168a92..bdcfe38d102 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,7 +53,7 @@ v 7.8.0 (unreleased) - Show assignees in merge request index page (Kelvin Mutuma) - Link head panel titles to relevant root page. - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). - - Fix duplicate authorized applications in user profile. + - Fix duplicate authorized applications in user profile and incorrect application client count in admin area. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index f2fed51eaf8..0632888dc82 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -17,6 +17,6 @@ %tr{:id => "application_#{application.id}"} %td= link_to application.name, admin_application_path(application) %td= application.redirect_uri - %td= application.access_tokens.count + %td= application.access_tokens.map { |t| t.resource_owner_id }.uniq.count %td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link' %td= render 'delete_form', application: application -- GitLab From b86caf0de317cfc4012f10d30a2e15eef471948e Mon Sep 17 00:00:00 2001 From: Bugagazavr Date: Mon, 16 Feb 2015 02:10:47 +0300 Subject: [PATCH 0907/1609] Correct json payload [ci skip] --- doc/api/repositories.md | 59 +++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 8acf85d21c8..33167453802 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -15,24 +15,21 @@ Parameters: ```json [ { - "name": "v1.0.0", "commit": { + "author_name": "John Smith", + "author_email": "john@example.com", + "authored_date": "2012-05-28T04:42:42-07:00", + "committed_date": "2012-05-28T04:42:42-07:00", + "committer_name": "Jack Smith", + "committer_email": "jack@example.com", "id": "2695effb5807a22ff3d138d593fd856244e155e7", - "parents": [], - "tree": "38017f2f189336fe4497e9d230c5bb1bf873f08d", "message": "Initial commit", - "author": { - "name": "John Smith", - "email": "john@example.com" - }, - "committer": { - "name": "Jack Smith", - "email": "jack@example.com" - }, - "authored_date": "2012-05-28T04:42:42-07:00", - "committed_date": "2012-05-28T04:42:42-07:00" + "parents_ids": [ + "2a4b78934375d7f53875269ffd4f45fd83a84ebe" + ] }, - "protected": null + "name": "v1.0.0", + "message": null } ] ``` @@ -53,23 +50,23 @@ Parameters: - `message` (optional) - Creates annotated tag. ```json -[ - { - "name": "v1.0.0", - "message": "Release 1.0.0", - "commit": { - "id": "2695effb5807a22ff3d138d593fd856244e155e7", - "parents": [], - "message": "Initial commit", - "authored_date": "2012-05-28T04:42:42-07:00", - "author_name": "John Smith", - "author email": "john@example.com", - "committer_name": "Jack Smith", - "committed_date": "2012-05-28T04:42:42-07:00", - "committer_email": "jack@example.com" - }, - } -] +{ + "commit": { + "author_name": "John Smith", + "author_email": "john@example.com", + "authored_date": "2012-05-28T04:42:42-07:00", + "committed_date": "2012-05-28T04:42:42-07:00", + "committer_name": "Jack Smith", + "committer_email": "jack@example.com", + "id": "2695effb5807a22ff3d138d593fd856244e155e7", + "message": "Initial commit", + "parents_ids": [ + "2a4b78934375d7f53875269ffd4f45fd83a84ebe" + ] + }, + "name": "v1.0.0", + "message": null +} ``` The message will be `nil` when creating a lightweight tag otherwise it will contain the annotation. -- GitLab From e0f61a59b4e9be1fc2f20320a0400793318b8a9f Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 15 Feb 2015 20:50:53 -0700 Subject: [PATCH 0908/1609] Use shorter map() syntax --- app/controllers/profiles_controller.rb | 4 +--- app/views/admin/applications/index.html.haml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index e3e36505f0b..a7863aba756 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -16,9 +16,7 @@ class ProfilesController < ApplicationController def applications @applications = current_user.oauth_applications @authorized_tokens = current_user.oauth_authorized_tokens - @authorized_apps = @authorized_tokens.map do |token| - token.application - end.uniq + @authorized_apps = @authorized_tokens.map(&:application).uniq end def update diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index 0632888dc82..d550278710e 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -17,6 +17,6 @@ %tr{:id => "application_#{application.id}"} %td= link_to application.name, admin_application_path(application) %td= application.redirect_uri - %td= application.access_tokens.map { |t| t.resource_owner_id }.uniq.count + %td= application.access_tokens.map(&:resource_owner_id).uniq.count %td= link_to 'Edit', edit_admin_application_path(application), class: 'btn btn-link' %td= render 'delete_form', application: application -- GitLab From 87b413592499ddcf1149d9e2b580f76a13bf625c Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Sun, 15 Feb 2015 21:51:23 -0800 Subject: [PATCH 0909/1609] Subscription should be a link on its own. --- app/views/help/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index 64494e3e6b5..af39dfeac5b 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -43,8 +43,8 @@ Use = link_to 'shortcuts', '#', onclick: 'Shortcuts.showHelp(event)' %li - Get a - = link_to 'support subscription', 'https://about.gitlab.com/pricing/' + Get a support + = link_to 'subscription', 'https://about.gitlab.com/pricing/' %li = link_to 'Compare', 'https://about.gitlab.com/features/#compare' GitLab editions -- GitLab From 958eaba3d9839ce6283933dad50c599758c554f3 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Mon, 16 Feb 2015 09:48:32 +0100 Subject: [PATCH 0910/1609] Fix typo in changelog Fixed small typo in changelog file. --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4d8fb3585e4..ebd34b85d86 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,7 +10,7 @@ v 7.8.0 (unreleased) - View note image attachments in new tab when clicked instead of downloading them - Improve sorting logic in UI and API. Explicitly define what sorting method is used by default - Allow more variations for commit messages closing issues (Julien Bianchi and Hannes Rosenögger) - - Fix overflow at sidebar when have several itens + - Fix overflow at sidebar when have several items - Add notes for label changes in issue and merge requests - Show tags in commit view (Hannes Rosenögger) - Only count a user's vote once on a merge request or issue (Michael Clarke) -- GitLab From 880fb9eac0888317db8bbf70587501ecfa115800 Mon Sep 17 00:00:00 2001 From: Daniel Steinborn Date: Mon, 16 Feb 2015 10:00:25 +0100 Subject: [PATCH 0911/1609] fixed rake task to block removed ldap users --- lib/tasks/gitlab/cleanup.rake | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 189ad6090a4..3c9802a0be4 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -90,13 +90,14 @@ namespace :gitlab do warn_user_is_not_gitlab block_flag = ENV['BLOCK'] - User.ldap.each do |ldap_user| - print "#{ldap_user.name} (#{ldap_user.extern_uid}) ..." - if Gitlab::LDAP::Access.allowed?(ldap_user) + User.find_each do |user| + next unless user.ldap_user? + print "#{user.name} (#{user.ldap_identity.extern_uid}) ..." + if Gitlab::LDAP::Access.allowed?(user) puts " [OK]".green else if block_flag - ldap_user.block! unless ldap_user.blocked? + user.block! unless user.blocked? puts " [BLOCKED]".red else puts " [NOT IN LDAP]".yellow -- GitLab From 2df8a91c8259711b3fb3d0ab3b31329aae869b96 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 16 Feb 2015 09:10:07 -0800 Subject: [PATCH 0912/1609] Rephrased wording in the documentation to say "installation from source" instead of "manual installation" or similar. --- README.md | 4 ++-- doc/README.md | 2 +- doc/hooks/custom_hooks.md | 2 +- doc/install/installation.md | 2 +- doc/install/requirements.md | 2 +- doc/raketasks/backup_restore.md | 2 +- doc/raketasks/import.md | 4 ++-- doc/update/README.md | 6 +++--- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8bfb301d1c7..b4f28a41be3 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Please see the [requirements documentation](doc/install/requirements.md) for sys ## Installation -The recommended way to install GitLab is using the provided [Omnibus packages](https://about.gitlab.com/downloads/). Compared to a manual installation, this is faster and less error prone. Just select your operating system, download the respective package (Debian or RPM) and install it using the system's package manager. +The recommended way to install GitLab is using the provided [Omnibus packages](https://about.gitlab.com/downloads/). Compared to an installation from source, this is faster and less error prone. Just select your operating system, download the respective package (Debian or RPM) and install it using the system's package manager. There are various other options to install GitLab, please refer to the [installation page on the GitLab website](https://about.gitlab.com/installation/) for more information. @@ -76,7 +76,7 @@ Since 2011 a minor or major version of GitLab is released on the 22nd of every m ## Upgrading -For updating the Omnibus installation please see the [update documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md). For manual installations there is an [upgrader script](doc/update/upgrader.md) and there are [upgrade guides](doc/update) detailing all necessary commands to migrate to the next version. +For updating the Omnibus installation please see the [update documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md). For installations from source there is an [upgrader script](doc/update/upgrader.md) and there are [upgrade guides](doc/update) detailing all necessary commands to migrate to the next version. ## Install a development environment diff --git a/doc/README.md b/doc/README.md index 79d4f5273ee..932e90e359a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,7 +13,7 @@ ## Administrator documentation -- [Install](install/README.md) Requirements, directory structures and manual installation. +- [Install](install/README.md) Requirements, directory structures and installation from source. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. - [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects. - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough. diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index 00867ead80d..f7d4f3de68b 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -24,7 +24,7 @@ set up a custom hook. 1. Pick a project that needs a custom git hook. 1. On the GitLab server, navigate to the project's repository directory. -For a manual install the path is usually +For an installation from source the path is usually `/home/git/repositories//.git`. For Omnibus installs the path is usually `/var/opt/gitlab/git-data/repositories//.git`. 1. Create a new directory in this location called `custom_hooks`. diff --git a/doc/install/installation.md b/doc/install/installation.md index bd81073c7eb..39ffe5052fc 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -2,7 +2,7 @@ ## Consider the Omnibus package installation -Since a manual installation is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). +Since an installation from source is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm). ## Select Version to Install diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 2cf9e82fd21..5bdb9caa2bf 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -22,7 +22,7 @@ For the installations options please see [the installation page on the GitLab we - FreeBSD On the above unsupported distributions is still possible to install GitLab yourself. -Please see the [manual installation guide](https://github.com/gitlabhq/gitlabhq/blob/master/doc/install/installation.md) and the [unofficial installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) on the public wiki for more information. +Please see the [installation from source guide](https://github.com/gitlabhq/gitlabhq/blob/master/doc/install/installation.md) and the [unofficial installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) on the public wiki for more information. ### Non-Unix operating systems such as Windows diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index bbcf395c745..99cdfff0ac6 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -137,7 +137,7 @@ with the name of your bucket: Please be informed that a backup does not store your configuration files. If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration). If you have a cookbook installation there should be a copy of your configuration in Chef. -If you have a manual installation please consider backing up your `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). +If you have an installation from source, please consider backing up your `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079). ## Restore a previously created backup diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md index 9a10c8d6850..8a38937062e 100644 --- a/doc/raketasks/import.md +++ b/doc/raketasks/import.md @@ -13,7 +13,7 @@ - For omnibus-gitlab, it is located at: `/var/opt/gitlab/git-data/repositories` by default, unless you changed it in the `/etc/gitlab/gitlab.rb` file. -- For manual installations, it is usually located at: `/home/git/repositories` or you can see where +- For installations from source, it is usually located at: `/home/git/repositories` or you can see where your repositories are located by looking at `config/gitlab.yml` under the `gitlab_shell => repos_path` entry. New folder needs to have git user ownership and read/write/execute access for git user and its group: @@ -47,7 +47,7 @@ with `/home/git`. $ sudo gitlab-rake gitlab:import:repos ``` -#### Manual Installation +#### Installation from source Before running this command you need to change the directory to where your GitLab installation is located: diff --git a/doc/update/README.md b/doc/update/README.md index 5380ddbd030..0472537eeb5 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -4,11 +4,11 @@ Depending on the installation method and your GitLab version, there are multiple - [Omnibus update guide](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/update.md) contains the steps needed to update a GitLab [package](https://about.gitlab.com/downloads/). -## Manual Installation +## Installation from source -- [The individual upgrade guides](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update) are for those who have installed GitLab manually. +- [The individual upgrade guides](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update) are for those who have installed GitLab from source. - [The CE to EE update guides](https://gitlab.com/subscribers/gitlab-ee/tree/master/doc/update) are for subscribers of the Enterprise Edition only. The steps are very similar to a version upgrade: stop the server, get the code, update config files for the new functionality, install libs and do migrations, update the init script, start the application and check the application status. -- [Upgrader](upgrader.md) is an automatic ruby script that performs the update for manual installations. +- [Upgrader](upgrader.md) is an automatic ruby script that performs the update for installations from source. - [Patch versions](patch_versions.md) guide includes the steps needed for a patch version, eg. 6.2.0 to 6.2.1. ## Miscellaneous -- GitLab From 29a997cde978d45b3d6bd75df8f9226288e3abcb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 16 Feb 2015 18:25:57 +0100 Subject: [PATCH 0913/1609] Properly clear notes bindings to prevent double comments on Ctrl-Enter. --- app/assets/javascripts/notes.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 47c5ecdedf1..c9c27a39f8d 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -77,7 +77,7 @@ class @Notes $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" - $(document).off "keypress", @notes_forms + $(document).off "keydown", @notes_forms $(document).off "keyup", ".js-note-text" $(document).off "click", ".js-note-target-reopen" $(document).off "click", ".js-note-target-close" -- GitLab From b0a9dbdfb132412837f4ca10cee1406560cb2b08 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 16 Feb 2015 10:14:07 -0800 Subject: [PATCH 0914/1609] Remove top margin for issue/mr title --- app/assets/stylesheets/generic/typography.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index 58243bc5ba2..c547ebb3aaf 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -17,6 +17,10 @@ h3.page-title { font-size: 22px; } +h4.page-title { + margin-top: 0px; +} + h6 { color: #888; text-transform: uppercase; @@ -131,4 +135,4 @@ textarea.js-gfm-input { .strikethrough { text-decoration: line-through; -} \ No newline at end of file +} -- GitLab From 7d5f86f6cbd187e75a6ba164ad6bfd036977dd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= Date: Mon, 9 Feb 2015 14:35:48 +0100 Subject: [PATCH 0915/1609] Fix broken access control and refactor avatar upload This commit moves the note folder from /public/uploads/note to /uploads/note and changes the uploader accordingly. Now it's no longer possible to avoid the access control by modifing the url. The Avatar upload has been refactored to use an own uploader as well to cleanly seperate the two upload types. --- CHANGELOG | 1 + app/controllers/files_controller.rb | 4 +- app/models/group.rb | 2 +- app/models/project.rb | 2 +- app/models/user.rb | 2 +- app/uploaders/attachment_uploader.rb | 8 +--- app/uploaders/avatar_uploader.rb | 32 +++++++++++++++ db/migrate/20150213111727_move_note_folder.rb | 19 +++++++++ features/steps/groups.rb | 2 +- features/steps/profile/profile.rb | 2 +- features/steps/project/project.rb | 2 +- lib/backup/manager.rb | 2 +- lib/backup/uploads.rb | 40 +++++++++++++------ uploads/.gitkeep | 0 14 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 app/uploaders/avatar_uploader.rb create mode 100644 db/migrate/20150213111727_move_note_folder.rb create mode 100644 uploads/.gitkeep diff --git a/CHANGELOG b/CHANGELOG index 4d8fb3585e4..5fe02ff4705 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ v 7.8.0 (unreleased) + - Fix broken access control for note attachments (Hannes Rosenögger) - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) - Include issue/mr participants in list of recipients for reassign/close/reopen emails diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index 9671245d3f4..561af8084c3 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -6,7 +6,9 @@ class FilesController < ApplicationController if uploader.file_storage? if can?(current_user, :read_project, note.project) disposition = uploader.image? ? 'inline' : 'attachment' - send_file uploader.file.path, disposition: disposition + # Replace old notes location in /public with the new one in / and send the file + path = uploader.file.path.gsub("#{Rails.root}/public",Rails.root.to_s) + send_file path, disposition: disposition else not_found! end diff --git a/app/models/group.rb b/app/models/group.rb index d6ec0be6081..da9621a2a1a 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -23,7 +23,7 @@ class Group < Namespace validate :avatar_type, if: ->(user) { user.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - mount_uploader :avatar, AttachmentUploader + mount_uploader :avatar, AvatarUploader after_create :post_create_hook after_destroy :post_destroy_hook diff --git a/app/models/project.rb b/app/models/project.rb index 56e1aa29040..e2c7f76eb09 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -138,7 +138,7 @@ class Project < ActiveRecord::Base if: ->(project) { project.avatar && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - mount_uploader :avatar, AttachmentUploader + mount_uploader :avatar, AvatarUploader # Scopes scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } diff --git a/app/models/user.rb b/app/models/user.rb index d4018f0c897..2ffcd1478d8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -177,7 +177,7 @@ class User < ActiveRecord::Base end end - mount_uploader :avatar, AttachmentUploader + mount_uploader :avatar, AvatarUploader # Scopes scope :admins, -> { where(admin: true) } diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index b122b6c8658..22742d287a4 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -3,10 +3,8 @@ class AttachmentUploader < CarrierWave::Uploader::Base storage :file - after :store, :reset_events_cache - def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "#{Rails.root}/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end def image? @@ -29,8 +27,4 @@ class AttachmentUploader < CarrierWave::Uploader::Base def file_storage? self.class.storage == CarrierWave::Storage::File end - - def reset_events_cache(file) - model.reset_events_cache if model.is_a?(User) - end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb new file mode 100644 index 00000000000..7cad044555b --- /dev/null +++ b/app/uploaders/avatar_uploader.rb @@ -0,0 +1,32 @@ +# encoding: utf-8 + +class AvatarUploader < CarrierWave::Uploader::Base + storage :file + + after :store, :reset_events_cache + + def store_dir + "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + end + + def image? + img_ext = %w(png jpg jpeg gif bmp tiff) + if file.respond_to?(:extension) + img_ext.include?(file.extension.downcase) + else + # Not all CarrierWave storages respond to :extension + ext = file.path.split('.').last.downcase + img_ext.include?(ext) + end + rescue + false + end + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end + + def reset_events_cache(file) + model.reset_events_cache if model.is_a?(User) + end +end diff --git a/db/migrate/20150213111727_move_note_folder.rb b/db/migrate/20150213111727_move_note_folder.rb new file mode 100644 index 00000000000..ca7f87d984f --- /dev/null +++ b/db/migrate/20150213111727_move_note_folder.rb @@ -0,0 +1,19 @@ +class MoveNoteFolder < ActiveRecord::Migration + def up + system( + "if [ -d '#{Rails.root}/public/uploads/note' ]; + then mv #{Rails.root}/public/uploads/note #{Rails.root}/uploads/note; + echo 'note folder has been moved successfully'; + else + echo 'note folder has already been moved or does not exist yet. Nothing to do here.'; fi") + end + + def down + system( + "if [ -d '#{Rails.root}/uploads/note' ]; + then mv #{Rails.root}/uploads/note #{Rails.root}/public/uploads/note; + echo 'note folder has been moved successfully'; + else + echo 'note folder has already been moved or does not exist yet. Nothing to do here.'; fi") + end +end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 610e7fd3a48..0a9b4ccba53 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -110,7 +110,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see new group "Owned" avatar' do - Group.find_by(name: "Owned").avatar.should be_instance_of AttachmentUploader + Group.find_by(name: "Owned").avatar.should be_instance_of AvatarUploader Group.find_by(name: "Owned").avatar.url.should == "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/gitlab_logo.png" end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index a907b0b7dcf..4efd2176782 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -29,7 +29,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see new avatar' do - @user.avatar.should be_instance_of AttachmentUploader + @user.avatar.should be_instance_of AvatarUploader @user.avatar.url.should == "/uploads/user/avatar/#{ @user.id }/gitlab_logo.png" end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 033d45e0253..d39c8e7d2db 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -35,7 +35,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see new project avatar' do - @project.avatar.should be_instance_of AttachmentUploader + @project.avatar.should be_instance_of AvatarUploader url = @project.avatar.url url.should == "/uploads/project/avatar/#{ @project.id }/gitlab_logo.png" end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index ab8db4e9837..06cd40a5b1c 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,6 +1,6 @@ module Backup class Manager - BACKUP_CONTENTS = %w{repositories/ db/ uploads/ backup_information.yml} + BACKUP_CONTENTS = %w{repositories/ db/ public/ uploads/ backup_information.yml} def pack # saving additional informations diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index e50e1ff4f13..75d8e18a862 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -1,29 +1,45 @@ module Backup class Uploads - attr_reader :app_uploads_dir, :backup_uploads_dir, :backup_dir + attr_reader :app_public_uploads_dir, :app_private_uploads_dir, :backup_public_uploads_dir, + :backup_private_uploads_dir, :backup_dir, :backup_public_dir def initialize - @app_uploads_dir = File.realpath(Rails.root.join('public', 'uploads')) + @app_public_uploads_dir = File.realpath(Rails.root.join('public', 'uploads')) + @app_private_uploads_dir = File.realpath(Rails.root.join('uploads')) @backup_dir = Gitlab.config.backup.path - @backup_uploads_dir = File.join(Gitlab.config.backup.path, 'uploads') + @backup_public_dir = File.join(backup_dir, 'public') + @backup_public_uploads_dir = File.join(backup_dir, 'public', 'uploads') + @backup_private_uploads_dir = File.join(backup_dir, 'uploads') end - # Copy uploads from public/uploads to backup/uploads + # Copy uploads from public/uploads to backup/public/uploads and from /uploads to backup/uploads def dump - FileUtils.mkdir_p(backup_uploads_dir) - FileUtils.cp_r(app_uploads_dir, backup_dir) + FileUtils.mkdir_p(backup_public_uploads_dir) + FileUtils.cp_r(app_public_uploads_dir, backup_public_dir) + + FileUtils.mkdir_p(backup_private_uploads_dir) + FileUtils.cp_r(app_private_uploads_dir, backup_dir) end def restore - backup_existing_uploads_dir + backup_existing_public_uploads_dir + backup_existing_private_uploads_dir - FileUtils.cp_r(backup_uploads_dir, app_uploads_dir) + FileUtils.cp_r(backup_public_uploads_dir, app_public_uploads_dir) + FileUtils.cp_r(backup_private_uploads_dir, app_private_uploads_dir) end - def backup_existing_uploads_dir - timestamped_uploads_path = File.join(app_uploads_dir, '..', "uploads.#{Time.now.to_i}") - if File.exists?(app_uploads_dir) - FileUtils.mv(app_uploads_dir, timestamped_uploads_path) + def backup_existing_public_uploads_dir + timestamped_public_uploads_path = File.join(app_public_uploads_dir, '..', "uploads.#{Time.now.to_i}") + if File.exists?(app_public_uploads_dir) + FileUtils.mv(app_public_uploads_dir, timestamped_public_uploads_path) + end + end + + def backup_existing_private_uploads_dir + timestamped_private_uploads_path = File.join(app_private_uploads_dir, '..', "uploads.#{Time.now.to_i}") + if File.exists?(app_private_uploads_dir) + FileUtils.mv(app_private_uploads_dir, timestamped_private_uploads_path) end end end diff --git a/uploads/.gitkeep b/uploads/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d -- GitLab From ebd39fc082b09177e0777e5de5729c3f98495e87 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 16 Feb 2015 18:42:52 +0100 Subject: [PATCH 0916/1609] Nitpicking. --- app/controllers/files_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index 561af8084c3..15523cbc2e7 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -5,9 +5,10 @@ class FilesController < ApplicationController if uploader.file_storage? if can?(current_user, :read_project, note.project) - disposition = uploader.image? ? 'inline' : 'attachment' # Replace old notes location in /public with the new one in / and send the file - path = uploader.file.path.gsub("#{Rails.root}/public",Rails.root.to_s) + path = uploader.file.path.gsub("#{Rails.root}/public", Rails.root.to_s) + + disposition = uploader.image? ? 'inline' : 'attachment' send_file path, disposition: disposition else not_found! -- GitLab From 47e38e5c0e516c5c09017760f495c53155fe26c1 Mon Sep 17 00:00:00 2001 From: Ewan Edwards Date: Mon, 16 Feb 2015 11:16:23 -0800 Subject: [PATCH 0917/1609] The "GitLab buttons in Gmail" document was not linked from anywhere else. It is now linked. --- doc/integration/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/integration/README.md b/doc/integration/README.md index 1fc8ab997ec..559a94533de 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -12,6 +12,8 @@ See the documentation below for details on how to configure these services. Jenkins support is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jenkins.html). +GitLab can also integrate with [Gmail](gitlab_buttons_in_gmail.md). + ## Project services Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a Project Service. -- GitLab From 7194810892d13712f3728524216d03661e66e942 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 3 Feb 2015 20:50:30 -0500 Subject: [PATCH 0918/1609] TestEnv improvements - Simplify cleaning the temporary testing path in TestEnv - Don't run gitlab:shell:install if it's already installed - Run git commands quietly --- spec/support/test_env.rb | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 1c150cbfe25..f869488d8d8 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -22,16 +22,7 @@ module TestEnv # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false - # Clean /tmp/tests - tmp_test_path = Rails.root.join('tmp', 'tests') - - if File.directory?(tmp_test_path) - Dir.entries(tmp_test_path).each do |entry| - unless ['.', '..', 'gitlab-shell', factory_repo_name].include?(entry) - FileUtils.rm_r(File.join(tmp_test_path, entry)) - end - end - end + clean_test_path FileUtils.mkdir_p(repos_path) @@ -50,15 +41,30 @@ module TestEnv allow_any_instance_of(NotificationService).to receive(:mailer).and_call_original end + # Clean /tmp/tests + # + # Keeps gitlab-shell and gitlab-test + def clean_test_path + tmp_test_path = Rails.root.join('tmp', 'tests', '**') + + Dir[tmp_test_path].each do |entry| + unless File.basename(entry) =~ /\Agitlab-(shell|test)\z/ + FileUtils.rm_rf(entry) + end + end + end + def setup_gitlab_shell - `rake gitlab:shell:install` + unless File.directory?(Rails.root.join(*%w(tmp tests gitlab-shell))) + `rake gitlab:shell:install` + end end def setup_factory_repo clone_url = "https://gitlab.com/gitlab-org/#{factory_repo_name}.git" unless File.directory?(factory_repo_path) - system(*%W(git clone #{clone_url} #{factory_repo_path})) + system(*%W(git clone -q #{clone_url} #{factory_repo_path})) end Dir.chdir(factory_repo_path) do @@ -79,7 +85,7 @@ module TestEnv end # We must copy bare repositories because we will push to them. - system(*%W(git clone --bare #{factory_repo_path} #{factory_repo_path_bare})) + system(*%W(git clone -q --bare #{factory_repo_path} #{factory_repo_path_bare})) end def copy_repo(project) @@ -101,7 +107,7 @@ module TestEnv end def factory_repo_path_bare - factory_repo_path.to_s + '_bare' + "#{factory_repo_path}_bare" end def factory_repo_name -- GitLab From d11c64a67ff0385cb4a3c3e3b52c023c083b7c57 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 16 Feb 2015 17:51:29 -0800 Subject: [PATCH 0919/1609] Updated the installation and update guides --- doc/install/installation.md | 6 +- doc/update/5.4-to-6.0.md | 3 + ...-or-7.x-to-7.7.md => 6.x-or-7.x-to-7.8.md} | 26 ++-- doc/update/7.7-to-7.8.md | 119 ++++++++++++++++++ 4 files changed, 138 insertions(+), 16 deletions(-) rename doc/update/{6.x-or-7.x-to-7.7.md => 6.x-or-7.x-to-7.8.md} (93%) create mode 100644 doc/update/7.7-to-7.8.md diff --git a/doc/install/installation.md b/doc/install/installation.md index 39ffe5052fc..f5dcec2f61e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -183,9 +183,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-6-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-8-stable gitlab -**Note:** You can change `7-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-8-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -280,7 +280,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.4.2] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.4.3] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md index d18c3fe8586..d9c6d9bfb91 100644 --- a/doc/update/5.4-to-6.0.md +++ b/doc/update/5.4-to-6.0.md @@ -5,6 +5,9 @@ GitLab 6.0 is affected by critical security vulnerabilities CVE-2013-4490 and CVE-2013-4489. +**You need to follow this guide first, before updating past 6.0, as it contains critical migration steps that are only present +in the `6-0-stable` branch** + ## Deprecations ### Global projects diff --git a/doc/update/6.x-or-7.x-to-7.7.md b/doc/update/6.x-or-7.x-to-7.8.md similarity index 93% rename from doc/update/6.x-or-7.x-to-7.7.md rename to doc/update/6.x-or-7.x-to-7.8.md index 8280cf2f38f..9cda434dc4e 100644 --- a/doc/update/6.x-or-7.x-to-7.7.md +++ b/doc/update/6.x-or-7.x-to-7.8.md @@ -1,7 +1,7 @@ -# From 6.x or 7.x to 7.7 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.4.md) for the most up to date instructions.* +# From 6.x or 7.x to 7.8 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.8.md) for the most up to date instructions.* -This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.7. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.8. ## Global issue numbers @@ -71,7 +71,7 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut For GitLab Community Edition: ```bash -sudo -u git -H git checkout 7-7-stable +sudo -u git -H git checkout 7-8-stable ``` OR @@ -79,7 +79,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 7-7-stable-ee +sudo -u git -H git checkout 7-8-stable-ee ``` ## 4. Install additional packages @@ -123,7 +123,7 @@ sudo apt-get install libkrb5-dev ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.4.1 +sudo -u git -H git checkout v2.4.3 ``` ## 7. Install libs, migrations, etc. @@ -158,14 +158,14 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7-7-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7-8-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-7-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-7-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/unicorn.rb.example but with your settings. * Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.4.0/config.yml.example but with your settings. -* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-7-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-7-stable/lib/support/nginx/gitlab-ssl but with your settings. +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stablef/lib/support/nginx/gitlab-ssl but with your settings. * Copy rack attack middleware config ```bash @@ -273,11 +273,11 @@ mysql> \q sudo -u git -H editor /home/git/gitlab/config/database.yml ``` -## Things went south? Revert to previous version (6.0) +## Things went south? Revert to previous version (7.0) ### 1. Revert the code to the previous version -Follow the [upgrade guide from 5.4 to 6.0](5.4-to-6.0.md), except for the database migration (the backup is already migrated to the previous version). +Follow the [upgrade guide from 6.9 to 7.0](6.9-to-7.0.md), except for the database migration (the backup is already migrated to the previous version). ### 2. Restore from the backup: diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md new file mode 100644 index 00000000000..01b4fc4c992 --- /dev/null +++ b/doc/update/7.7-to-7.8.md @@ -0,0 +1,119 @@ +# From 7.7 to 7.8 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-8-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.4.3 +``` + +### 4. Install libs, migrations, etc. + +```bash +sudo apt-get install libkrb5-dev + +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-6-stable:config/gitlab.yml.example origin/7-8-stable:config/gitlab.yml.example +``` + +#### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your setting + +#### Setup time zone (optional) + +Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it. + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +### 8. GitHub settings (if applicable) + +If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it +only contains a root URL (ex. `https://gitlab.example.com/`) + +## Things went south? Revert to previous version (7.6) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.5 to 7.6](7.5-to-7.6.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. -- GitLab From b6a678ef43f763b3c507ace0639b802207ed3cb7 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 16 Feb 2015 20:27:58 -0800 Subject: [PATCH 0920/1609] Use correct shell version in update doc. --- doc/update/6.x-or-7.x-to-7.8.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/update/6.x-or-7.x-to-7.8.md b/doc/update/6.x-or-7.x-to-7.8.md index 9cda434dc4e..90d889d5113 100644 --- a/doc/update/6.x-or-7.x-to-7.8.md +++ b/doc/update/6.x-or-7.x-to-7.8.md @@ -163,7 +163,7 @@ git diff 6-0-stable:config/gitlab.yml.example 7-8-stable:config/gitlab.yml.examp * Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/gitlab.yml.example but with your settings. * Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/unicorn.rb.example but with your settings. -* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.4.0/config.yml.example but with your settings. +* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.4.3/config.yml.example but with your settings. * HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab but with your settings. * HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stablef/lib/support/nginx/gitlab-ssl but with your settings. * Copy rack attack middleware config @@ -235,7 +235,7 @@ SET foreign_key_checks = 1; # Find MySQL users mysql> SELECT user FROM mysql.user WHERE user LIKE '%git%'; -# If git user exists and gitlab user does not exist +# If git user exists and gitlab user does not exist # you are done with the database cleanup tasks mysql> \q -- GitLab From 904c382f6abb3c1ee3b3a7c5de0579b1cf2e3bea Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Feb 2015 00:16:28 -0800 Subject: [PATCH 0921/1609] Fix code rendering for snippets --- app/views/shared/_file_highlight.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 52b48ff7451..fba69dd0f3f 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -7,4 +7,5 @@ = link_to "#L#{i}", id: "L#{i}", rel: "#L#{i}" do %i.fa.fa-link = i - = highlight(blob.name, blob.data) + :preserve + #{highlight(blob.name, blob.data)} -- GitLab From 3df135a66c01a2af933349996488889ea26a3048 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Feb 2015 00:16:28 -0800 Subject: [PATCH 0922/1609] Fix code rendering for snippets --- app/views/shared/_file_highlight.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 52b48ff7451..fba69dd0f3f 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -7,4 +7,5 @@ = link_to "#L#{i}", id: "L#{i}", rel: "#L#{i}" do %i.fa.fa-link = i - = highlight(blob.name, blob.data) + :preserve + #{highlight(blob.name, blob.data)} -- GitLab From 2c893cb380bba56149dcb5b1d6f6daba2af1d8fc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Feb 2015 00:23:09 -0800 Subject: [PATCH 0923/1609] Fix dev fixture for admin --- db/fixtures/development/01_admin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/fixtures/development/01_admin.rb b/db/fixtures/development/01_admin.rb index 1b2dec31323..bba2fc4b186 100644 --- a/db/fixtures/development/01_admin.rb +++ b/db/fixtures/development/01_admin.rb @@ -3,6 +3,7 @@ Gitlab::Seeder.quiet do s.id = 1 s.name = 'Administrator' s.email = 'admin@example.com' + s.notification_email = 'admin@example.com' s.username = 'root' s.password = '5iveL!fe' s.admin = true -- GitLab From 080449f8af56e1aa0d80c921d0bc6ea4a61f1c38 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Feb 2015 14:14:27 +0100 Subject: [PATCH 0924/1609] Make sure Markdown previews always use the same styling as the eventual destination. --- CHANGELOG | 1 + app/assets/javascripts/notes.js.coffee | 8 ++++---- app/views/projects/_issuable_form.html.haml | 2 +- app/views/projects/_md_preview.html.haml | 2 +- app/views/projects/merge_requests/_new_submit.html.haml | 2 +- app/views/projects/milestones/_form.html.haml | 2 +- app/views/projects/notes/_edit_form.html.haml | 2 +- app/views/projects/notes/_form.html.haml | 2 +- app/views/projects/wikis/_form.html.haml | 2 +- features/steps/project/merge_requests.rb | 4 ++-- features/steps/shared/note.rb | 2 +- spec/features/notes_on_merge_requests_spec.rb | 2 +- 12 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2e0d86862bf..85aabd3198b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -50,6 +50,7 @@ v 7.8.0 (unreleased) - Prevent losing unsaved comments by automatically restoring them when comment page is loaded again. - Don't allow page to be scaled on mobile. - Clean the username acquired from OAuth/LDAP so it doesn't fail username validation and block signing up. + - Make sure Markdown previews always use the same styling as the eventual destination. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 47c5ecdedf1..6d4eade1287 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -272,7 +272,7 @@ class @Notes note_li = $(".note-row-" + note.id) note_li.replaceWith(note.html) note_li.find('.note-edit-form').hide() - note_li.find('.note-text').show() + note_li.find('.note-body > .note-text').show() ### Called in response to clicking the edit note link @@ -284,7 +284,7 @@ class @Notes showEditForm: (e) -> e.preventDefault() note = $(this).closest(".note") - note.find(".note-text").hide() + note.find(".note-body > .note-text").hide() note.find(".note-header").hide() base_form = note.find(".note-edit-form") form = base_form.clone().insertAfter(base_form) @@ -311,7 +311,7 @@ class @Notes cancelEdit: (e) -> e.preventDefault() note = $(this).closest(".note") - note.find(".note-text").show() + note.find(".note-body > .note-text").show() note.find(".note-header").show() note.find(".current-note-edit-form").remove() @@ -345,7 +345,7 @@ class @Notes removeAttachment: -> note = $(this).closest(".note") note.find(".note-attachment").remove() - note.find(".note-text").show() + note.find(".note-body > .note-text").show() note.find(".js-note-attachment-delete").hide() note.find(".note-edit-form").hide() diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 9e2e214b3e8..5a57673b584 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -15,7 +15,7 @@ = f.label :description, 'Description', class: 'control-label' .col-sm-10 - = render layout: 'projects/md_preview' do + = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' .col-sm-12.hint diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index cb75149434f..d7d5c8a3401 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -10,4 +10,4 @@ .md-write-holder = yield .md-preview-holder.hide - .js-md-preview + .js-md-preview{class: (preview_class if defined?(preview_class))} diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index ac374532ffd..bca3e45bf19 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -19,7 +19,7 @@ .form-group.issuable-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 - = render layout: 'projects/md_preview' do + = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' .col-sm-12-hint diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 0f51a347f01..b3b170d7114 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -21,7 +21,7 @@ .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 - = render layout: 'projects/md_preview' do + = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' .hint .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 59e2b3f1b0b..cdc76f5d96f 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,6 +1,6 @@ .note-edit-form = form_for note, url: project_note_path(@project, note), method: :put, remote: true, authenticity_token: true do |f| - = render layout: 'projects/md_preview' do + = render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 3879a0f10da..1a4e06289f8 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -5,7 +5,7 @@ = f.hidden_field :noteable_id = f.hidden_field :noteable_type - = render layout: 'projects/md_preview' do + = render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 111484c8316..84731e43e95 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -22,7 +22,7 @@ .form-group.wiki-content = f.label :content, class: 'control-label' .col-sm-10 - = render layout: 'projects/md_preview' do + = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do = render 'projects/zen', f: f, attr: :content, classes: 'description form-control' .col-sm-12.hint .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'} diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 6f421de1aba..c97c3075c53 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -253,7 +253,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should still see a comment like "Line is correct" in the first file' do - within '.files [id^=diff]:nth-child(1) .note-text' do + within '.files [id^=diff]:nth-child(1) .note-body > .note-text' do page.should have_visible_content "Line is correct" end end @@ -271,7 +271,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see comments on the side-by-side diff page' do - within '.files [id^=diff]:nth-child(1) .parallel .note-text' do + within '.files [id^=diff]:nth-child(1) .parallel .note-body > .note-text' do page.should have_visible_content "Line is correct" end end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 625bcc0b266..45773056953 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -116,7 +116,7 @@ module SharedNote end step 'The comment with the header should not have an ID' do - within(".note-text") do + within(".note-body > .note-text") do page.should have_content("Comment with a header") page.should_not have_css("#comment-with-a-header") end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 7790d0ecd73..76d1a72bdb6 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -81,7 +81,7 @@ describe 'Comments' do within("#note_#{note.id}") do expect(find('.current-note-edit-form', visible: true)).to be_visible expect(find('.note-edit-form', visible: true)).to be_visible - expect(find(:css, '.note-text', visible: false)).not_to be_visible + expect(find(:css, '.note-body > .note-text', visible: false)).not_to be_visible end end -- GitLab From 57e91940dd719564f395116c7d91870396b0d58b Mon Sep 17 00:00:00 2001 From: Marco Cyriacks Date: Tue, 17 Feb 2015 20:52:03 +0100 Subject: [PATCH 0925/1609] Add new style to "New group" button The new group button style was changed to have the same look as the new project button (green background). --- app/assets/stylesheets/sections/dashboard.scss | 9 +++++++++ app/views/dashboard/_groups.html.haml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index 77d403cc687..feb9a4ad295 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -120,6 +120,15 @@ } } +.dash-new-group { + background: $bg_success; + border: 1px solid $border_success; + + a { + color: #FFF; + } +} + .dash-list .str-truncated { max-width: 72%; } diff --git a/app/views/dashboard/_groups.html.haml b/app/views/dashboard/_groups.html.haml index ddf44270802..e3df43d8892 100644 --- a/app/views/dashboard/_groups.html.haml +++ b/app/views/dashboard/_groups.html.haml @@ -3,7 +3,7 @@ .input-group = search_field_tag :filter_group, nil, placeholder: 'Filter by name', class: 'dash-filter form-control' - if current_user.can_create_group? - .input-group-addon + .input-group-addon.dash-new-group = link_to new_group_path, class: "" do %strong New group %ul.well-list.dash-list -- GitLab From c3e05fc321c6c99a4e31216721f91699ef6469a1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Feb 2015 13:15:29 -0800 Subject: [PATCH 0926/1609] Bump gitlab-shell version --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 35cee72dcbf..437459cd94c 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.4.3 +2.5.0 -- GitLab From 9bf8480b4a0d3ea6e284c4bd8bf26243f3f3f6f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Sat, 14 Feb 2015 16:04:45 +0100 Subject: [PATCH 0927/1609] Generalize the image upload in markdown This commit generalizes the image upload via drag and drop so it supports all files. It also adds access control for these files. --- CHANGELOG | 1 + .../javascripts/dropzone_input.js.coffee | 21 +++-- app/controllers/files_controller.rb | 29 ++++++- app/controllers/projects_controller.rb | 18 ++--- app/services/projects/file_service.rb | 55 +++++++++++++ app/services/projects/image_service.rb | 39 --------- app/uploaders/file_uploader.rb | 19 ++++- app/views/projects/_issuable_form.html.haml | 2 +- app/views/projects/issues/_form.html.haml | 2 +- .../projects/merge_requests/_form.html.haml | 2 +- .../merge_requests/_new_submit.html.haml | 5 +- app/views/projects/milestones/_form.html.haml | 4 +- app/views/projects/notes/_edit_form.html.haml | 2 +- app/views/projects/notes/_form.html.haml | 6 +- app/views/projects/wikis/_form.html.haml | 5 +- config/routes.rb | 5 +- spec/controllers/projects_controller_spec.rb | 36 +++++---- spec/services/projects/file_service_spec.rb | 81 +++++++++++++++++++ spec/services/projects/image_service_spec.rb | 62 -------------- 19 files changed, 241 insertions(+), 153 deletions(-) create mode 100644 app/services/projects/file_service.rb delete mode 100644 app/services/projects/image_service.rb create mode 100644 spec/services/projects/file_service_spec.rb delete mode 100644 spec/services/projects/image_service_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 05d1e7bdb40..9e50178d8b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ v 7.8.0 (unreleased) - Fix broken access control for note attachments (Hannes Rosenögger) + - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) - Include issue/mr participants in list of recipients for reassign/close/reopen emails diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index d98d5482937..bed8471b39a 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -9,7 +9,7 @@ class @DropzoneInput iconPicture = "" iconSpinner = "" btnAlert = "" - project_image_path_upload = window.project_image_path_upload or null + project_file_path_upload = window.project_file_path_upload or null form_textarea = $(form).find("textarea.markdown-area") form_textarea.wrap "
    " @@ -72,13 +72,12 @@ class @DropzoneInput form.find(".md-preview-holder").hide() dropzone = form_dropzone.dropzone( - url: project_image_path_upload + url: project_file_path_upload dictDefaultMessage: "" clickable: true - paramName: "markdown_img" + paramName: "markdown_file" maxFilesize: 10 uploadMultiple: false - acceptedFiles: "image/jpg,image/jpeg,image/gif,image/png" headers: "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") @@ -133,7 +132,10 @@ class @DropzoneInput child = $(dropzone[0]).children("textarea") formatLink = (str) -> - "![" + str.alt + "](" + str.url + ")" + text = "[" + str.alt + "](" + str.url + ")" + if str.is_image is true + text = "!" + text + text handlePaste = (event) -> pasteEvent = event.originalEvent @@ -177,9 +179,9 @@ class @DropzoneInput uploadFile = (item, filename) -> formData = new FormData() - formData.append "markdown_img", item, filename + formData.append "markdown_file", item, filename $.ajax - url: project_image_path_upload + url: project_file_path_upload type: "POST" data: formData dataType: "json" @@ -234,4 +236,7 @@ class @DropzoneInput return formatLink: (str) -> - "![" + str.alt + "](" + str.url + ")" + text = "[" + str.alt + "](" + str.url + ")" + if str.is_image is true + text = "!" + text + text diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index 15523cbc2e7..a86340dd9bb 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -1,5 +1,5 @@ class FilesController < ApplicationController - def download + def download_notes note = Note.find(params[:id]) uploader = note.attachment @@ -14,7 +14,32 @@ class FilesController < ApplicationController not_found! end else - redirect_to uploader.url + not_found! end end + + def download_files + namespace_id = params[:namespace] + project_id = params[:project] + folder_id = params[:folder_id] + filename = params[:filename] + project_with_namespace="#{namespace_id}/#{project_id}" + filename_with_id="#{folder_id}/#{filename}" + + project = Project.find_with_namespace(project_with_namespace) + + uploader = FileUploader.new("#{Rails.root}/uploads","#{project_with_namespace}/#{folder_id}") + uploader.retrieve_from_store!(filename) + + if can?(current_user, :read_project, project) + download(uploader) + else + not_found! + end + end + + def download(uploader) + disposition = uploader.image? ? 'inline' : 'attachment' + send_file uploader.file.path, disposition: disposition + end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 462ab3d4749..b430278903a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -134,12 +134,13 @@ class ProjectsController < ApplicationController end end - def upload_image - link_to_image = ::Projects::ImageService.new(repository, params, root_url).execute + def upload_file + link_to_file = ::Projects::FileService.new(repository, params, root_url). + execute respond_to do |format| - if link_to_image - format.json { render json: { link: link_to_image } } + if link_to_file + format.json { render json: { link: link_to_file } } else format.json { render json: 'Invalid file.', status: :unprocessable_entity } end @@ -158,13 +159,8 @@ class ProjectsController < ApplicationController private - def upload_path - base_dir = FileUploader.generate_dir - File.join(repository.path_with_namespace, base_dir) - end - - def accepted_images - %w(png jpg jpeg gif) + def invalid_file(error) + render json: { message: error.message }, status: :internal_server_error end def set_title diff --git a/app/services/projects/file_service.rb b/app/services/projects/file_service.rb new file mode 100644 index 00000000000..8c149bf53a1 --- /dev/null +++ b/app/services/projects/file_service.rb @@ -0,0 +1,55 @@ +module Projects + class FileService < BaseService + include Rails.application.routes.url_helpers + def initialize(repository, params, root_url) + @repository, @params, @root_url = repository, params.dup, root_url + end + + def execute + uploader = FileUploader.new("#{Rails.root}/uploads", upload_path, accepted_files) + file = @params['markdown_file'] + + if file + alt = file.original_filename + uploader.store!(file) + filename = nil + if image?(file) + filename=File.basename(alt, '.*') + else + filename=File.basename(alt) + end + link = { + 'alt' => filename, + 'url' => uploader.secure_url, + 'is_image' => image?(file) + } + else + link = nil + end + end + + protected + + def accepted_files + # insert accepted mime types here (e.g %w(jpg jpeg gif png)) + nil + end + + def accepted_images + %w(jpg jpeg gif png) + end + + def image?(file) + accepted_images.map { |format| file.content_type.include? format }.any? + end + + def upload_path + base_dir = FileUploader.generate_dir + File.join(@repository.path_with_namespace, base_dir) + end + + def correct_mime_type?(file) + accepted_files.map { |format| image.content_type.include? format }.any? + end + end +end diff --git a/app/services/projects/image_service.rb b/app/services/projects/image_service.rb deleted file mode 100644 index 7ca7e82c4a3..00000000000 --- a/app/services/projects/image_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Projects - class ImageService < BaseService - include Rails.application.routes.url_helpers - def initialize(repository, params, root_url) - @repository, @params, @root_url = repository, params.dup, root_url - end - - def execute - uploader = FileUploader.new('uploads', upload_path, accepted_images) - image = @params['markdown_img'] - - if image && correct_mime_type?(image) - alt = image.original_filename - uploader.store!(image) - link = { - 'alt' => File.basename(alt, '.*'), - 'url' => File.join(@root_url, uploader.url) - } - else - link = nil - end - end - - protected - - def upload_path - base_dir = FileUploader.generate_dir - File.join(@repository.path_with_namespace, base_dir) - end - - def accepted_images - %w(png jpg jpeg gif) - end - - def correct_mime_type?(image) - accepted_images.map{ |format| image.content_type.include? format }.any? - end - end -end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 0fa987c93f6..ac7bd5b27ec 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -21,7 +21,7 @@ class FileUploader < CarrierWave::Uploader::Base end def extension_white_list - @allowed_extensions + @allowed_extensions || super end def store!(file) @@ -38,4 +38,21 @@ class FileUploader < CarrierWave::Uploader::Base def self.generate_dir SecureRandom.hex(5) end + + def secure_url + Gitlab.config.gitlab.relative_url_root + "/files/#{@path}/#{@filename}" + end + + def image? + img_ext = %w(png jpg jpeg gif bmp tiff) + if file.respond_to?(:extension) + img_ext.include?(file.extension.downcase) + else + # Not all CarrierWave storages respond to :extension + ext = file.path.split('.').last.downcase + img_ext.include?(ext) + end + rescue + false + end end diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 5a57673b584..18897b055aa 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -23,7 +23,7 @@ Parsed with #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. .pull-right - Attach images (JPG, PNG, GIF) by dragging & dropping + Attach files by dragging & dropping or #{link_to 'selecting them', '#', class: 'markdown-selector' }. .clearfix diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 2a7b44955cd..975980bd6bd 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -11,4 +11,4 @@ e.preventDefault(); }); - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_file_path_upload = "#{upload_file_project_path @project}"; diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index d52e64666a0..28c4734e14f 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -9,4 +9,4 @@ e.preventDefault(); }); - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_file_path_upload = "#{upload_file_project_path @project}"; diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index bca3e45bf19..0653b30fcca 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -27,7 +27,7 @@ Parsed with #{link_to 'Gitlab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}. .pull-right - Attach images (JPG, PNG, GIF) by dragging & dropping + Attach files by dragging & dropping or #{link_to 'selecting them', '#', class: 'markdown-selector'}. .clearfix @@ -113,10 +113,11 @@ e.preventDefault(); }); - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_file_path_upload = "#{upload_file_project_path @project}"; :javascript var merge_request merge_request = new MergeRequest({ action: 'commits' }); + diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index b3b170d7114..5fbb668570c 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -25,7 +25,7 @@ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control' .hint .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}. - .pull-left Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. .clearfix .error-alert .col-md-6 @@ -51,4 +51,4 @@ onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_file_path_upload = "#{upload_file_project_path @project}"; diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index cdc76f5d96f..4ba59078318 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -6,7 +6,7 @@ .comment-hints.clearfix .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. + .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. .note-form-actions .buttons diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 1a4e06289f8..fe3dab569f4 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f| += form_for [@project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form" }, authenticity_token: true do |f| = note_target_fields = f.hidden_field :commit_id = f.hidden_field :line_code @@ -11,7 +11,7 @@ .comment-hints.clearfix .pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }} - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. + .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }. .note-form-actions @@ -29,4 +29,4 @@ = f.file_field :attachment, class: "js-note-attachment-input hidden" :javascript - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_file_path_upload = "#{upload_file_project_path @project}"; diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 84731e43e95..0afee138c8b 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -26,7 +26,7 @@ = render 'projects/zen', f: f, attr: :content, classes: 'description form-control' .col-sm-12.hint .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'} - .pull-right Attach images (JPG, PNG, GIF) by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. + .pull-right Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }. .clearfix .error-alert @@ -43,5 +43,6 @@ = link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel" :javascript - window.project_image_path_upload = "#{upload_image_project_path @project}"; + window.project_file_path_upload = "#{upload_file_project_path @project}"; + diff --git a/config/routes.rb b/config/routes.rb index 65786d83566..bd659ddeabd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -93,7 +93,8 @@ Gitlab::Application.routes.draw do # # Attachments serving # - get 'files/:type/:id/:filename' => 'files#download', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ } + get 'files/:type/:id/:filename' => 'files#download_notes', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ } + get 'files/:namespace/:project/:folder_id/:filename' => 'files#download_files', constraints: { namespace: /[^\/]+/, project: /[a-zA-Z.\/0-9_\-]+/, filename: /.+/ } # # Admin Area @@ -220,7 +221,7 @@ Gitlab::Application.routes.draw do put :transfer post :archive post :unarchive - post :upload_image + post :upload_file post :toggle_star post :markdown_preview get :autocomplete_sources diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index ef786ccd324..039751a41e8 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -7,43 +7,49 @@ describe ProjectsController do let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } - describe "POST #upload_image" do + describe 'POST #upload_file' do before do sign_in(user) project.team << [user, :developer] end - context "without params['markdown_img']" do - it "returns an error" do - post :upload_image, id: project.to_param, format: :json + context "without params['markdown_file']" do + it 'returns an error' do + post :upload_file, id: project.to_param, format: :json expect(response.status).to eq(422) end end - context "with invalid file" do + context 'with valid image' do before do - post :upload_image, id: project.to_param, markdown_img: txt, format: :json + post :upload_file, + id: project.to_param, + markdown_file: jpg, + format: :json end - it "returns an error" do - expect(response.status).to eq(422) + it 'returns a content with original filename, new link, and correct type.' do + expect(response.body).to match '\"alt\":\"rails_sample\"' + expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}" + expect(response.body).to match '\"is_image\":true' end end - context "with valid file" do + context 'with valid non-image file' do before do - post :upload_image, id: project.to_param, markdown_img: jpg, format: :json + post :upload_file, id: project.to_param, markdown_file: txt, format: :json end - it "returns a content with original filename and new link." do - expect(response.body).to match "\"alt\":\"rails_sample\"" + it 'returns a content with original filename, new link, and correct type.' do + expect(response.body).to match '\"alt\":\"doc_sample.txt\"' expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}" + expect(response.body).to match '\"is_image\":false' end end end - describe "POST #toggle_star" do - it "toggles star if user is signed in" do + describe 'POST #toggle_star' do + it 'toggles star if user is signed in' do sign_in(user) expect(user.starred?(public_project)).to be_falsey post :toggle_star, id: public_project.to_param @@ -52,7 +58,7 @@ describe ProjectsController do expect(user.starred?(public_project)).to be_falsey end - it "does nothing if user is not signed in" do + it 'does nothing if user is not signed in' do post :toggle_star, id: public_project.to_param expect(user.starred?(public_project)).to be_falsey post :toggle_star, id: public_project.to_param diff --git a/spec/services/projects/file_service_spec.rb b/spec/services/projects/file_service_spec.rb new file mode 100644 index 00000000000..38ab4a467bb --- /dev/null +++ b/spec/services/projects/file_service_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe Projects::FileService do + describe 'File service' do + before do + @user = create :user + @project = create :project, creator_id: @user.id, namespace: @user.namespace + end + + context 'for valid gif file' do + before do + gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + @link_to_file = upload_file(@project.repository, + { 'markdown_file' => gif }, + 'http://test.example/') + end + + it { expect(@link_to_file).to have_key('alt') } + it { expect(@link_to_file).to have_key('url') } + it { expect(@link_to_file).to have_key('is_image') } + it { expect(@link_to_file).to have_value('banana_sample') } + it { expect(@link_to_file['is_image']).to equal(true) } + it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match('banana_sample.gif') } + end + + context 'for valid png file' do + before do + png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', + 'image/png') + @link_to_file = upload_file(@project.repository, + { 'markdown_file' => png }, + 'http://test.example/') + end + + it { expect(@link_to_file).to have_key('alt') } + it { expect(@link_to_file).to have_key('url') } + it { expect(@link_to_file).to have_value('dk') } + it { expect(@link_to_file).to have_key('is_image') } + it { expect(@link_to_file['is_image']).to equal(true) } + it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match('dk.png') } + end + + context 'for valid jpg file' do + before do + jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') + @link_to_file = upload_file(@project.repository, { 'markdown_file' => jpg }, 'http://test.example/') + end + + it { expect(@link_to_file).to have_key('alt') } + it { expect(@link_to_file).to have_key('url') } + it { expect(@link_to_file).to have_key('is_image') } + it { expect(@link_to_file).to have_value('rails_sample') } + it { expect(@link_to_file['is_image']).to equal(true) } + it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match('rails_sample.jpg') } + end + + context 'for txt file' do + before do + txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') + @link_to_file = upload_file(@project.repository, + { 'markdown_file' => txt }, + 'http://test.example/') + end + + it { expect(@link_to_file).to have_key('alt') } + it { expect(@link_to_file).to have_key('url') } + it { expect(@link_to_file).to have_key('is_image') } + it { expect(@link_to_file).to have_value('doc_sample.txt') } + it { expect(@link_to_file['is_image']).to equal(false) } + it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match('doc_sample.txt') } + end + end + + def upload_file(repository, params, root_url) + Projects::FileService.new(repository, params, root_url).execute + end +end diff --git a/spec/services/projects/image_service_spec.rb b/spec/services/projects/image_service_spec.rb deleted file mode 100644 index 23c4e227ae3..00000000000 --- a/spec/services/projects/image_service_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'spec_helper' - -describe Projects::ImageService do - describe 'Image service' do - before do - @user = create :user - @project = create :project, creator_id: @user.id, namespace: @user.namespace - end - - context 'for valid gif file' do - before do - gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') - @link_to_image = upload_image(@project.repository, { 'markdown_img' => gif }, "http://test.example/") - end - - it { expect(@link_to_image).to have_key("alt") } - it { expect(@link_to_image).to have_key("url") } - it { expect(@link_to_image).to have_value("banana_sample") } - it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") } - it { expect(@link_to_image["url"]).to match("banana_sample.gif") } - end - - context 'for valid png file' do - before do - png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') - @link_to_image = upload_image(@project.repository, { 'markdown_img' => png }, "http://test.example/") - end - - it { expect(@link_to_image).to have_key("alt") } - it { expect(@link_to_image).to have_key("url") } - it { expect(@link_to_image).to have_value("dk") } - it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") } - it { expect(@link_to_image["url"]).to match("dk.png") } - end - - context 'for valid jpg file' do - before do - jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') - @link_to_image = upload_image(@project.repository, { 'markdown_img' => jpg }, "http://test.example/") - end - - it { expect(@link_to_image).to have_key("alt") } - it { expect(@link_to_image).to have_key("url") } - it { expect(@link_to_image).to have_value("rails_sample") } - it { expect(@link_to_image["url"]).to match("http://test.example/uploads/#{@project.path_with_namespace}") } - it { expect(@link_to_image["url"]).to match("rails_sample.jpg") } - end - - context 'for txt file' do - before do - txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') - @link_to_image = upload_image(@project.repository, { 'markdown_img' => txt }, "http://test.example/") - end - - it { expect(@link_to_image).to be_nil } - end - end - - def upload_image(repository, params, root_url) - Projects::ImageService.new(repository, params, root_url).execute - end -end -- GitLab From ca504a77fe994da893cd0632de5e0e7ea5b729fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Sat, 14 Feb 2015 16:45:22 +0100 Subject: [PATCH 0928/1609] Fix tests --- spec/services/projects/file_service_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/services/projects/file_service_spec.rb b/spec/services/projects/file_service_spec.rb index 38ab4a467bb..e2d6766735e 100644 --- a/spec/services/projects/file_service_spec.rb +++ b/spec/services/projects/file_service_spec.rb @@ -20,7 +20,7 @@ describe Projects::FileService do it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file).to have_value('banana_sample') } it { expect(@link_to_file['is_image']).to equal(true) } - it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('banana_sample.gif') } end @@ -38,7 +38,7 @@ describe Projects::FileService do it { expect(@link_to_file).to have_value('dk') } it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file['is_image']).to equal(true) } - it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('dk.png') } end @@ -53,7 +53,7 @@ describe Projects::FileService do it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file).to have_value('rails_sample') } it { expect(@link_to_file['is_image']).to equal(true) } - it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('rails_sample.jpg') } end @@ -70,7 +70,7 @@ describe Projects::FileService do it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file).to have_value('doc_sample.txt') } it { expect(@link_to_file['is_image']).to equal(false) } - it { expect(@link_to_file['url']).to match("http://test.example/uploads/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('doc_sample.txt') } end end -- GitLab From 9729cc584f5758395960416f308a9c45f698cdee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Sat, 14 Feb 2015 19:52:45 +0100 Subject: [PATCH 0929/1609] implement Project::UploadsController --- app/controllers/files_controller.rb | 29 ++----------------- .../projects/uploads_controller.rb | 16 ++++++++++ app/uploaders/file_uploader.rb | 4 ++- config/routes.rb | 5 ++-- spec/services/projects/file_service_spec.rb | 8 ++--- 5 files changed, 28 insertions(+), 34 deletions(-) create mode 100644 app/controllers/projects/uploads_controller.rb diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index a86340dd9bb..15523cbc2e7 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -1,5 +1,5 @@ class FilesController < ApplicationController - def download_notes + def download note = Note.find(params[:id]) uploader = note.attachment @@ -14,32 +14,7 @@ class FilesController < ApplicationController not_found! end else - not_found! + redirect_to uploader.url end end - - def download_files - namespace_id = params[:namespace] - project_id = params[:project] - folder_id = params[:folder_id] - filename = params[:filename] - project_with_namespace="#{namespace_id}/#{project_id}" - filename_with_id="#{folder_id}/#{filename}" - - project = Project.find_with_namespace(project_with_namespace) - - uploader = FileUploader.new("#{Rails.root}/uploads","#{project_with_namespace}/#{folder_id}") - uploader.retrieve_from_store!(filename) - - if can?(current_user, :read_project, project) - download(uploader) - else - not_found! - end - end - - def download(uploader) - disposition = uploader.image? ? 'inline' : 'attachment' - send_file uploader.file.path, disposition: disposition - end end diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb new file mode 100644 index 00000000000..1c9fb1c86fb --- /dev/null +++ b/app/controllers/projects/uploads_controller.rb @@ -0,0 +1,16 @@ +class Projects::UploadsController < Projects::ApplicationController + layout 'project' + + before_filter :project + + def show + folder_id = params[:folder_id] + filename = params[:filename] + + uploader = FileUploader.new("#{Rails.root}/uploads","#{@project.path_with_namespace}/#{folder_id}") + uploader.retrieve_from_store!(filename) + + disposition = uploader.image? ? 'inline' : 'attachment' + send_file uploader.file.path, disposition: disposition + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index ac7bd5b27ec..51ae8040e52 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -40,7 +40,9 @@ class FileUploader < CarrierWave::Uploader::Base end def secure_url - Gitlab.config.gitlab.relative_url_root + "/files/#{@path}/#{@filename}" + path_array = @path.split('/') + path = File.join(path_array[0],path_array[1],'uploads',path_array[2]) + Gitlab.config.gitlab.relative_url_root + "/#{path}/#{@filename}" end def image? diff --git a/config/routes.rb b/config/routes.rb index bd659ddeabd..d29ad8db639 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -93,8 +93,7 @@ Gitlab::Application.routes.draw do # # Attachments serving # - get 'files/:type/:id/:filename' => 'files#download_notes', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ } - get 'files/:namespace/:project/:folder_id/:filename' => 'files#download_files', constraints: { namespace: /[^\/]+/, project: /[a-zA-Z.\/0-9_\-]+/, filename: /.+/ } + get 'files/:type/:id/:filename' => 'files#download', constraints: { id: /\d+/, type: /[a-z]+/, filename: /.+/ } # # Admin Area @@ -257,6 +256,8 @@ Gitlab::Application.routes.draw do end end + get '/uploads/:folder_id/:filename' => 'uploads#show', constraints: { filename: /.+/ } + get '/compare/:from...:to' => 'compare#show', :as => 'compare', :constraints => { from: /.+/, to: /.+/ } diff --git a/spec/services/projects/file_service_spec.rb b/spec/services/projects/file_service_spec.rb index e2d6766735e..7bbe5b575c9 100644 --- a/spec/services/projects/file_service_spec.rb +++ b/spec/services/projects/file_service_spec.rb @@ -20,7 +20,7 @@ describe Projects::FileService do it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file).to have_value('banana_sample') } it { expect(@link_to_file['is_image']).to equal(true) } - it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('banana_sample.gif') } end @@ -38,7 +38,7 @@ describe Projects::FileService do it { expect(@link_to_file).to have_value('dk') } it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file['is_image']).to equal(true) } - it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('dk.png') } end @@ -53,7 +53,7 @@ describe Projects::FileService do it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file).to have_value('rails_sample') } it { expect(@link_to_file['is_image']).to equal(true) } - it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('rails_sample.jpg') } end @@ -70,7 +70,7 @@ describe Projects::FileService do it { expect(@link_to_file).to have_key('is_image') } it { expect(@link_to_file).to have_value('doc_sample.txt') } it { expect(@link_to_file['is_image']).to equal(false) } - it { expect(@link_to_file['url']).to match("/files/#{@project.path_with_namespace}") } + it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") } it { expect(@link_to_file['url']).to match('doc_sample.txt') } end end -- GitLab From 192e7306626e073a5e6fb3b41d69f0e3fddb0821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Sun, 15 Feb 2015 18:48:32 +0100 Subject: [PATCH 0930/1609] Fix tests --- spec/controllers/projects_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 039751a41e8..2d52e3fd913 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -30,7 +30,7 @@ describe ProjectsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"rails_sample\"' - expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}" + expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads" expect(response.body).to match '\"is_image\":true' end end @@ -42,7 +42,7 @@ describe ProjectsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"doc_sample.txt\"' - expect(response.body).to match "\"url\":\"http://test.host/uploads/#{project.path_with_namespace}" + expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads" expect(response.body).to match '\"is_image\":false' end end -- GitLab From d2ebdf664b42d4fac6b2e060ef79aa9fe0b0e72d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 16 Feb 2015 19:58:40 +0100 Subject: [PATCH 0931/1609] Refactor. --- .gitignore | 1 + Gemfile | 1 + Gemfile.lock | 6 ++ .../javascripts/dropzone_input.js.coffee | 26 ++++----- app/controllers/files_controller.rb | 17 +++--- .../projects/uploads_controller.rb | 37 ++++++++++--- app/controllers/projects_controller.rb | 17 ------ app/services/projects/file_service.rb | 55 ------------------- app/services/projects/upload_service.rb | 22 ++++++++ app/uploaders/attachment_uploader.rb | 2 +- app/uploaders/file_uploader.rb | 38 ++++--------- app/views/projects/issues/_form.html.haml | 2 +- .../projects/merge_requests/_form.html.haml | 2 +- .../merge_requests/_new_submit.html.haml | 2 +- app/views/projects/milestones/_form.html.haml | 2 +- app/views/projects/notes/_form.html.haml | 2 +- app/views/projects/wikis/_form.html.haml | 2 +- config/routes.rb | 7 ++- db/schema.rb | 1 - .../projects/uploads_controller_spec.rb | 49 +++++++++++++++++ spec/controllers/projects_controller_spec.rb | 45 +-------------- ...service_spec.rb => upload_service_spec.rb} | 20 +++---- 22 files changed, 163 insertions(+), 193 deletions(-) delete mode 100644 app/services/projects/file_service.rb create mode 100644 app/services/projects/upload_service.rb create mode 100644 spec/controllers/projects/uploads_controller_spec.rb rename spec/services/projects/{file_service_spec.rb => upload_service_spec.rb} (80%) diff --git a/.gitignore b/.gitignore index 7a7b5c93936..89fe301ee8f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ nohup.out public/assets/ public/uploads.* public/uploads/ +uploads/ rails_best_practices_output.html tags tmp/ diff --git a/Gemfile b/Gemfile index c3d8299e944..3f0eae8ef42 100644 --- a/Gemfile +++ b/Gemfile @@ -205,6 +205,7 @@ group :development do gem "letter_opener" gem 'quiet_assets', '~> 1.0.1' gem 'rack-mini-profiler', require: false + gem "byebug" # Better errors handler gem 'better_errors' diff --git a/Gemfile.lock b/Gemfile.lock index 3283da40f8d..2f5ebf80b12 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,6 +61,9 @@ GEM sass (~> 3.2) browser (0.7.2) builder (3.2.2) + byebug (3.2.0) + columnize (~> 0.8) + debugger-linecache (~> 1.2) cal-heatmap-rails (0.0.1) capybara (2.2.1) mime-types (>= 1.16) @@ -88,6 +91,7 @@ GEM coffee-script-source (1.6.3) colored (1.2) colorize (0.5.8) + columnize (0.9.0) connection_pool (2.1.0) coveralls (0.7.0) multi_json (~> 1.3) @@ -103,6 +107,7 @@ GEM daemons (1.1.9) database_cleaner (1.3.0) debug_inspector (0.0.2) + debugger-linecache (1.2.0) default_value_for (3.0.0) activerecord (>= 3.2.0, < 5.0) descendants_tracker (0.0.3) @@ -646,6 +651,7 @@ DEPENDENCIES binding_of_caller bootstrap-sass (~> 3.0) browser + byebug cal-heatmap-rails (~> 0.0.1) capybara (~> 2.2.1) carrierwave diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index bed8471b39a..2d9b496b132 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -9,7 +9,7 @@ class @DropzoneInput iconPicture = "" iconSpinner = "" btnAlert = "" - project_file_path_upload = window.project_file_path_upload or null + project_uploads_path = window.project_uploads_path or null form_textarea = $(form).find("textarea.markdown-area") form_textarea.wrap "
    " @@ -72,10 +72,10 @@ class @DropzoneInput form.find(".md-preview-holder").hide() dropzone = form_dropzone.dropzone( - url: project_file_path_upload + url: project_uploads_path dictDefaultMessage: "" clickable: true - paramName: "markdown_file" + paramName: "file" maxFilesize: 10 uploadMultiple: false headers: @@ -131,10 +131,9 @@ class @DropzoneInput child = $(dropzone[0]).children("textarea") - formatLink = (str) -> - text = "[" + str.alt + "](" + str.url + ")" - if str.is_image is true - text = "!" + text + formatLink = (link) -> + text = "[#{link.alt}](#{link.url})" + text = "!#{text}" if link.is_image text handlePaste = (event) -> @@ -179,9 +178,9 @@ class @DropzoneInput uploadFile = (item, filename) -> formData = new FormData() - formData.append "markdown_file", item, filename + formData.append "file", item, filename $.ajax - url: project_file_path_upload + url: project_uploads_path type: "POST" data: formData dataType: "json" @@ -235,8 +234,7 @@ class @DropzoneInput $(@).closest('.gfm-form').find('.div-dropzone').click() return - formatLink: (str) -> - text = "[" + str.alt + "](" + str.url + ")" - if str.is_image is true - text = "!" + text - text + formatLink: (link) -> + text = "[#{link.alt}](#{link.url})" + text = "!#{text}" if link.is_image + text \ No newline at end of file diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index 15523cbc2e7..267239b7b84 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -3,18 +3,21 @@ class FilesController < ApplicationController note = Note.find(params[:id]) uploader = note.attachment - if uploader.file_storage? - if can?(current_user, :read_project, note.project) - # Replace old notes location in /public with the new one in / and send the file + if can?(current_user, :read_project, note.project) + if uploader.file_storage? path = uploader.file.path.gsub("#{Rails.root}/public", Rails.root.to_s) - disposition = uploader.image? ? 'inline' : 'attachment' - send_file path, disposition: disposition + if File.exist?(path) + disposition = uploader.image? ? 'inline' : 'attachment' + send_file path, disposition: disposition + else + not_found! + end else - not_found! + redirect_to uploader.url end else - redirect_to uploader.url + not_found! end end end diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 1c9fb1c86fb..355163ac879 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -3,14 +3,37 @@ class Projects::UploadsController < Projects::ApplicationController before_filter :project + def create + link_to_file = ::Projects::UploadService.new(repository, params[:file]). + execute + + respond_to do |format| + if link_to_file + format.json do + render json: { link: link_to_file } + end + else + format.json do + render json: 'Invalid file.', status: :unprocessable_entity + end + end + end + end + def show - folder_id = params[:folder_id] - filename = params[:filename] - - uploader = FileUploader.new("#{Rails.root}/uploads","#{@project.path_with_namespace}/#{folder_id}") - uploader.retrieve_from_store!(filename) + uploader = FileUploader.new(project, params[:secret]) + + if uploader.file_storage? + uploader.retrieve_from_store!(params[:filename]) - disposition = uploader.image? ? 'inline' : 'attachment' - send_file uploader.file.path, disposition: disposition + if uploader.file.exists? + disposition = uploader.image? ? 'inline' : 'attachment' + send_file uploader.file.path, disposition: disposition + else + not_found! + end + else + redirect_to uploader.url + end end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index b430278903a..9be66b6b9f9 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -134,19 +134,6 @@ class ProjectsController < ApplicationController end end - def upload_file - link_to_file = ::Projects::FileService.new(repository, params, root_url). - execute - - respond_to do |format| - if link_to_file - format.json { render json: { link: link_to_file } } - else - format.json { render json: 'Invalid file.', status: :unprocessable_entity } - end - end - end - def toggle_star current_user.toggle_star(@project) @project.reload @@ -159,10 +146,6 @@ class ProjectsController < ApplicationController private - def invalid_file(error) - render json: { message: error.message }, status: :internal_server_error - end - def set_title @title = 'New Project' end diff --git a/app/services/projects/file_service.rb b/app/services/projects/file_service.rb deleted file mode 100644 index 8c149bf53a1..00000000000 --- a/app/services/projects/file_service.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Projects - class FileService < BaseService - include Rails.application.routes.url_helpers - def initialize(repository, params, root_url) - @repository, @params, @root_url = repository, params.dup, root_url - end - - def execute - uploader = FileUploader.new("#{Rails.root}/uploads", upload_path, accepted_files) - file = @params['markdown_file'] - - if file - alt = file.original_filename - uploader.store!(file) - filename = nil - if image?(file) - filename=File.basename(alt, '.*') - else - filename=File.basename(alt) - end - link = { - 'alt' => filename, - 'url' => uploader.secure_url, - 'is_image' => image?(file) - } - else - link = nil - end - end - - protected - - def accepted_files - # insert accepted mime types here (e.g %w(jpg jpeg gif png)) - nil - end - - def accepted_images - %w(jpg jpeg gif png) - end - - def image?(file) - accepted_images.map { |format| file.content_type.include? format }.any? - end - - def upload_path - base_dir = FileUploader.generate_dir - File.join(@repository.path_with_namespace, base_dir) - end - - def correct_mime_type?(file) - accepted_files.map { |format| image.content_type.include? format }.any? - end - end -end diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb new file mode 100644 index 00000000000..a186c97628f --- /dev/null +++ b/app/services/projects/upload_service.rb @@ -0,0 +1,22 @@ +module Projects + class UploadService < BaseService + def initialize(project, file) + @project, @file = project, file + end + + def execute + return nil unless @file + + uploader = FileUploader.new(@project) + uploader.store!(@file) + + filename = uploader.image? ? uploader.file.basename : uploader.file.filename + + { + 'alt' => filename, + 'url' => uploader.secure_url, + 'is_image' => uploader.image? + } + end + end +end diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index 22742d287a4..58dc6e90c1e 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -21,7 +21,7 @@ class AttachmentUploader < CarrierWave::Uploader::Base end def secure_url - Gitlab.config.gitlab.relative_url_root + "/files/#{model.class.to_s.underscore}/#{model.id}/#{file.filename}" + File.join(Gitlab.config.gitlab.relative_url_root, "files", model.class.to_s.underscore, model.id.to_s, file.filename) end def file_storage? diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 51ae8040e52..c040f6bbe92 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -2,47 +2,33 @@ class FileUploader < CarrierWave::Uploader::Base storage :file - def initialize(base_dir, path = '', allowed_extensions = nil) - @base_dir = base_dir - @path = path - @allowed_extensions = allowed_extensions + def initialize(project, secret = self.class.generate_secret) + @project = project + @secret = secret end def base_dir - @base_dir + "#{Rails.root}/uploads" end def store_dir - File.join(@base_dir, @path) + File.join(base_dir, @project.path_with_namespace, @secret) end def cache_dir - File.join(@base_dir, 'tmp', @path) + File.join(base_dir, 'tmp', @project.path_with_namespace, @secret) end - def extension_white_list - @allowed_extensions || super - end - - def store!(file) - @filename = self.class.generate_filename(file) - super - end - - def self.generate_filename(file) - original_filename = File.basename(file.original_filename, '.*') - extension = File.extname(file.original_filename) - new_filename = Digest::MD5.hexdigest(original_filename) + extension - end - - def self.generate_dir + def self.generate_secret SecureRandom.hex(5) end def secure_url - path_array = @path.split('/') - path = File.join(path_array[0],path_array[1],'uploads',path_array[2]) - Gitlab.config.gitlab.relative_url_root + "/#{path}/#{@filename}" + File.join(Gitlab.config.gitlab.relative_url_root, @project.path_with_namespace, "uploads", @secret, file.filename) + end + + def file_storage? + self.class.storage == CarrierWave::Storage::File end def image? diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 975980bd6bd..afeeed6edf4 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -11,4 +11,4 @@ e.preventDefault(); }); - window.project_file_path_upload = "#{upload_file_project_path @project}"; + window.project_uploads_path = "#{project_uploads_path @project}"; diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 28c4734e14f..c1a05e4586f 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -9,4 +9,4 @@ e.preventDefault(); }); - window.project_file_path_upload = "#{upload_file_project_path @project}"; + window.project_uploads_path = "#{project_uploads_path @project}"; diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 0653b30fcca..4cf2a05b1a3 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -113,7 +113,7 @@ e.preventDefault(); }); - window.project_file_path_upload = "#{upload_file_project_path @project}"; + window.project_uploads_path = "#{project_uploads_path @project}"; :javascript var merge_request diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 5fbb668570c..dbcd23eee05 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -51,4 +51,4 @@ onSelect: function(dateText, inst) { $("#milestone_due_date").val(dateText) } }).datepicker("setDate", $.datepicker.parseDate('yy-mm-dd', $('#milestone_due_date').val())); - window.project_file_path_upload = "#{upload_file_project_path @project}"; + window.project_uploads_path = "#{project_uploads_path @project}"; diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index fe3dab569f4..9f9efc782d5 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -29,4 +29,4 @@ = f.file_field :attachment, class: "js-note-attachment-input hidden" :javascript - window.project_file_path_upload = "#{upload_file_project_path @project}"; + window.project_uploads_path = "#{project_uploads_path @project}"; diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 0afee138c8b..b1579878ed1 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -43,6 +43,6 @@ = link_to "Cancel", project_wiki_path(@project, :home), class: "btn btn-cancel" :javascript - window.project_file_path_upload = "#{upload_file_project_path @project}"; + window.project_uploads_path = "#{project_uploads_path @project}"; diff --git a/config/routes.rb b/config/routes.rb index d29ad8db639..f0a7cf1e8a6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -220,7 +220,6 @@ Gitlab::Application.routes.draw do put :transfer post :archive post :unarchive - post :upload_file post :toggle_star post :markdown_preview get :autocomplete_sources @@ -256,7 +255,11 @@ Gitlab::Application.routes.draw do end end - get '/uploads/:folder_id/:filename' => 'uploads#show', constraints: { filename: /.+/ } + resources :uploads, only: [:create] do + collection do + get ":secret/:filename", action: :show, constraints: { filename: /.+/ } + end + end get '/compare/:from...:to' => 'compare#show', :as => 'compare', :constraints => { from: /.+/, to: /.+/ } diff --git a/db/schema.rb b/db/schema.rb index e11a068c9c5..be3d35a431d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -26,7 +26,6 @@ ActiveRecord::Schema.define(version: 20150213121042) do t.datetime "updated_at" t.string "home_page_url" t.integer "default_branch_protection", default: 2 - t.boolean "twitter_sharing_enabled", default: true end create_table "broadcast_messages", force: true do |t| diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb new file mode 100644 index 00000000000..8c99b5ca528 --- /dev/null +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -0,0 +1,49 @@ +require('spec_helper') + +describe Projects::UploadsController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } + let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + + describe 'POST #create' do + before do + sign_in(user) + project.team << [user, :developer] + end + + context "without params['file']" do + it 'returns an error' do + post :create, project_id: project.to_param, format: :json + expect(response.status).to eq(422) + end + end + + context 'with valid image' do + before do + post :create, + project_id: project.to_param, + file: jpg, + format: :json + end + + it 'returns a content with original filename, new link, and correct type.' do + expect(response.body).to match '\"alt\":\"rails_sample\"' + expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads" + expect(response.body).to match '\"is_image\":true' + end + end + + context 'with valid non-image file' do + before do + post :create, project_id: project.to_param, file: txt, format: :json + end + + it 'returns a content with original filename, new link, and correct type.' do + expect(response.body).to match '\"alt\":\"doc_sample.txt\"' + expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads" + expect(response.body).to match '\"is_image\":false' + end + end + end +end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 2d52e3fd913..9be4c2e505c 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -4,50 +4,7 @@ describe ProjectsController do let(:project) { create(:project) } let(:public_project) { create(:project, :public) } let(:user) { create(:user) } - let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } - let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } - - describe 'POST #upload_file' do - before do - sign_in(user) - project.team << [user, :developer] - end - - context "without params['markdown_file']" do - it 'returns an error' do - post :upload_file, id: project.to_param, format: :json - expect(response.status).to eq(422) - end - end - - context 'with valid image' do - before do - post :upload_file, - id: project.to_param, - markdown_file: jpg, - format: :json - end - - it 'returns a content with original filename, new link, and correct type.' do - expect(response.body).to match '\"alt\":\"rails_sample\"' - expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads" - expect(response.body).to match '\"is_image\":true' - end - end - - context 'with valid non-image file' do - before do - post :upload_file, id: project.to_param, markdown_file: txt, format: :json - end - - it 'returns a content with original filename, new link, and correct type.' do - expect(response.body).to match '\"alt\":\"doc_sample.txt\"' - expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads" - expect(response.body).to match '\"is_image\":false' - end - end - end - + describe 'POST #toggle_star' do it 'toggles star if user is signed in' do sign_in(user) diff --git a/spec/services/projects/file_service_spec.rb b/spec/services/projects/upload_service_spec.rb similarity index 80% rename from spec/services/projects/file_service_spec.rb rename to spec/services/projects/upload_service_spec.rb index 7bbe5b575c9..fc34b456482 100644 --- a/spec/services/projects/file_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::FileService do +describe Projects::UploadService do describe 'File service' do before do @user = create :user @@ -10,9 +10,7 @@ describe Projects::FileService do context 'for valid gif file' do before do gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') - @link_to_file = upload_file(@project.repository, - { 'markdown_file' => gif }, - 'http://test.example/') + @link_to_file = upload_file(@project.repository, gif) end it { expect(@link_to_file).to have_key('alt') } @@ -28,9 +26,7 @@ describe Projects::FileService do before do png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') - @link_to_file = upload_file(@project.repository, - { 'markdown_file' => png }, - 'http://test.example/') + @link_to_file = upload_file(@project.repository, png) end it { expect(@link_to_file).to have_key('alt') } @@ -45,7 +41,7 @@ describe Projects::FileService do context 'for valid jpg file' do before do jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') - @link_to_file = upload_file(@project.repository, { 'markdown_file' => jpg }, 'http://test.example/') + @link_to_file = upload_file(@project.repository, jpg) end it { expect(@link_to_file).to have_key('alt') } @@ -60,9 +56,7 @@ describe Projects::FileService do context 'for txt file' do before do txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') - @link_to_file = upload_file(@project.repository, - { 'markdown_file' => txt }, - 'http://test.example/') + @link_to_file = upload_file(@project.repository, txt) end it { expect(@link_to_file).to have_key('alt') } @@ -75,7 +69,7 @@ describe Projects::FileService do end end - def upload_file(repository, params, root_url) - Projects::FileService.new(repository, params, root_url).execute + def upload_file(repository, file) + Projects::UploadService.new(repository, file).execute end end -- GitLab From ab401a6132411294cee03cf4e0902ec75c2c42dc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 16 Feb 2015 22:08:44 +0100 Subject: [PATCH 0932/1609] Remove note attachment file selector. --- app/assets/javascripts/notes.js.coffee | 13 ------------- app/views/projects/notes/_edit_form.html.haml | 10 +--------- app/views/projects/notes/_form.html.haml | 8 -------- 3 files changed, 1 insertion(+), 30 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 1c090bd06dc..90e6fd6d154 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -39,9 +39,6 @@ class @Notes # reset main target form after submit $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm - # attachment button - $(document).on "click", ".js-choose-note-attachment-button", @chooseNoteAttachment - # update the file name when an attachment is selected $(document).on "change", ".js-note-attachment-input", @updateFormAttachment @@ -73,7 +70,6 @@ class @Notes $(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-attachment-delete" $(document).off "ajax:complete", ".js-main-target-form" - $(document).off "click", ".js-choose-note-attachment-button" $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" @@ -173,15 +169,6 @@ class @Notes form.find(".js-note-text").data("autosave").reset() - ### - Called when clicking the "Choose File" button. - - Opens the file selection dialog. - ### - chooseNoteAttachment: -> - form = $(this).closest("form") - form.find(".js-note-attachment-input").click() - ### Shows the main form and does some setup on it. diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 4ba59078318..ca097f3d55a 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -11,12 +11,4 @@ .note-form-actions .buttons = f.submit 'Save Comment', class: "btn btn-primary btn-save btn-grouped js-comment-button" - = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel" - - .note-form-option.hidden-xs - %a.choose-btn.btn.js-choose-note-attachment-button - %i.fa.fa-paperclip - %span Choose File ... -   - %span.file_name.js-attachment-filename - = f.file_field :attachment, class: "js-note-attachment-input hidden" + = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel" \ No newline at end of file diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 9f9efc782d5..8b331ef819f 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -20,13 +20,5 @@ = yield(:note_actions) %a.btn.grouped.js-close-discussion-note-form Cancel - .note-form-option.hidden-xs - %a.choose-btn.btn.js-choose-note-attachment-button - %i.fa.fa-paperclip - %span Choose File ... -   - %span.file_name.js-attachment-filename - = f.file_field :attachment, class: "js-note-attachment-input hidden" - :javascript window.project_uploads_path = "#{project_uploads_path @project}"; -- GitLab From ab65be7a2f5d46a681d882f4f90d1f0438c64b02 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 16 Feb 2015 22:08:55 +0100 Subject: [PATCH 0933/1609] Use longer upload secret. --- app/uploaders/file_uploader.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index c040f6bbe92..bdfbfc668bd 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -20,7 +20,7 @@ class FileUploader < CarrierWave::Uploader::Base end def self.generate_secret - SecureRandom.hex(5) + SecureRandom.hex end def secure_url -- GitLab From 99fb4d387fbcddc56292b788a84b1e62d27765dd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 16 Feb 2015 22:09:17 +0100 Subject: [PATCH 0934/1609] Add paperclip icon to links to uploads in notes. --- app/assets/stylesheets/sections/notes.scss | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index 5494845eb8c..40adc8b3ba7 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -66,6 +66,22 @@ ul.notes { overflow: auto; word-wrap: break-word; @include md-typography; + + a[href*="/uploads/"] { + &:before { + margin-right: 4px; + + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + content: "\f0c6"; + } + + &:hover:before { + text-decoration: none; + } + } } } .note-header { -- GitLab From 4036fb167ddce47db8ad4d885000b2c9db96595d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 16 Feb 2015 22:09:52 +0100 Subject: [PATCH 0935/1609] Change textarea upload hover icon from picture to paperclip. --- app/assets/javascripts/dropzone_input.js.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee index 2d9b496b132..06e9f0001ae 100644 --- a/app/assets/javascripts/dropzone_input.js.coffee +++ b/app/assets/javascripts/dropzone_input.js.coffee @@ -6,7 +6,7 @@ class @DropzoneInput divHover = "
    " divSpinner = "
    " divAlert = "
    " - iconPicture = "" + iconPaperclip = "" iconSpinner = "" btnAlert = "" project_uploads_path = window.project_uploads_path or null @@ -19,7 +19,7 @@ class @DropzoneInput form_dropzone = $(form).find('.div-dropzone') form_dropzone.parent().addClass "div-dropzone-wrapper" form_dropzone.append divHover - $(".div-dropzone-hover").append iconPicture + $(".div-dropzone-hover").append iconPaperclip form_dropzone.append divSpinner $(".div-dropzone-spinner").append iconSpinner $(".div-dropzone-spinner").css -- GitLab From 896c046217ab44e7e685f0c2ca2d4f3835d63d44 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Feb 2015 16:14:49 -0800 Subject: [PATCH 0936/1609] Affix assignee/milestone block --- app/assets/javascripts/issue.js.coffee | 6 ++ .../javascripts/merge_request.js.coffee | 6 ++ app/assets/stylesheets/sections/issuable.scss | 25 +++++++ app/assets/stylesheets/sections/issues.scss | 11 ++- .../stylesheets/sections/merge_requests.scss | 9 ++- .../projects/issues/_discussion.html.haml | 40 +++++------ app/views/projects/issues/show.html.haml | 69 ++++++++++--------- .../merge_requests/_discussion.html.haml | 37 +++++----- .../projects/merge_requests/_show.html.haml | 65 ++++++++--------- 9 files changed, 162 insertions(+), 106 deletions(-) create mode 100644 app/assets/stylesheets/sections/issuable.scss diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 45c248e6fb6..9b7c1be8355 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -15,3 +15,9 @@ class @Issue "issue" updateTaskState ) + + $('.issuable-affix').affix offset: + top: -> + @top = $('.issue-details').outerHeight(true) + 25 + bottom: -> + @bottom = $('.footer').outerHeight(true) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 5bcbd56852d..757592842eb 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -20,6 +20,12 @@ class @MergeRequest if $("a.btn-close").length $("li.task-list-item input:checkbox").prop("disabled", false) + $('.issuable-affix').affix offset: + top: -> + @top = $('.merge-request-details').outerHeight(true) + 70 + bottom: -> + @bottom = $('.footer').outerHeight(true) + # Local jQuery finder $: (selector) -> this.$el.find(selector) diff --git a/app/assets/stylesheets/sections/issuable.scss b/app/assets/stylesheets/sections/issuable.scss new file mode 100644 index 00000000000..75bd39853bd --- /dev/null +++ b/app/assets/stylesheets/sections/issuable.scss @@ -0,0 +1,25 @@ +@media (max-width: $screen-sm-max) { + .issuable-affix { + margin-top: 20px; + } +} + +@media (max-width: $screen-md-max) { + .issuable-affix { + position: static; + } +} + +@media (min-width: $screen-md-max) { + .issuable-affix { + &.affix-top { + position: static; + } + + &.affix { + position: fixed; + top: 70px; + width: 220px; + } + } +} diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index 7a9d3334d96..ccfc9b704a6 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -94,8 +94,15 @@ } } -.issue-show-labels .color-label { - padding: 6px 10px; +.issue-show-labels { + a { + margin-right: 5px; + margin-bottom: 5px; + display: inline-block; + .color-label { + padding: 6px 10px; + } + } } form.edit-issue { diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 0e27c389387..6662a38344a 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -95,7 +95,14 @@ color: #999; .merge-request-labels { - display: inline-block; + a { + margin-right: 5px; + margin-bottom: 5px; + display: inline-block; + .color-label { + padding: 6px 10px; + } + } } } } diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index e04e1985f1f..3a278058944 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -14,24 +14,24 @@ .voting_notes#notes= render "projects/notes/notes_with_form" .col-md-3 - %div - .clearfix - %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} - = cross_project_reference(@project, @issue) - %hr - .context - %cite.cgray - = render partial: 'issue_context', locals: { issue: @issue } - %hr - .clearfix - .votes-holder - %h6 Votes - #votes= render 'votes/votes_block', votable: @issue - - - if @issue.labels.any? + .issuable-affix + .clearfix + %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} + = cross_project_reference(@project, @issue) %hr - %h6 Labels - .issue-show-labels - - @issue.labels.each do |label| - = link_to project_issues_path(@project, label_name: label.name) do - %p= render_colored_label(label) + .context + %cite.cgray + = render partial: 'issue_context', locals: { issue: @issue } + %hr + .clearfix + .votes-holder + %h6 Votes + #votes= render 'votes/votes_block', votable: @issue + + - if @issue.labels.any? + %hr + %h6 Labels + .issue-show-labels + - @issue.labels.each do |label| + = link_to project_issues_path(@project, label_name: label.name) do + = render_colored_label(label) diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 75411c6d86f..bf343cbb7af 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,37 +1,40 @@ -%h4.page-title - .issue-box{ class: issue_box_class(@issue) } - - if @issue.closed? - Closed - - else - Open - Issue ##{@issue.iid} - %small.creator - · created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)} +.issue + .issue-details + %h4.page-title + .issue-box{ class: issue_box_class(@issue) } + - if @issue.closed? + Closed + - else + Open + Issue ##{@issue.iid} + %small.creator + · created by #{link_to_member(@project, @issue.author)} #{issue_timestamp(@issue)} - .pull-right - - if can?(current_user, :write_issue, @project) - = link_to new_project_issue_path(@project), class: "btn btn-grouped new-issue-link", title: "New Issue", id: "new_issue_link" do - %i.fa.fa-plus - New Issue - - if can?(current_user, :modify_issue, @issue) - - if @issue.closed? - = link_to 'Reopen', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen" - - else - = link_to 'Close', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue" + .pull-right + - if can?(current_user, :write_issue, @project) + = link_to new_project_issue_path(@project), class: "btn btn-grouped new-issue-link", title: "New Issue", id: "new_issue_link" do + %i.fa.fa-plus + New Issue + - if can?(current_user, :modify_issue, @issue) + - if @issue.closed? + = link_to 'Reopen', project_issue_path(@project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen" + - else + = link_to 'Close', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue" - = link_to edit_project_issue_path(@project, @issue), class: "btn btn-grouped issuable-edit" do - %i.fa.fa-pencil-square-o - Edit + = link_to edit_project_issue_path(@project, @issue), class: "btn btn-grouped issuable-edit" do + %i.fa.fa-pencil-square-o + Edit -%hr -%h3.issue-title - = gfm escape_once(@issue.title) -%div - - if @issue.description.present? - .description - .wiki - = preserve do - = markdown(@issue.description, parse_tasks: true) + %hr + %h3.issue-title + = gfm escape_once(@issue.title) + %div + - if @issue.description.present? + .description + .wiki + = preserve do + = markdown(@issue.description, parse_tasks: true) -%hr -= render "projects/issues/discussion" + %hr + .issue-discussion + = render "projects/issues/discussion" diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index f1f66569a9f..51e65f874c2 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -10,22 +10,23 @@ = render "projects/merge_requests/show/participants" = render "projects/notes/notes_with_form" .col-md-3 - .clearfix - %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} - = cross_project_reference(@project, @merge_request) - %hr - .context - %cite.cgray - = render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request } - %hr - .votes-holder - %h6 Votes - #votes= render 'votes/votes_block', votable: @merge_request - - - if @merge_request.labels.any? + .issuable-affix + .clearfix + %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} + = cross_project_reference(@project, @merge_request) + %hr + .context + %cite.cgray + = render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request } %hr - %h6 Labels - .merge-request-show-labels - - @merge_request.labels.each do |label| - = link_to project_merge_requests_path(@project, label_name: label.name) do - %p= render_colored_label(label) + .votes-holder + %h6 Votes + #votes= render 'votes/votes_block', votable: @merge_request + + - if @merge_request.labels.any? + %hr + %h6 Labels + .merge-request-show-labels + - @merge_request.labels.each do |label| + = link_to project_merge_requests_path(@project, label_name: label.name) do + = render_colored_label(label) diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 8e31a7e3fe4..af7044160c1 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,37 +1,38 @@ .merge-request{'data-url' => project_merge_request_path(@project, @merge_request)} - = render "projects/merge_requests/show/mr_title" - %hr - = render "projects/merge_requests/show/mr_box" - %hr - .append-bottom-20 - .slead - %span From - - if @merge_request.for_fork? - %strong.label-branch< - - if @merge_request.source_project - = link_to @merge_request.source_project_namespace, project_path(@merge_request.source_project) - - else - \ #{@merge_request.source_project_namespace} - \:#{@merge_request.source_branch} - %span into - %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} - - else - %strong.label-branch #{@merge_request.source_branch} - %span into - %strong.label-branch #{@merge_request.target_branch} - - if @merge_request.open? - %span.pull-right - .btn-group - %a.btn.dropdown-toggle{ data: {toggle: :dropdown} } - %i.fa.fa-download - Download as - %span.caret - %ul.dropdown-menu - %li= link_to "Email Patches", project_merge_request_path(@project, @merge_request, format: :patch) - %li= link_to "Plain Diff", project_merge_request_path(@project, @merge_request, format: :diff) + .merge-request-details + = render "projects/merge_requests/show/mr_title" + %hr + = render "projects/merge_requests/show/mr_box" + %hr + .append-bottom-20 + .slead + %span From + - if @merge_request.for_fork? + %strong.label-branch< + - if @merge_request.source_project + = link_to @merge_request.source_project_namespace, project_path(@merge_request.source_project) + - else + \ #{@merge_request.source_project_namespace} + \:#{@merge_request.source_branch} + %span into + %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch} + - else + %strong.label-branch #{@merge_request.source_branch} + %span into + %strong.label-branch #{@merge_request.target_branch} + - if @merge_request.open? + %span.pull-right + .btn-group + %a.btn.dropdown-toggle{ data: {toggle: :dropdown} } + %i.fa.fa-download + Download as + %span.caret + %ul.dropdown-menu + %li= link_to "Email Patches", project_merge_request_path(@project, @merge_request, format: :patch) + %li= link_to "Plain Diff", project_merge_request_path(@project, @merge_request, format: :diff) - = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/show/state_widget" + = render "projects/merge_requests/show/how_to_merge" + = render "projects/merge_requests/show/state_widget" - if @commits.present? %ul.nav.nav-tabs.merge-request-tabs -- GitLab From 24d939afb9816f3de2ca247de82f96ca32de3612 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Feb 2015 16:23:44 -0800 Subject: [PATCH 0937/1609] Remove Group#owner_id from API since it is not used any more --- CHANGELOG | 1 + doc/api/groups.md | 2 -- lib/api/entities.rb | 2 +- lib/api/groups.rb | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 05d1e7bdb40..3ddb7876b72 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -58,6 +58,7 @@ v 7.8.0 (unreleased) - Add quick help links to the GitLab pricing and feature comparison pages. - Fix duplicate authorized applications in user profile and incorrect application client count in admin area. - Make sure Markdown previews always use the same styling as the eventual destination. + - Remove deprecated Group#owner_id from API v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/doc/api/groups.md b/doc/api/groups.md index 3c1858e697d..b5a4b05ccaf 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -14,7 +14,6 @@ GET /groups "id": 1, "name": "Foobar Group", "path": "foo-bar", - "owner_id": 18, "description": "An interesting group" } ] @@ -87,7 +86,6 @@ GET /groups?search=foobar "id": 1, "name": "Foobar Group", "path": "foo-bar", - "owner_id": 18, "description": "An interesting group" } ] diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 8d0664386b4..7572104fc16 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -65,7 +65,7 @@ module API end class Group < Grape::Entity - expose :id, :name, :path, :owner_id, :description + expose :id, :name, :path, :description end class GroupDetail < Group diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 384a28e41f5..a92abd4b690 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -33,9 +33,9 @@ module API attrs = attributes_for_keys [:name, :path, :description] @group = Group.new(attrs) - @group.owner = current_user if @group.save + @group.add_owner(current_user) present @group, with: Entities::Group else render_api_error!("Failed to save group #{@group.errors.messages}", 400) -- GitLab From ff492307b9fb0cd0f016a358cbf1228392f764d1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Feb 2015 16:27:40 -0800 Subject: [PATCH 0938/1609] Dont show gitlab.com import for gitlab.com :) --- app/views/projects/new.html.haml | 6 +++--- safe/public.pem | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 safe/public.pem diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 61f6a66c386..6f5851d61a1 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -52,7 +52,7 @@ %i.fa.fa-github Import projects from GitHub = render 'github_import_modal' - + .project-import.form-group .col-sm-2 .col-sm-10 @@ -60,7 +60,7 @@ = link_to status_import_gitlab_path do %i.fa.fa-heart Import projects from GitLab.com - - else + - elsif request.host != 'gitlab.com' = link_to '#', class: 'how_to_import_link light' do %i.fa.fa-heart Import projects from GitLab.com @@ -99,4 +99,4 @@ e.preventDefault() import_modal = $(this).parent().find(".modal").show() $('.modal-header .close').bind 'click', -> - $(".modal").hide() \ No newline at end of file + $(".modal").hide() diff --git a/safe/public.pem b/safe/public.pem new file mode 100644 index 00000000000..c5ffe20a5c7 --- /dev/null +++ b/safe/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnp2mUaLBoHFX127ysonX +OihiGpI4098eFfH1iAxpKHIof0vs0jFF05IUScNXJZ1U3w8G1U/unY/wGGa3NzAb +ZfDd22eOF6X2Gfiey6U4w9dFf0/UT5x1bphlpX357yh4O9oWWuNaWD062DTbOOsJ +U6UW2U/sZAu/QScys0Nw+gJ58t93hb4jFq+nO5IAQc6g4S8ek5YvIXOshFEpF2in +ZLbSYowx92+9GzfjvdQ7fk0Q2ssg0zfScVa6FY8n019osz0SC3wcSd/qicdfecpu +7oycpd9YDqk4lufE1qVMOsgE8OO4KXMrByz2f+T0p/bH9zdBa5HYylf1T7i60hIL +kQIDAQAB +-----END PUBLIC KEY----- -- GitLab From 70edf950fe6baf90bb98c904d9132924e55e50d6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Feb 2015 22:40:00 -0800 Subject: [PATCH 0939/1609] Show contributed projects on user page and stars for it --- app/controllers/users_controller.rb | 3 +++ app/views/explore/projects/_project.html.haml | 8 +++--- app/views/users/_projects.html.haml | 27 ++++++++++++++----- app/views/users/show.html.haml | 4 +-- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 84a04c5ebe6..e4f588c6a60 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -8,6 +8,9 @@ class UsersController < ApplicationController visible_projects = ProjectsFinder.new.execute(current_user) authorized_projects_ids = visible_projects.pluck(:id) + @contributed_projects = Project.where(id: authorized_projects_ids). + in_group_namespace + @projects = @user.personal_projects. where(id: authorized_projects_ids) diff --git a/app/views/explore/projects/_project.html.haml b/app/views/explore/projects/_project.html.haml index ffbddbae4d6..b093ec00c57 100644 --- a/app/views/explore/projects/_project.html.haml +++ b/app/views/explore/projects/_project.html.haml @@ -3,11 +3,9 @@ .project-access-icon = visibility_level_icon(project.visibility_level) = link_to project.name_with_namespace, project - - - if current_page?(starred_explore_projects_path) - %strong.pull-right - %i.fa.fa-star - = pluralize project.star_count, 'star' + %span.pull-right + %i.fa.fa-star + = project.star_count .project-info - if project.description.present? diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml index 1d38f8e8ab8..c925a48f550 100644 --- a/app/views/users/_projects.html.haml +++ b/app/views/users/_projects.html.haml @@ -1,6 +1,21 @@ -.panel.panel-default - .panel-heading Personal projects - %ul.well-list - - projects.each do |project| - %li - = link_to_project project +- if @contributed_projects.present? + .panel.panel-default + .panel-heading Projects contributed to + %ul.well-list + - @contributed_projects.sort_by(&:star_count).reverse.each do |project| + %li + = link_to_project project + %span.pull-right.light + %i.fa.fa-star + = project.star_count + +- if @projects.present? + .panel.panel-default + .panel-heading Personal projects + %ul.well-list + - @projects.sort_by(&:star_count).reverse.each do |project| + %li + = link_to_project project + %span.pull-right.light + %i.fa.fa-star + = project.star_count diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index b05918b019e..5e82d5780cf 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -35,9 +35,7 @@ = render @events .col-md-4 = render 'profile', user: @user - - if @projects.present? - = render 'projects', projects: @projects - + = render 'projects' :coffeescript $ -> -- GitLab From 367d9a2dc683ef549905f35186412b5248376028 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Feb 2015 22:42:04 -0800 Subject: [PATCH 0940/1609] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 3ddb7876b72..107bda8ebcb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,6 +59,7 @@ v 7.8.0 (unreleased) - Fix duplicate authorized applications in user profile and incorrect application client count in admin area. - Make sure Markdown previews always use the same styling as the eventual destination. - Remove deprecated Group#owner_id from API + - Show projects user contributed to on user page. Show stars near project on user page. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch -- GitLab From 65b125a5035cb021aeb81e168fd4ae1ad6c74c11 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 18 Feb 2015 08:16:42 +0100 Subject: [PATCH 0941/1609] Update schema. --- db/schema.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/schema.rb b/db/schema.rb index be3d35a431d..e11a068c9c5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -26,6 +26,7 @@ ActiveRecord::Schema.define(version: 20150213121042) do t.datetime "updated_at" t.string "home_page_url" t.integer "default_branch_protection", default: 2 + t.boolean "twitter_sharing_enabled", default: true end create_table "broadcast_messages", force: true do |t| -- GitLab From a8a328b1513c0aa442faaf8e8dd6f06f86ac3211 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 00:16:42 -0800 Subject: [PATCH 0942/1609] DB performance improvements to GitLab --- app/controllers/dashboard_controller.rb | 15 +++++++++------ app/controllers/groups_controller.rb | 17 +++++++++++------ app/controllers/projects_controller.rb | 13 ++++++++----- app/controllers/users_controller.rb | 7 ++++--- app/helpers/application_helper.rb | 8 +++++++- app/views/dashboard/_activities.html.haml | 7 +------ app/views/dashboard/_project.html.haml | 2 +- app/views/groups/_projects.html.haml | 2 +- app/views/groups/show.html.haml | 5 +---- app/views/projects/_home_panel.html.haml | 2 +- lib/gitlab/current_settings.rb | 14 +++++++++----- 11 files changed, 53 insertions(+), 39 deletions(-) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 9e59264e418..ee9dc343337 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -12,11 +12,7 @@ class DashboardController < ApplicationController @groups = current_user.authorized_groups.order_name_asc @has_authorized_projects = @projects.count > 0 @projects_count = @projects.count - @projects = @projects.limit(@projects_limit) - - @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) - @events = @event_filter.apply_filter(@events) - @events = @events.limit(20).offset(params[:offset] || 0) + @projects = @projects.includes(:namespace).limit(@projects_limit) @last_push = current_user.recent_push @@ -24,7 +20,14 @@ class DashboardController < ApplicationController respond_to do |format| format.html - format.json { pager_json("events/_events", @events.count) } + + format.json do + @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) + @events = @event_filter.apply_filter(@events).includes(:target, project: :namespace) + @events = @events.limit(20).offset(params[:offset] || 0) + pager_json("events/_events", @events.count) + end + format.atom { render layout: false } end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index aad3709090e..7b7531f1426 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -10,11 +10,11 @@ class GroupsController < ApplicationController # Load group projects before_filter :load_projects, except: [:new, :create, :projects, :edit, :update] + before_filter :event_filter, only: :show + before_filter :set_title, only: [:new, :create] layout :determine_layout - before_filter :set_title, only: [:new, :create] - def new @group = Group.new end @@ -32,14 +32,19 @@ class GroupsController < ApplicationController end def show - @events = Event.in_projects(project_ids) - @events = event_filter.apply_filter(@events) - @events = @events.limit(20).offset(params[:offset] || 0) @last_push = current_user.recent_push if current_user + @projects = @projects.includes(:namespace) respond_to do |format| format.html - format.json { pager_json("events/_events", @events.count) } + + format.json do + @events = Event.in_projects(project_ids) + @events = event_filter.apply_filter(@events).includes(:target, project: :namespace) + @events = @events.limit(20).offset(params[:offset] || 0) + pager_json("events/_events", @events.count) + end + format.atom { render layout: false } end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 462ab3d4749..fb58ddd06e7 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -5,9 +5,10 @@ class ProjectsController < ApplicationController # Authorize before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive] + before_filter :set_title, only: [:new, :create] + before_filter :event_filter, only: :show layout 'navless', only: [:new, :create, :fork] - before_filter :set_title, only: [:new, :create] def new @project = Project.new @@ -56,9 +57,6 @@ class ProjectsController < ApplicationController end limit = (params[:limit] || 20).to_i - @events = @project.events.recent - @events = event_filter.apply_filter(@events) - @events = @events.limit(limit).offset(params[:offset] || 0) @show_star = !(current_user && current_user.starred?(@project)) @@ -76,7 +74,12 @@ class ProjectsController < ApplicationController end end - format.json { pager_json('events/_events', @events.count) } + format.json do + @events = @project.events.recent + @events = event_filter.apply_filter(@events).includes(:target, project: :namespace) + @events = @events.limit(limit).offset(params[:offset] || 0) + pager_json('events/_events', @events.count) + end end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index e4f588c6a60..b4de500fcf1 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -9,17 +9,18 @@ class UsersController < ApplicationController authorized_projects_ids = visible_projects.pluck(:id) @contributed_projects = Project.where(id: authorized_projects_ids). - in_group_namespace + in_group_namespace.includes(:namespace) @projects = @user.personal_projects. - where(id: authorized_projects_ids) + where(id: authorized_projects_ids).includes(:namespace) # Collect only groups common for both users @groups = @user.groups & GroupsFinder.new.execute(current_user) # Get user activity feed for projects common for both users @events = @user.recent_events. - where(project_id: authorized_projects_ids).limit(30) + where(project_id: authorized_projects_ids). + includes(:target, project: :namespace).limit(30) @title = @user.name @title_url = user_path(@user) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e45f4650309..f65c5335a62 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -51,7 +51,13 @@ module ApplicationHelper end def project_icon(project_id, options = {}) - project = Project.find_with_namespace(project_id) + project = + if project_id.is_a?(Project) + project = project_id + else + Project.find_with_namespace(project_id) + end + if project.avatar.present? image_tag project.avatar.url, options elsif project.avatar_in_git diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index fdf96dd6f56..c1fc1602d0a 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -1,9 +1,4 @@ = render "events/event_last_push", event: @last_push = render 'shared/event_filter' - -- if @events.any? - .content_list -- else - .nothing-here-block Projects activity will be displayed here - +.content_list = spinner diff --git a/app/views/dashboard/_project.html.haml b/app/views/dashboard/_project.html.haml index f0fb2c1881b..fa9179cb249 100644 --- a/app/views/dashboard/_project.html.haml +++ b/app/views/dashboard/_project.html.haml @@ -1,6 +1,6 @@ = link_to project_path(project), class: dom_class(project) do .dash-project-avatar - = project_icon(project.to_param, alt: '', class: 'avatar project-avatar s40') + = project_icon(project, alt: '', class: 'avatar project-avatar s40') .dash-project-access-icon = visibility_level_icon(project.visibility_level) %span.str-truncated diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index a2f1d28a275..b505760fa8f 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -13,7 +13,7 @@ %li.project-row = link_to project_path(project), class: dom_class(project) do .dash-project-avatar - = project_icon(project.to_param, alt: '', class: 'avatar s40') + = project_icon(project, alt: '', class: 'avatar s40') .dash-project-access-icon = visibility_level_icon(project.visibility_level) %span.str-truncated diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index f2e591c1939..d5af859ee62 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -13,10 +13,7 @@ - if current_user = render "events/event_last_push", event: @last_push = render 'shared/event_filter' - - if @events.any? - .content_list - - else - .nothing-here-block Project activity will be displayed here + .content_list = spinner %aside.side.col-md-4 = render "projects", projects: @projects diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 5697f9ea1af..d8545dd2c85 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,7 +1,7 @@ - empty_repo = @project.empty_repo? .project-home-panel{:class => ("empty-project" if empty_repo)} .project-identicon-holder - = project_icon(@project.to_param, alt: '', class: 'avatar project-avatar') + = project_icon(@project, alt: '', class: 'avatar project-avatar') .project-home-row .project-home-desc - if @project.description.present? diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 93e7edf508c..1a25eebe7d1 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -1,11 +1,15 @@ module Gitlab module CurrentSettings def current_application_settings - if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('application_settings') - ApplicationSetting.current || - ApplicationSetting.create_from_defaults - else - fake_application_settings + key = :current_application_settings + + RequestStore.store[key] ||= begin + if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('application_settings') + RequestStore.store[:current_application_settings] = + (ApplicationSetting.current || ApplicationSetting.create_from_defaults) + else + fake_application_settings + end end end -- GitLab From a6070074bc30cf2e6c9eb9b053fc79bdd35d9d6b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 00:17:23 -0800 Subject: [PATCH 0943/1609] Update CHANGELOG with performance improvements --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 107bda8ebcb..98592af2190 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -60,6 +60,7 @@ v 7.8.0 (unreleased) - Make sure Markdown previews always use the same styling as the eventual destination. - Remove deprecated Group#owner_id from API - Show projects user contributed to on user page. Show stars near project on user page. + - Improve database performance for GitLab v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch -- GitLab From a04ac76117aec7262d223f3dd61a97c7ff88f3a7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 00:23:01 -0800 Subject: [PATCH 0944/1609] Fix MR labels css --- .../stylesheets/sections/merge_requests.scss | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 6662a38344a..81cd6d745b6 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -95,14 +95,7 @@ color: #999; .merge-request-labels { - a { - margin-right: 5px; - margin-bottom: 5px; - display: inline-block; - .color-label { - padding: 6px 10px; - } - } + display: inline-block; } } } @@ -192,6 +185,13 @@ } } -.merge-request-show-labels .label { - padding: 6px 10px; +.merge-request-show-labels { + a { + margin-right: 5px; + margin-bottom: 5px; + display: inline-block; + .color-label { + padding: 6px 10px; + } + } } -- GitLab From 15bee7e0ffa2f7eccd700da0238ad7a7e66ddbb0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 18 Feb 2015 09:40:42 +0100 Subject: [PATCH 0945/1609] Fix Markdown relative links to files with anchors. --- app/helpers/gitlab_markdown_helper.rb | 9 +++++---- spec/helpers/gitlab_markdown_helper_spec.rb | 14 +++++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 800cacdc2c2..ab30f498c01 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -110,7 +110,7 @@ module GitlabMarkdownHelper end def link_to_ignore?(link) - if link =~ /\#\w+/ + if link =~ /\A\#\w+/ # ignore anchors like true else @@ -122,10 +122,11 @@ module GitlabMarkdownHelper ["http://","https://", "ftp://", "mailto:"] end - def rebuild_path(path) - path.gsub!(/(#.*)/, "") + def rebuild_path(file_path) + file_path = file_path.dup + file_path.gsub!(/(#.*)/, "") id = $1 || "" - file_path = relative_file_path(path) + file_path = relative_file_path(file_path) file_path = sanitize_slashes(file_path) [ diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 317a559f83c..ab908a3d61e 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -584,7 +584,7 @@ describe GitlabMarkdownHelper do it "should leave code blocks untouched" do allow(helper).to receive(:user_color_scheme_class).and_return(:white) - target_html = "
    some code from $40\nhere too\n
    \n" + target_html = "
    some code from $#{snippet.id}\nhere too\n
    \n" expect(helper.markdown("\n some code from $#{snippet.id}\n here too\n")). to eq(target_html) @@ -638,6 +638,18 @@ describe GitlabMarkdownHelper do expect(markdown(actual)).to match(expected) end + it "should handle relative urls for a file in master with an anchor" do + actual = "[GitLab API doc](doc/api/README.md#section)\n" + expected = "

    GitLab API doc

    \n" + expect(markdown(actual)).to match(expected) + end + + it "should not handle relative urls for the current file with an anchor" do + actual = "[GitLab API doc](#section)\n" + expected = "

    GitLab API doc

    \n" + expect(markdown(actual)).to match(expected) + end + it "should handle relative urls for a directory in master" do actual = "[GitLab API doc](doc/api)\n" expected = "

    GitLab API doc

    \n" -- GitLab From 823f8c1a1c6b993d7fa2e4885c4f53897a9e8eb7 Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Wed, 18 Feb 2015 18:13:10 +0100 Subject: [PATCH 0946/1609] Escape process text The Feature request copy paste text was not properly escaped. --- PROCESS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PROCESS.md b/PROCESS.md index 5cc25de05a4..1b6b3e7d32d 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -71,7 +71,7 @@ Thanks for the issue report. Please reformat your issue to conform to the issue ### Feature requests -Thank you for your interest in improving GitLab. We don't use the issue tracker for feature requests. Things that are wrong but are not a regression compared to older versions of GitLab are considered feature requests and not issues. Please use the [feature request forum](http://feedback.gitlab.com/) for this purpose or create a merge request implementing this feature. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. +Thank you for your interest in improving GitLab. We don't use the issue tracker for feature requests. Things that are wrong but are not a regression compared to older versions of GitLab are considered feature requests and not issues. Please use the \[feature request forum\]\(http://feedback.gitlab.com/) for this purpose or create a merge request implementing this feature. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. ### Issue report for old version -- GitLab From 63f11a68c5e9edf36d062bd4f029d81a0861ef82 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 09:38:46 -0800 Subject: [PATCH 0947/1609] Fix event loading with associations --- app/controllers/dashboard_controller.rb | 15 +++++++++++---- app/controllers/groups_controller.rb | 15 +++++++++++---- app/controllers/projects_controller.rb | 2 +- app/controllers/users_controller.rb | 2 +- app/models/event.rb | 1 + 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index ee9dc343337..eca7b39bcdf 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -22,13 +22,14 @@ class DashboardController < ApplicationController format.html format.json do - @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) - @events = @event_filter.apply_filter(@events).includes(:target, project: :namespace) - @events = @events.limit(20).offset(params[:offset] || 0) + load_events pager_json("events/_events", @events.count) end - format.atom { render layout: false } + format.atom do + load_events + render layout: false + end end end @@ -77,4 +78,10 @@ class DashboardController < ApplicationController def load_projects @projects = current_user.authorized_projects.sorted_by_activity.non_archived end + + def load_events + @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) + @events = @event_filter.apply_filter(@events).with_associations + @events = @events.limit(20).offset(params[:offset] || 0) + end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7b7531f1426..d011523c94f 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -39,13 +39,14 @@ class GroupsController < ApplicationController format.html format.json do - @events = Event.in_projects(project_ids) - @events = event_filter.apply_filter(@events).includes(:target, project: :namespace) - @events = @events.limit(20).offset(params[:offset] || 0) + load_events pager_json("events/_events", @events.count) end - format.atom { render layout: false } + format.atom do + load_events + render layout: false + end end end @@ -154,4 +155,10 @@ class GroupsController < ApplicationController def group_params params.require(:group).permit(:name, :description, :path, :avatar) end + + def load_events + @events = Event.in_projects(project_ids) + @events = event_filter.apply_filter(@events).with_associations + @events = @events.limit(20).offset(params[:offset] || 0) + end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fb58ddd06e7..b0fde88babc 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -76,7 +76,7 @@ class ProjectsController < ApplicationController format.json do @events = @project.events.recent - @events = event_filter.apply_filter(@events).includes(:target, project: :namespace) + @events = event_filter.apply_filter(@events).with_associations @events = @events.limit(limit).offset(params[:offset] || 0) pager_json('events/_events', @events.count) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index b4de500fcf1..8c5605c8b4b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -20,7 +20,7 @@ class UsersController < ApplicationController # Get user activity feed for projects common for both users @events = @user.recent_events. where(project_id: authorized_projects_ids). - includes(:target, project: :namespace).limit(30) + with_associations.limit(30) @title = @user.name @title_url = user_path(@user) diff --git a/app/models/event.rb b/app/models/event.rb index cae7f0be85b..5579ab1dbb0 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -47,6 +47,7 @@ class Event < ActiveRecord::Base scope :recent, -> { order("created_at DESC") } scope :code_push, -> { where(action: PUSHED) } scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent } + scope :with_associations, -> { includes(project: :namespace) } class << self def reset_event_cache_for(target) -- GitLab From 9a5199f00577e759b14e1a2058400105f90e7cf4 Mon Sep 17 00:00:00 2001 From: Stefan Tatschner Date: Wed, 18 Feb 2015 19:06:58 +0100 Subject: [PATCH 0948/1609] Add missing color codes for line anchors, fixes #8628 --- app/assets/stylesheets/highlight/dark.scss | 1 + app/assets/stylesheets/highlight/monokai.scss | 5 +++++ app/assets/stylesheets/highlight/solarized_dark.scss | 5 +++++ app/assets/stylesheets/highlight/solarized_light.scss | 5 +++++ app/assets/stylesheets/highlight/white.scss | 5 +++++ 5 files changed, 21 insertions(+) diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index 4095d35b05f..fcd4d47bace 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -12,6 +12,7 @@ border-left: 1px solid #666; } + // highlight line via anchor pre.hll { background-color: #fff !important; } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 730018e3e28..bcd2e716657 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -12,6 +12,11 @@ border-left: 1px solid #555; } + // highlight line via anchor + pre.hll { + background-color: #49483e !important; + } + .hll { background-color: #49483e } .c { color: #75715e } /* Comment */ .err { color: #960050; background-color: #1e0010 } /* Error */ diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index be6904100ec..4a6b759bd2c 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -12,6 +12,11 @@ border-left: 1px solid #113b46; } + // highlight line via anchor + pre.hll { + background-color: #073642 !important; + } + /* Solarized Dark For use with Jekyll and Pygments diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 55be6e30383..7254f4d7ac1 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -12,6 +12,11 @@ border-left: 1px solid #c5d0d4; } + // highlight line via anchor + pre.hll { + background-color: #eee8d5 !important; + } + /* Solarized Light For use with Jekyll and Pygments diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 050a5d241a6..4d6f5dfd91e 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -12,6 +12,11 @@ border-left: 1px solid #bbb; } + // highlight line via anchor + pre.hll { + background-color: #f8eec7 !important; + } + .hll { background-color: #f8f8f8 } .c { color: #999988; font-style: italic; } .err { color: #a61717; background-color: #e3d2d2; } -- GitLab From 3d6b042e9e8613162b92b2f61342f2f0ee919924 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 18 Feb 2015 21:59:15 +0100 Subject: [PATCH 0949/1609] Fix push access check when not signed in. --- lib/gitlab/git_access.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 6444cec7eb5..9b31190a882 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -6,6 +6,8 @@ module Gitlab attr_reader :params, :project, :git_cmd, :user def self.can_push_to_branch?(user, project, ref) + return false unless user + if project.protected_branch?(ref) && !(project.developers_can_push_to_protected_branch?(ref) && project.team.developer?(user)) user.can?(:push_code_to_protected_branches, project) -- GitLab From 2f0a764d310a8fc6628f560debfa930ef2842297 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 13:28:24 -0800 Subject: [PATCH 0950/1609] Fix user page performance and authorization --- app/controllers/users_controller.rb | 17 ++++++++++------- app/models/user.rb | 11 +++++++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8c5605c8b4b..4c2fe4c3c8d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -4,11 +4,8 @@ class UsersController < ApplicationController layout :determine_layout def show - # Projects user can view - visible_projects = ProjectsFinder.new.execute(current_user) - authorized_projects_ids = visible_projects.pluck(:id) - - @contributed_projects = Project.where(id: authorized_projects_ids). + @contributed_projects = Project. + where(id: authorized_projects_ids & @user.contributed_projects_ids). in_group_namespace.includes(:namespace) @projects = @user.personal_projects. @@ -32,8 +29,8 @@ class UsersController < ApplicationController end def calendar - visible_projects = ProjectsFinder.new.execute(current_user) - calendar = Gitlab::CommitsCalendar.new(visible_projects, @user) + projects = Project.where(id: authorized_projects_ids & @user.contributed_projects_ids) + calendar = Gitlab::CommitsCalendar.new(projects, @user) @timestamps = calendar.timestamps @starting_year = calendar.starting_year @starting_month = calendar.starting_month @@ -58,4 +55,10 @@ class UsersController < ApplicationController return authenticate_user! end end + + def authorized_projects_ids + # Projects user can view + @authorized_projects_ids ||= + ProjectsFinder.new.execute(current_user).pluck(:id) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 2ffcd1478d8..ed9a0168747 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -255,7 +255,7 @@ class User < ActiveRecord::Base counter = 0 base = username while User.by_login(username).present? || Namespace.by_path(username).present? - counter += 1 + counter += 1 username = "#{base}#{counter}" end @@ -459,7 +459,7 @@ class User < ActiveRecord::Base def set_notification_email if self.notification_email.blank? || !self.all_emails.include?(self.notification_email) - self.notification_email = self.email + self.notification_email = self.email end end @@ -607,4 +607,11 @@ class User < ActiveRecord::Base def oauth_authorized_tokens Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) end + + def contributed_projects_ids + Event.where(author_id: self). + reorder(project_id: :desc). + select('DISTINCT(project_id)'). + map(&:project_id) + end end -- GitLab From 155b2d46ae19bebeb9b40794fb9b2e3f6e3ab993 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Wed, 18 Feb 2015 13:48:57 -0800 Subject: [PATCH 0951/1609] Say unassigned instead of WIP for merge requests since it might not fit everyones workflow. --- app/views/projects/merge_requests/_merge_request.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 5afc87fb6b1..1686ca0e876 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -18,7 +18,7 @@ - if merge_request.assignee assigned to #{link_to_member(merge_request.source_project, merge_request.assignee)} - else - Work In Progress + Unassigned - if merge_request.votes_count > 0 = render 'votes/votes_inline', votable: merge_request - if merge_request.notes.any? -- GitLab From 138aa81e60f18214e0a95a6ffc6ec1ddbc27925a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 14:20:26 -0800 Subject: [PATCH 0952/1609] Get contributed projects only if push event exists --- app/models/user.rb | 1 + app/views/users/calendar.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index ed9a0168747..ba148f492a4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -610,6 +610,7 @@ class User < ActiveRecord::Base def contributed_projects_ids Event.where(author_id: self). + code_push. reorder(project_id: :desc). select('DISTINCT(project_id)'). map(&:project_id) diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 13bdc5ed1e7..1d1c974da24 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -1,4 +1,4 @@ -%h4 Calendar +%h4 Commits calendar #cal-heatmap.calendar :javascript new calendar( -- GitLab From 833d4dddf2fc3a933a28b4deb60ef6a3dc7eb0fb Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 14:34:05 -0800 Subject: [PATCH 0953/1609] Dont send 404 if no broadcast messages now because it flood gitlab-shell logs with 404 errors :( --- lib/api/internal.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index b5542c1874b..04ff049989b 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -73,8 +73,6 @@ module API get "/broadcast_message" do if message = BroadcastMessage.current present message, with: Entities::BroadcastMessage - else - not_found! end end end -- GitLab From 558dd811971776fc4a921b79296f5d792b245686 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 14:58:20 -0800 Subject: [PATCH 0954/1609] Improve broadcast message API --- GITLAB_SHELL_VERSION | 2 +- lib/api/helpers.rb | 4 ++-- lib/api/internal.rb | 2 ++ spec/requests/api/internal_spec.rb | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 437459cd94c..73462a5a134 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.5.0 +2.5.1 diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a50ee4659a3..228a719fbdf 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -83,7 +83,7 @@ module API end def authenticate_by_gitlab_shell_token! - unauthorized! unless secret_token == params['secret_token'] + unauthorized! unless secret_token == params['secret_token'].try(:chomp) end def authenticated_as_admin! @@ -236,7 +236,7 @@ module API end def secret_token - File.read(Rails.root.join('.gitlab_shell_secret')) + File.read(Rails.root.join('.gitlab_shell_secret')).chomp end def handle_member_errors(errors) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 04ff049989b..ba3fe619b92 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -73,6 +73,8 @@ module API get "/broadcast_message" do if message = BroadcastMessage.current present message, with: Entities::BroadcastMessage + else + {} end end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 10b467d85fd..4c7d15d6594 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -32,7 +32,8 @@ describe API::API, api: true do it do get api("/internal/broadcast_message"), secret_token: secret_token - expect(response.status).to eq(404) + expect(response.status).to eq(200) + expect(json_response).to be_empty end end end -- GitLab From 716544085ce3d100f466103ba5d7c00a771ba6ca Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 15:16:13 -0800 Subject: [PATCH 0955/1609] Get contributed projects for last year only --- app/models/user.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/user.rb b/app/models/user.rb index ba148f492a4..3bbbd23c1bd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -610,6 +610,7 @@ class User < ActiveRecord::Base def contributed_projects_ids Event.where(author_id: self). + where("created_at > ?", Time.now - 1.year). code_push. reorder(project_id: :desc). select('DISTINCT(project_id)'). -- GitLab From 86d5e20664856a0f6635ba184dd85a4f342f8b8f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 16:40:22 -0800 Subject: [PATCH 0956/1609] Respect star ordering on explore page --- app/controllers/explore/projects_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index d75fd8e72fa..0e5891ae807 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -18,7 +18,7 @@ class Explore::ProjectsController < ApplicationController def starred @starred_projects = ProjectsFinder.new.execute(current_user) - @starred_projects = @starred_projects.order('star_count DESC') + @starred_projects = @starred_projects.reorder('star_count DESC') @starred_projects = @starred_projects.page(params[:page]).per(10) end end -- GitLab From 5555c4d99c3d2eeaf171d6e4178a1b7c93b363a6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 17:42:29 -0800 Subject: [PATCH 0957/1609] Time for 7.9.0.pre --- CHANGELOG | 4 +++- VERSION | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 98592af2190..35387538d39 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ -v 7.8.0 (unreleased) +v 7.9.0 (unreleased) - Fix broken access control for note attachments (Hannes Rosenögger) + +v 7.8.0 (unreleased) - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) - Include issue/mr participants in list of recipients for reassign/close/reopen emails diff --git a/VERSION b/VERSION index ccc446c2f8c..e5d25bf79a9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.8.0.pre +7.9.0.pre -- GitLab From 10e4e2110c388ac43f1ebf437b963f13a1882129 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Wed, 18 Feb 2015 20:49:19 -0800 Subject: [PATCH 0958/1609] Improve the explanation and linking of the Oauth docs. --- doc/README.md | 1 + doc/api/README.md | 3 ++- doc/api/oauth2.md | 11 +++++++---- doc/integration/README.md | 3 +-- doc/integration/external-issue-tracker.md | 3 ++- doc/integration/oauth_provider.md | 6 +++++- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/doc/README.md b/doc/README.md index 932e90e359a..59cfe1bb11a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -10,6 +10,7 @@ - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. +- [OAuth2 provider](integration/oauth_provider.md) to allow you to login to other applications from GitLab. ## Administrator documentation diff --git a/doc/api/README.md b/doc/api/README.md index 8cbba8598d5..dec530d0b81 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -22,6 +22,7 @@ ## Clients Find API Clients for GitLab [on our website](https://about.gitlab.com/applications/#api-clients). +You can use [GitLab as an OAuth2 client](oauth2.md) to make API calls. ## Introduction @@ -67,7 +68,7 @@ curl https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user ``` -Read more about [OAuth2 in GitLab](oauth2.md). +Read more about [GitLab as an OAuth2 client](oauth2.md). ## Status codes diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 7bb391054ce..d416a826f79 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -1,14 +1,17 @@ -# OAuth2 authentication +# GitLab as an OAuth2 client -OAuth2 is a protocol that enables us to get access to private details of user's account without getting its password. +This document is about using other OAuth authentication service providers to sign into GitLab. +If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md). -Before using the OAuth2 you should create an application in user's account. Each application getting unique App ID and App Secret parameters. You should not share them. +OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password. + +Before using the OAuth2 you should create an application in user's account. Each application gets a unique App ID and App Secret parameters. You should not share these. This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper) ## Web Application Flow -This flow is using for authentication from third-party web sites and probably is most used. +This flow is using for authentication from third-party web sites and is probably used the most. It basically consists of an exchange of an authorization token for an access token. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1) This flow consists from 3 steps. diff --git a/doc/integration/README.md b/doc/integration/README.md index 1fc8ab997ec..e5f33d8deed 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -8,9 +8,8 @@ See the documentation below for details on how to configure these services. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. - [Slack](slack.md) Integrate with the Slack chat service -- [OAuth2 provider](oauth_provider.md) OAuth2 application creation -Jenkins support is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jenkins.html). +GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html). ## Project services diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 53d6898b6e8..96755707dee 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -8,6 +8,8 @@ GitLab has a great issue tracker but you can also use an external issue tracker ![Jira screenshot](jira-integration-points.png) +GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html). + ## Configuration ### Project Service @@ -23,7 +25,6 @@ Fill in the required details on the page: * `issues_url` The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the url. This id is used by GitLab as a placeholder to replace the issue number. * `new_issue_url` This is the URL to create a new issue in Redmine for the project linked to this GitLab project. - ### Service Template It is necessary to configure the external issue tracker per project, because project specific details are needed for the integration with GitLab. diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md index 5fdb74a43df..192c321f712 100644 --- a/doc/integration/oauth_provider.md +++ b/doc/integration/oauth_provider.md @@ -1,4 +1,8 @@ -## GitLab as OAuth2 provider +## GitLab as OAuth2 authentication service provider + +This document is about using GitLab as an OAuth authentication service provider to sign into other services. +If you want to use other OAuth authentication service providers to sign into GitLab please see the [OAuth2 client documentation](../api/oauth2.md) + OAuth2 provides client applications a 'secure delegated access' to server resources on behalf of a resource owner. Or you can allow users to sign in to your application with their GitLab.com account. In fact OAuth allows to issue access token to third-party clients by an authorization server, with the approval of the resource owner, or end-user. -- GitLab From ff70d2f24e8b437a4c006b61a9b669309718baad Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 22:06:49 -0800 Subject: [PATCH 0959/1609] Improve GitLab.com integration documentation --- doc/integration/gitlab.md | 27 ++++++++++++++++----------- doc/integration/gitlab_app.png | Bin 0 -> 55325 bytes 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 doc/integration/gitlab_app.png diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index b95ef5c0af3..87400bed5b5 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -1,10 +1,13 @@ -# GitLab OAuth2 OmniAuth Provider +# Integrate your server with GitLab.com -To enable the GitLab OmniAuth provider you must register your application with GitLab. GitLab will generate a client ID and secret key for you to use. +Import projects from GitLab.com and login to your GitLab instance with your GitLab.com account. -1. Sign in to GitLab. +To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com. +GitLab.com will generate a application ID and secret key for you to use. -1. Navigate to your settings. +1. Sign in to GitLab.com + +1. Navigate to your profile settings. 1. Select "Applications" in the left menu. @@ -15,17 +18,17 @@ To enable the GitLab OmniAuth provider you must register your application with G - Redirect URI: ``` - http://gitlab.example.com/import/gitlab/callback - http://gitlab.example.com/users/auth/gitlab/callback + http://your-gitlab.example.com/import/gitlab/callback + http://your-gitlab.example.com/users/auth/gitlab/callback ``` The first link is required for the importer and second for the authorization. 1. Select "Submit". -1. You should now see a Application ID and Secret. Keep this page open as you continue configuration. - -1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). Keep this page open as you continue configuration. ![GitHub app](github_app.png) +1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). + Keep this page open as you continue configuration. + ![GitLab app](gitlab_app.png) 1. On your GitLab server, open the configuration file. @@ -43,7 +46,7 @@ To enable the GitLab OmniAuth provider you must register your application with G sudo -u git -H editor config/gitlab.yml ``` -1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings. +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for inital settings. 1. Add the provider configuration: @@ -76,4 +79,6 @@ To enable the GitLab OmniAuth provider you must register your application with G 1. Restart GitLab for the changes to take effect. -On the sign in page there should now be a GitLab icon below the regular sign in form. Click the icon to begin the authentication process. GitLab will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to your GitLab instance and will be signed in. +On the sign in page there should now be a GitLab.com icon below the regular sign in form. +Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application. +If everything goes well the user will be returned to your GitLab instance and will be signed in. diff --git a/doc/integration/gitlab_app.png b/doc/integration/gitlab_app.png new file mode 100644 index 0000000000000000000000000000000000000000..3f9391a821bcb2d625e4c8e4db10f1da66168240 GIT binary patch literal 55325 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%0D)hhknj`Z@K>S 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 Date: Wed, 18 Feb 2015 22:07:09 -0800 Subject: [PATCH 0960/1609] Fix broken links in documentation --- doc/README.md | 2 +- doc/integration/github.md | 2 +- doc/integration/omniauth.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/README.md b/doc/README.md index 59cfe1bb11a..4e00dceac2b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -10,7 +10,7 @@ - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. -- [OAuth2 provider](integration/oauth_provider.md) to allow you to login to other applications from GitLab. +- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. ## Administrator documentation diff --git a/doc/integration/github.md b/doc/integration/github.md index c9c27859c5e..137d7e9d632 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -35,7 +35,7 @@ To enable the GitHub OmniAuth provider you must register your application with G sudo -u git -H editor config/gitlab.yml ``` -1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings. +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for inital settings. 1. Add the provider configuration: diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 7433de33909..c92fa3ee4b7 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -70,7 +70,7 @@ Now we can choose one or more of the Supported Providers below to continue confi ## Supported Providers - [GitHub](github.md) -- [GitLab](gitlab.md) +- [GitLab.com](gitlab.md) - [Google](google.md) - [Shibboleth](shibboleth.md) - [Twitter](twitter.md) -- GitLab From 3c2139ed172c607467ec6cf412d7ed33147bac22 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Feb 2015 22:23:24 -0800 Subject: [PATCH 0961/1609] Fix trending projects ordering --- app/finders/trending_projects_finder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb index 32d7968924a..a79bd47d986 100644 --- a/app/finders/trending_projects_finder.rb +++ b/app/finders/trending_projects_finder.rb @@ -8,7 +8,7 @@ class TrendingProjectsFinder # for period of time - ex. month projects.joins(:notes).where('notes.created_at > ?', start_date). select("projects.*, count(notes.id) as ncount"). - group("projects.id").order("ncount DESC") + group("projects.id").reorder("ncount DESC") end private -- GitLab From 93e42f690bc057ca0e803074aaeb1b55ea9c2232 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 19 Feb 2015 11:20:58 +0100 Subject: [PATCH 0962/1609] Document fun facts about omnibus-gitlab --- doc/development/omnibus.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 doc/development/omnibus.md diff --git a/doc/development/omnibus.md b/doc/development/omnibus.md new file mode 100644 index 00000000000..0ba354d28a2 --- /dev/null +++ b/doc/development/omnibus.md @@ -0,0 +1,32 @@ +# What you should know about omnibus packages + +Most users install GitLab using our omnibus packages. As a developer it can be +good to know how the omnibus packages differ from what you have on your laptop +when you are coding. + +## Files are owned by root by default + +All the files in the Rails tree (`app/`, `config/` etc.) are owned by 'root' in +omnibus installations. This makes the installation simpler and it provides +extra security. The omnibus reconfigure script contains commands that give +write access to the 'git' user only where needed. + +For example, the 'git' user is allowed to write in the `log/` directory, in +`public/uploads`, and they are allowed to rewrite the `db/schema.rb` file. + +In other cases, the reconfigure script tricks GitLab into not trying to write a +file. For instance, GitLab will generate a `.secret` file if it cannot find one +and write it to the Rails root. In the omnibus packages, reconfigure writes the +`.secret` file first, so that GitLab never tries to write it. + +## Code, data and logs are in separate directories + +The omnibus design separates code (read-only, under `/opt/gitlab`) from data +(read/write, under `/var/opt/gitlab`) and logs (read/write, under +`/var/log/gitlab`). To make this happen the reconfigure script sets custom +paths where it can in GitLab config files, and where there are no path +settings, it uses symlinks. + +For example, `config/gitlab.yml` is treated as data so that file is a symlink. +The same goes for `public/uploads`. The `log/` directory is replaced by omnibus +with a symlink to `/var/log/gitlab/gitlab-rails`. -- GitLab From 008e3d66e9649079037260c072020340c69f3eec Mon Sep 17 00:00:00 2001 From: Standa Opichal Date: Wed, 18 Feb 2015 15:47:10 +0100 Subject: [PATCH 0963/1609] Fix for TeamCity buildQueue REST API build/@branchName. Strips the refs/heads/ if any to get just the branchName out of a :ref e.g.: `refs/heads/feature/newProfile` -> `feature/newProfile` The TeamCity buildQueue POST data require the branchName attribute to contain just the branch name (just xyzbranch and not the full ref in the refs/heads/xyzbranch). With the refs/heads/xyzbranch the build is triggered and looks like it is working however it always ompiles the default branch (or master). --- app/models/project_services/teamcity_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index c4b6ef5d9a9..b6932f1c77b 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -115,13 +115,13 @@ class TeamcityService < CiService end end - def execute(data) + def execute(push) auth = { username: username, password: password, } - branch = data[:ref] + branch = push[:ref].gsub('refs/heads/', '') self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", body: ""\ -- GitLab From 7c3147e6e969a7ae97e2f8d05e536abeeb7d3936 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 08:57:33 -0800 Subject: [PATCH 0964/1609] Revert "Nitpicking." This reverts commit ebd39fc082b09177e0777e5de5729c3f98495e87. --- app/controllers/files_controller.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index 15523cbc2e7..561af8084c3 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -5,10 +5,9 @@ class FilesController < ApplicationController if uploader.file_storage? if can?(current_user, :read_project, note.project) - # Replace old notes location in /public with the new one in / and send the file - path = uploader.file.path.gsub("#{Rails.root}/public", Rails.root.to_s) - disposition = uploader.image? ? 'inline' : 'attachment' + # Replace old notes location in /public with the new one in / and send the file + path = uploader.file.path.gsub("#{Rails.root}/public",Rails.root.to_s) send_file path, disposition: disposition else not_found! -- GitLab From 8184a6564454faf0f9ae9dfee1377c3407d08447 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 08:57:35 -0800 Subject: [PATCH 0965/1609] Revert "Fix broken access control and refactor avatar upload" This reverts commit 7d5f86f6cbd187e75a6ba164ad6bfd036977dd07. --- app/controllers/files_controller.rb | 4 +- app/models/group.rb | 2 +- app/models/project.rb | 2 +- app/models/user.rb | 2 +- app/uploaders/attachment_uploader.rb | 8 +++- app/uploaders/avatar_uploader.rb | 32 --------------- db/migrate/20150213111727_move_note_folder.rb | 19 --------- features/steps/groups.rb | 2 +- features/steps/profile/profile.rb | 2 +- features/steps/project/project.rb | 2 +- lib/backup/manager.rb | 2 +- lib/backup/uploads.rb | 40 ++++++------------- uploads/.gitkeep | 0 13 files changed, 27 insertions(+), 90 deletions(-) delete mode 100644 app/uploaders/avatar_uploader.rb delete mode 100644 db/migrate/20150213111727_move_note_folder.rb delete mode 100644 uploads/.gitkeep diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index 561af8084c3..9671245d3f4 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -6,9 +6,7 @@ class FilesController < ApplicationController if uploader.file_storage? if can?(current_user, :read_project, note.project) disposition = uploader.image? ? 'inline' : 'attachment' - # Replace old notes location in /public with the new one in / and send the file - path = uploader.file.path.gsub("#{Rails.root}/public",Rails.root.to_s) - send_file path, disposition: disposition + send_file uploader.file.path, disposition: disposition else not_found! end diff --git a/app/models/group.rb b/app/models/group.rb index da9621a2a1a..d6ec0be6081 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -23,7 +23,7 @@ class Group < Namespace validate :avatar_type, if: ->(user) { user.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - mount_uploader :avatar, AvatarUploader + mount_uploader :avatar, AttachmentUploader after_create :post_create_hook after_destroy :post_destroy_hook diff --git a/app/models/project.rb b/app/models/project.rb index e2c7f76eb09..56e1aa29040 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -138,7 +138,7 @@ class Project < ActiveRecord::Base if: ->(project) { project.avatar && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - mount_uploader :avatar, AvatarUploader + mount_uploader :avatar, AttachmentUploader # Scopes scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } diff --git a/app/models/user.rb b/app/models/user.rb index 3bbbd23c1bd..a9776b633a6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -177,7 +177,7 @@ class User < ActiveRecord::Base end end - mount_uploader :avatar, AvatarUploader + mount_uploader :avatar, AttachmentUploader # Scopes scope :admins, -> { where(admin: true) } diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index 22742d287a4..b122b6c8658 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -3,8 +3,10 @@ class AttachmentUploader < CarrierWave::Uploader::Base storage :file + after :store, :reset_events_cache + def store_dir - "#{Rails.root}/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end def image? @@ -27,4 +29,8 @@ class AttachmentUploader < CarrierWave::Uploader::Base def file_storage? self.class.storage == CarrierWave::Storage::File end + + def reset_events_cache(file) + model.reset_events_cache if model.is_a?(User) + end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb deleted file mode 100644 index 7cad044555b..00000000000 --- a/app/uploaders/avatar_uploader.rb +++ /dev/null @@ -1,32 +0,0 @@ -# encoding: utf-8 - -class AvatarUploader < CarrierWave::Uploader::Base - storage :file - - after :store, :reset_events_cache - - def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" - end - - def image? - img_ext = %w(png jpg jpeg gif bmp tiff) - if file.respond_to?(:extension) - img_ext.include?(file.extension.downcase) - else - # Not all CarrierWave storages respond to :extension - ext = file.path.split('.').last.downcase - img_ext.include?(ext) - end - rescue - false - end - - def file_storage? - self.class.storage == CarrierWave::Storage::File - end - - def reset_events_cache(file) - model.reset_events_cache if model.is_a?(User) - end -end diff --git a/db/migrate/20150213111727_move_note_folder.rb b/db/migrate/20150213111727_move_note_folder.rb deleted file mode 100644 index ca7f87d984f..00000000000 --- a/db/migrate/20150213111727_move_note_folder.rb +++ /dev/null @@ -1,19 +0,0 @@ -class MoveNoteFolder < ActiveRecord::Migration - def up - system( - "if [ -d '#{Rails.root}/public/uploads/note' ]; - then mv #{Rails.root}/public/uploads/note #{Rails.root}/uploads/note; - echo 'note folder has been moved successfully'; - else - echo 'note folder has already been moved or does not exist yet. Nothing to do here.'; fi") - end - - def down - system( - "if [ -d '#{Rails.root}/uploads/note' ]; - then mv #{Rails.root}/uploads/note #{Rails.root}/public/uploads/note; - echo 'note folder has been moved successfully'; - else - echo 'note folder has already been moved or does not exist yet. Nothing to do here.'; fi") - end -end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 0a9b4ccba53..610e7fd3a48 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -110,7 +110,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see new group "Owned" avatar' do - Group.find_by(name: "Owned").avatar.should be_instance_of AvatarUploader + Group.find_by(name: "Owned").avatar.should be_instance_of AttachmentUploader Group.find_by(name: "Owned").avatar.url.should == "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/gitlab_logo.png" end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index 4efd2176782..a907b0b7dcf 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -29,7 +29,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see new avatar' do - @user.avatar.should be_instance_of AvatarUploader + @user.avatar.should be_instance_of AttachmentUploader @user.avatar.url.should == "/uploads/user/avatar/#{ @user.id }/gitlab_logo.png" end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index d39c8e7d2db..033d45e0253 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -35,7 +35,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see new project avatar' do - @project.avatar.should be_instance_of AvatarUploader + @project.avatar.should be_instance_of AttachmentUploader url = @project.avatar.url url.should == "/uploads/project/avatar/#{ @project.id }/gitlab_logo.png" end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 06cd40a5b1c..ab8db4e9837 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,6 +1,6 @@ module Backup class Manager - BACKUP_CONTENTS = %w{repositories/ db/ public/ uploads/ backup_information.yml} + BACKUP_CONTENTS = %w{repositories/ db/ uploads/ backup_information.yml} def pack # saving additional informations diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index 75d8e18a862..e50e1ff4f13 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -1,45 +1,29 @@ module Backup class Uploads - attr_reader :app_public_uploads_dir, :app_private_uploads_dir, :backup_public_uploads_dir, - :backup_private_uploads_dir, :backup_dir, :backup_public_dir + attr_reader :app_uploads_dir, :backup_uploads_dir, :backup_dir def initialize - @app_public_uploads_dir = File.realpath(Rails.root.join('public', 'uploads')) - @app_private_uploads_dir = File.realpath(Rails.root.join('uploads')) + @app_uploads_dir = File.realpath(Rails.root.join('public', 'uploads')) @backup_dir = Gitlab.config.backup.path - @backup_public_dir = File.join(backup_dir, 'public') - @backup_public_uploads_dir = File.join(backup_dir, 'public', 'uploads') - @backup_private_uploads_dir = File.join(backup_dir, 'uploads') + @backup_uploads_dir = File.join(Gitlab.config.backup.path, 'uploads') end - # Copy uploads from public/uploads to backup/public/uploads and from /uploads to backup/uploads + # Copy uploads from public/uploads to backup/uploads def dump - FileUtils.mkdir_p(backup_public_uploads_dir) - FileUtils.cp_r(app_public_uploads_dir, backup_public_dir) - - FileUtils.mkdir_p(backup_private_uploads_dir) - FileUtils.cp_r(app_private_uploads_dir, backup_dir) + FileUtils.mkdir_p(backup_uploads_dir) + FileUtils.cp_r(app_uploads_dir, backup_dir) end def restore - backup_existing_public_uploads_dir - backup_existing_private_uploads_dir + backup_existing_uploads_dir - FileUtils.cp_r(backup_public_uploads_dir, app_public_uploads_dir) - FileUtils.cp_r(backup_private_uploads_dir, app_private_uploads_dir) + FileUtils.cp_r(backup_uploads_dir, app_uploads_dir) end - def backup_existing_public_uploads_dir - timestamped_public_uploads_path = File.join(app_public_uploads_dir, '..', "uploads.#{Time.now.to_i}") - if File.exists?(app_public_uploads_dir) - FileUtils.mv(app_public_uploads_dir, timestamped_public_uploads_path) - end - end - - def backup_existing_private_uploads_dir - timestamped_private_uploads_path = File.join(app_private_uploads_dir, '..', "uploads.#{Time.now.to_i}") - if File.exists?(app_private_uploads_dir) - FileUtils.mv(app_private_uploads_dir, timestamped_private_uploads_path) + def backup_existing_uploads_dir + timestamped_uploads_path = File.join(app_uploads_dir, '..', "uploads.#{Time.now.to_i}") + if File.exists?(app_uploads_dir) + FileUtils.mv(app_uploads_dir, timestamped_uploads_path) end end end diff --git a/uploads/.gitkeep b/uploads/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 -- GitLab From 1d6050104c17d7924d5cce0e6ddb35f5da45a08e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 19 Feb 2015 17:16:46 +0100 Subject: [PATCH 0966/1609] Correctly set default projects limit for new users. --- app/models/user.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 3bbbd23c1bd..13d4eae0044 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -55,14 +55,13 @@ class User < ActiveRecord::Base include Gitlab::ConfigHelper include TokenAuthenticatable extend Gitlab::ConfigHelper - extend Gitlab::CurrentSettings + include Gitlab::CurrentSettings default_value_for :admin, false default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false default_value_for :hide_no_password, false - default_value_for :projects_limit, current_application_settings.default_projects_limit default_value_for :theme_id, gitlab_config.default_theme devise :database_authenticatable, :lockable, :async, @@ -141,6 +140,7 @@ class User < ActiveRecord::Base before_save :ensure_authentication_token after_save :ensure_namespace_correct + after_initialize :set_projects_limit after_create :post_create_hook after_destroy :post_destroy_hook @@ -463,6 +463,13 @@ class User < ActiveRecord::Base end end + def set_projects_limit + connection_default_value_defined = new_record? && !projects_limit_changed? + return unless self.projects_limit.nil? || connection_default_value_defined + + self.projects_limit = current_application_settings.default_projects_limit + end + def requires_ldap_check? if !Gitlab.config.ldap.enabled false -- GitLab From d4e43dc1e6c8d61fcc0bbafbccdda845cb533e08 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 11:58:29 -0800 Subject: [PATCH 0967/1609] Bump gitlab-shell --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 73462a5a134..aedc15bb0c6 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.5.1 +2.5.3 -- GitLab From ee26dae63e312e236a6e7f4c79ee1e382c4082a2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 15:02:49 -0800 Subject: [PATCH 0968/1609] Update bootstrap-sass gem --- Gemfile.lock | 8 ++++++-- .../stylesheets/sections/merge_requests.scss | 2 ++ app/views/admin/projects/index.html.haml | 14 ++++++-------- app/views/devise/sessions/_new_base.html.haml | 8 ++++---- app/views/projects/_visibility_level.html.haml | 2 +- .../merge_requests/show/_mr_accept.html.haml | 2 +- 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3283da40f8d..a9784f36ac9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,6 +47,9 @@ GEM astrolabe (1.3.0) parser (>= 2.2.0.pre.3, < 3.0) attr_required (1.0.0) + autoprefixer-rails (5.1.6) + execjs + json awesome_print (1.2.0) axiom-types (0.0.5) descendants_tracker (~> 0.0.1) @@ -57,8 +60,9 @@ GEM erubis (>= 2.6.6) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootstrap-sass (3.0.3.0) - sass (~> 3.2) + bootstrap-sass (3.3.3) + autoprefixer-rails (>= 5.0.0.1) + sass (>= 3.2.19) browser (0.7.2) builder (3.2.2) cal-heatmap-rails (0.0.1) diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 81cd6d745b6..a3eabb5e330 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -24,6 +24,7 @@ .accept-control { display: inline-block; + margin: 0; margin-left: 20px; padding: 10px 0; line-height: 20px; @@ -31,6 +32,7 @@ .remove_source_checkbox { margin: 0; + font-weight: bold; } } } diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 36a4a2fb4af..70121c84b4d 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -13,15 +13,13 @@ .form-group %strong Activity .checkbox - = label_tag :with_push, 'Not empty' - = check_box_tag :with_push, 1, params[:with_push] -   - %span.light Projects with push events + = label_tag :with_push do + = check_box_tag :with_push, 1, params[:with_push] + %span Projects with push events .checkbox - = label_tag :abandoned, 'Abandoned' - = check_box_tag :abandoned, 1, params[:abandoned] -   - %span.light No activity over 6 month + = label_tag :abandoned do + = check_box_tag :abandoned, 1, params[:abandoned] + %span No activity over 6 month %fieldset %strong Visibility level: diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index ab9085f0ba7..54a39726771 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -2,11 +2,11 @@ = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus" = f.password_field :password, class: "form-control bottom", placeholder: "Password" - if devise_mapping.rememberable? - .remember-me - %label.checkbox.remember_me{for: "user_remember_me"} + .remember-me.checkbox + %label{for: "user_remember_me"} = f.check_box :remember_me %span Remember me - .pull-right - = link_to "Forgot your password?", new_password_path(resource_name) + .pull-right + = link_to "Forgot your password?", new_password_path(resource_name) %div = f.submit "Sign in", class: "btn btn-save" diff --git a/app/views/projects/_visibility_level.html.haml b/app/views/projects/_visibility_level.html.haml index 5f34e66b3ed..42c8e685224 100644 --- a/app/views/projects/_visibility_level.html.haml +++ b/app/views/projects/_visibility_level.html.haml @@ -7,8 +7,8 @@ - Gitlab::VisibilityLevel.values.each do |level| .radio - restricted = restricted_visibility_levels.include?(level) - = f.radio_button :visibility_level, level, checked: (visibility_level == level), disabled: restricted = label :project_visibility_level, level do + = f.radio_button :visibility_level, level, checked: (visibility_level == level), disabled: restricted = visibility_level_icon(level) .option-title = visibility_level_label(level) diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index f8ee6973637..d58d20a9442 100644 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -17,7 +17,7 @@ .accept-action = f.submit "Accept Merge Request", class: "btn btn-create accept_merge_request" - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? - .accept-control + .accept-control.checkbox = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do = check_box_tag :should_remove_source_branch Remove source-branch -- GitLab From 6177256033f045e14161c8672223da84c6db56dc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 15:46:48 -0800 Subject: [PATCH 0969/1609] Move labels/milestones tabs to side navigation --- app/views/layouts/nav/_project.html.haml | 10 ++++ app/views/projects/_issues_nav.html.haml | 51 ------------------- app/views/projects/issues/index.html.haml | 25 ++++++++- app/views/projects/labels/index.html.haml | 2 - .../projects/merge_requests/index.html.haml | 9 +++- app/views/projects/milestones/index.html.haml | 1 - app/views/projects/milestones/show.html.haml | 1 - 7 files changed, 42 insertions(+), 57 deletions(-) delete mode 100644 app/views/projects/_issues_nav.html.haml diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 8d572ddcd10..ecbd821b1b9 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -49,6 +49,11 @@ %span Graphs + = nav_link(controller: :milestones) do + = link_to project_milestones_path(@project), title: 'Milestones' do + %i.fa.fa-clock-o + Milestones + - if project_nav_tab? :issues = nav_link(controller: %w(issues milestones labels)) do = link_to url_for_project_issues, title: 'Issues', class: 'shortcuts-issues' do @@ -66,6 +71,11 @@ Merge Requests %span.count.merge_counter= @project.merge_requests.opened.count + = nav_link(controller: :labels) do + = link_to project_labels_path(@project), title: 'Labels' do + %i.fa.fa-tags + Labels + - if project_nav_tab? :wiki = nav_link(controller: :wikis) do = link_to project_wiki_path(@project, :home), title: 'Wiki', class: 'shortcuts-wiki' do diff --git a/app/views/projects/_issues_nav.html.haml b/app/views/projects/_issues_nav.html.haml deleted file mode 100644 index f4e3d9a1093..00000000000 --- a/app/views/projects/_issues_nav.html.haml +++ /dev/null @@ -1,51 +0,0 @@ -%ul.nav.nav-tabs - - if project_nav_tab? :issues - = nav_link(controller: :issues) do - = link_to project_issues_path(@project), class: "tab" do - %i.fa.fa-exclamation-circle - Issues - - if project_nav_tab? :merge_requests - = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project), class: "tab" do - %i.fa.fa-tasks - Merge Requests - = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), class: "tab" do - %i.fa.fa-clock-o - Milestones - = nav_link(controller: :labels) do - = link_to project_labels_path(@project), class: "tab" do - %i.fa.fa-tags - Labels - - - - if current_controller?(:issues) - - if current_user - %li.hidden-xs - = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }) do - %i.fa.fa-rss - - %li.pull-right - .pull-right - .pull-left - = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do - .append-right-10.hidden-xs.hidden-sm - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } - = hidden_field_tag :state, params['state'] - = hidden_field_tag :scope, params['scope'] - = hidden_field_tag :assignee_id, params['assignee_id'] - = hidden_field_tag :milestone_id, params['milestone_id'] - = hidden_field_tag :label_id, params['label_id'] - - - if can? current_user, :write_issue, @project - = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do - %i.fa.fa-plus - New Issue - - - if current_controller?(:merge_requests) - %li.pull-right - .pull-right - - if can? current_user, :write_merge_request, @project - = link_to new_project_merge_request_path(@project), class: "btn btn-new pull-left", title: "New Merge Request" do - %i.fa.fa-plus - New Merge Request diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 0d00d6bfded..669ba224177 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,4 +1,27 @@ -= render "projects/issues_nav" +%h3.page-title + Issues + - if current_user + .hidden-xs.inline + = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }) do + %small + %i.fa.fa-rss + .pull-right + .pull-left + = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do + .append-right-10.hidden-xs.hidden-sm + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } + = hidden_field_tag :state, params['state'] + = hidden_field_tag :scope, params['scope'] + = hidden_field_tag :assignee_id, params['assignee_id'] + = hidden_field_tag :milestone_id, params['milestone_id'] + = hidden_field_tag :label_id, params['label_id'] + - if can? current_user, :write_issue, @project + = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do + %i.fa.fa-plus + New Issue + + +%hr .issues-holder = render "issues" diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index c7c17c7797e..1ad7bdeffe5 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,5 +1,3 @@ -= render "projects/issues_nav" - - if can? current_user, :admin_label, @project = link_to new_project_label_path(@project), class: "pull-right btn btn-new" do New label diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 2654ea70990..e4ee583c798 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,5 +1,12 @@ -= render "projects/issues_nav" +%h3.page-title + Merge Requests + .pull-right + - if can? current_user, :write_merge_request, @project + = link_to new_project_merge_request_path(@project), class: "btn btn-new pull-left", title: "New Merge Request" do + %i.fa.fa-plus + New Merge Request +%hr .merge-requests-holder .append-bottom-10 = render 'shared/issuable_filter' diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 04a1b9243d5..7bad9c98548 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,4 +1,3 @@ -= render "projects/issues_nav" .milestones_content %h3.page-title Milestones diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 031b5a31895..0187c65bc2c 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,4 +1,3 @@ -= render "projects/issues_nav" %h4.page-title .issue-box{ class: issue_box_class(@milestone) } - if @milestone.closed? -- GitLab From b876793d031247bed2aa3b31dc9ebb960bef47e0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 15:47:10 -0800 Subject: [PATCH 0970/1609] Restlye issueable filters to be more compact --- app/assets/javascripts/issues.js.coffee | 6 +- app/views/projects/issues/_issues.html.haml | 15 -- app/views/projects/issues/index.html.haml | 25 ++- app/views/shared/_issuable_filter.html.haml | 193 ++++++++++---------- 4 files changed, 120 insertions(+), 119 deletions(-) diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 2499ad5ad80..6513f4bcefc 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -15,7 +15,7 @@ $(this).html totalIssues + 1 else $(this).html totalIssues - 1 - $("body").on "click", ".issues-filters .dropdown-menu a", -> + $("body").on "click", ".issues-other-filters .dropdown-menu a", -> $('.issues-list').block( message: null, overlayCSS: @@ -77,9 +77,9 @@ ids.push $(value).attr("data-id") $("#update_issues_ids").val ids - $(".issues-filters").hide() + $(".issues-other-filters").hide() $(".issues_bulk_update").show() else $("#update_issues_ids").val [] $(".issues_bulk_update").hide() - $(".issues-filters").show() + $(".issues-other-filters").show() diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 816851a8abe..5d243adb5fe 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -1,18 +1,3 @@ -.append-bottom-10 - .check-all-holder - = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left", disabled: !can?(current_user, :modify_issue, @project) - = render 'shared/issuable_filter' - - .clearfix - .issues_bulk_update.hide - = form_tag bulk_update_project_issues_path(@project), method: :post do - = select_tag('update[status]', options_for_select([['Open', 'open'], ['Closed', 'closed']]), prompt: "Status") - = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') - = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") - = hidden_field_tag 'update[issues_ids]', [] - = hidden_field_tag :status, params[:status] - = button_tag "Update issues", class: "btn update_selected_issues btn-save" - .panel.panel-default %ul.well-list.issues-list = render @issues diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 669ba224177..0d0e3e3c82f 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,12 +1,11 @@ -%h3.page-title - Issues - - if current_user - .hidden-xs.inline - = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }) do - %small - %i.fa.fa-rss +.append-bottom-10 .pull-right .pull-left + - if current_user + .hidden-xs.pull-left + = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do + %i.fa.fa-rss + = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do .append-right-10.hidden-xs.hidden-sm = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } @@ -21,7 +20,17 @@ %i.fa.fa-plus New Issue + = render 'shared/issuable_filter' + + .clearfix + .issues_bulk_update.hide + = form_tag bulk_update_project_issues_path(@project), method: :post do + = select_tag('update[status]', options_for_select([['Open', 'open'], ['Closed', 'closed']]), prompt: "Status") + = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') + = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") + = hidden_field_tag 'update[issues_ids]', [] + = hidden_field_tag :status, params[:status] + = button_tag "Update issues", class: "btn update_selected_issues btn-save" -%hr .issues-holder = render "issues" diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml index cd97481bb6c..707c668dd89 100644 --- a/app/views/shared/_issuable_filter.html.haml +++ b/app/views/shared/_issuable_filter.html.haml @@ -1,6 +1,6 @@ .issues-filters - .pull-left.append-right-20 - %ul.nav.nav-pills.nav-compact + .issues-state-filters + %ul.nav.nav-tabs %li{class: ("active" if params[:state] == 'opened')} = link_to page_filter_path(state: 'opened') do %i.fa.fa-exclamation-circle @@ -14,99 +14,106 @@ %i.fa.fa-compass All - .dropdown.inline.assignee-filter - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light assignee: - - if @assignee.present? - %strong= @assignee.name - - elsif params[:assignee_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(assignee_id: nil) do - Any - = link_to page_filter_path(assignee_id: 0) do - Unassigned - - @assignees.sort_by(&:name).each do |user| - %li - = link_to page_filter_path(assignee_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name - - .dropdown.inline.prepend-left-10.author-filter - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light author: - - if @author.present? - %strong= @author.name - - elsif params[:author_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(author_id: nil) do - Any - = link_to page_filter_path(author_id: 0) do - Unassigned - - @authors.sort_by(&:name).each do |user| - %li - = link_to page_filter_path(author_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name - - .dropdown.inline.prepend-left-10.milestone-filter - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-clock-o - %span.light milestone: - - if @milestone.present? - %strong= @milestone.title - - elsif params[:milestone_id] == "0" - None (backlog) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(milestone_id: nil) do - Any - = link_to page_filter_path(milestone_id: 0) do - None (backlog) - - @milestones.each do |milestone| - %li - = link_to page_filter_path(milestone_id: milestone.id) do - %strong= milestone.title - %small.light= milestone.expires_at + %div + - if controller.controller_name == 'issues' + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left", + disabled: !can?(current_user, :modify_issue, @project) + .issues-other-filters + .dropdown.inline.assignee-filter + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-user + %span.light assignee: + - if @assignee.present? + %strong= @assignee.name + - elsif params[:assignee_id] == "0" + Unassigned + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to page_filter_path(assignee_id: nil) do + Any + = link_to page_filter_path(assignee_id: 0) do + Unassigned + - @assignees.sort_by(&:name).each do |user| + %li + = link_to page_filter_path(assignee_id: user.id) do + = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' + = user.name - - if @project - .dropdown.inline.prepend-left-10.labels-filter - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-tags - %span.light label: - - if params[:label_name].present? - %strong= params[:label_name] - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(label_name: nil) do + .dropdown.inline.prepend-left-10.author-filter + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-user + %span.light author: + - if @author.present? + %strong= @author.name + - elsif params[:author_id] == "0" + Unassigned + - else Any - - if @project.labels.any? - - @project.labels.each do |label| + %b.caret + %ul.dropdown-menu + %li + = link_to page_filter_path(author_id: nil) do + Any + = link_to page_filter_path(author_id: 0) do + Unassigned + - @authors.sort_by(&:name).each do |user| %li - = link_to page_filter_path(label_name: label.name) do - = render_colored_label(label) - - else + = link_to page_filter_path(author_id: user.id) do + = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' + = user.name + + .dropdown.inline.prepend-left-10.milestone-filter + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-clock-o + %span.light milestone: + - if @milestone.present? + %strong= @milestone.title + - elsif params[:milestone_id] == "0" + None (backlog) + - else + Any + %b.caret + %ul.dropdown-menu %li - = link_to generate_project_labels_path(@project, redirect: request.original_url), method: :post do - %i.fa.fa-plus-circle - Create default labels + = link_to page_filter_path(milestone_id: nil) do + Any + = link_to page_filter_path(milestone_id: 0) do + None (backlog) + - @milestones.each do |milestone| + %li + = link_to page_filter_path(milestone_id: milestone.id) do + %strong= milestone.title + %small.light= milestone.expires_at + + - if @project + .dropdown.inline.prepend-left-10.labels-filter + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-tags + %span.light label: + - if params[:label_name].present? + %strong= params[:label_name] + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to page_filter_path(label_name: nil) do + Any + - if @project.labels.any? + - @project.labels.each do |label| + %li + = link_to page_filter_path(label_name: label.name) do + = render_colored_label(label) + - else + %li + = link_to generate_project_labels_path(@project, redirect: request.original_url), method: :post do + %i.fa.fa-plus-circle + Create default labels - .pull-right - = render 'shared/sort_dropdown' + .pull-right + = render 'shared/sort_dropdown' -- GitLab From 4511bc1f3d7b28a01a0d35d69eea14b80f6bf91f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 15:56:23 -0800 Subject: [PATCH 0971/1609] Prettify milestones page --- app/views/projects/merge_requests/index.html.haml | 14 +++++--------- app/views/projects/milestones/index.html.haml | 13 +++++-------- app/views/shared/_milestones_filter.html.haml | 2 +- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index e4ee583c798..35e85156951 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,14 +1,10 @@ -%h3.page-title - Merge Requests - .pull-right - - if can? current_user, :write_merge_request, @project - = link_to new_project_merge_request_path(@project), class: "btn btn-new pull-left", title: "New Merge Request" do - %i.fa.fa-plus - New Merge Request - -%hr .merge-requests-holder .append-bottom-10 + .pull-right + - if can? current_user, :write_merge_request, @project + = link_to new_project_merge_request_path(@project), class: "btn btn-new pull-left", title: "New Merge Request" do + %i.fa.fa-plus + New Merge Request = render 'shared/issuable_filter' .panel.panel-default %ul.well-list.mr-list diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 7bad9c98548..6060f1bf860 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,11 +1,8 @@ -.milestones_content - %h3.page-title - Milestones - - if can? current_user, :admin_milestone, @project - = link_to new_project_milestone_path(@project), class: "pull-right btn btn-new", title: "New Milestone" do - %i.fa.fa-plus - New Milestone - +.pull-right + - if can? current_user, :admin_milestone, @project + = link_to new_project_milestone_path(@project), class: "pull-right btn btn-new", title: "New Milestone" do + %i.fa.fa-plus + New Milestone = render 'shared/milestones_filter' .milestones diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 208f1b77372..f685ae7726c 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,5 +1,5 @@ .milestones-filters.append-bottom-10 - %ul.nav.nav-pills.nav-compact + %ul.nav.nav-tabs %li{class: ("active" if params[:state].blank? || params[:state] == 'opened')} = link_to milestones_filter_path(state: 'opened') do %i.fa.fa-exclamation-circle -- GitLab From 78aa1bb4e2aa355c8567ab660756a1bfc884df36 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 16:16:41 -0800 Subject: [PATCH 0972/1609] Fix tab highlighting --- app/views/layouts/nav/_project.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index ecbd821b1b9..6fbaeb45e32 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -55,7 +55,7 @@ Milestones - if project_nav_tab? :issues - = nav_link(controller: %w(issues milestones labels)) do + = nav_link(controller: :issues) do = link_to url_for_project_issues, title: 'Issues', class: 'shortcuts-issues' do %i.fa.fa-exclamation-circle %span -- GitLab From ad67bf51d2c9c0a3d3061346336cd85f482931b5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 16:24:59 -0800 Subject: [PATCH 0973/1609] Fix collapsing of milestones and labels items --- app/views/layouts/nav/_project.html.haml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 6fbaeb45e32..caf319899f8 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -52,7 +52,8 @@ = nav_link(controller: :milestones) do = link_to project_milestones_path(@project), title: 'Milestones' do %i.fa.fa-clock-o - Milestones + %span + Milestones - if project_nav_tab? :issues = nav_link(controller: :issues) do @@ -74,7 +75,8 @@ = nav_link(controller: :labels) do = link_to project_labels_path(@project), title: 'Labels' do %i.fa.fa-tags - Labels + %span + Labels - if project_nav_tab? :wiki = nav_link(controller: :wikis) do -- GitLab From c2623d2e203914840a5a9173b7e12aa77597d869 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 16:27:20 -0800 Subject: [PATCH 0974/1609] Sidebar items should be same height for collapsed and expanded version --- app/assets/stylesheets/sections/nav_sidebar.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss index 9c7d1a03a03..e9b97c5ea3e 100644 --- a/app/assets/stylesheets/sections/nav_sidebar.scss +++ b/app/assets/stylesheets/sections/nav_sidebar.scss @@ -65,7 +65,7 @@ color: #555; display: block; text-decoration: none; - padding: 6px 15px; + padding: 8px 15px; font-size: 13px; line-height: 20px; text-shadow: 0 1px 2px #FFF; @@ -133,7 +133,7 @@ li a { padding-left: 18px; font-size: 14px; - padding: 10px 15px; + padding: 8px 15px; text-align: center; & > span { -- GitLab From 00ac564423249c5be50e44d44ef822b4b686a931 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 16:46:19 -0800 Subject: [PATCH 0975/1609] Improve sidebar active state --- app/assets/stylesheets/sections/nav_sidebar.scss | 7 +++++-- app/views/layouts/nav/_project.html.haml | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss index e9b97c5ea3e..de97be30b7d 100644 --- a/app/assets/stylesheets/sections/nav_sidebar.scss +++ b/app/assets/stylesheets/sections/nav_sidebar.scss @@ -40,9 +40,12 @@ .nav-sidebar li { &.active a { - color: #111; - background: #EEE; + color: #333; + background: #FFF; font-weight: bold; + border: 1px solid #EEE; + border-right: 1px solid transparent; + border-left: 3px solid $style_color; &.no-highlight { background: none; diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index caf319899f8..96d156e00d6 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -6,7 +6,7 @@ %span Back to project - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do + = nav_link(html_options: {class: "separate-item"}) do = link_to edit_project_path(@project), title: 'Settings', class: "stat-tab tab no-highlight" do %i.fa.fa-cogs %span -- GitLab From 6a6a33452288542aa93354f6ce5a7720721e0688 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 17:24:34 -0800 Subject: [PATCH 0976/1609] Fix active tab tests --- .../stylesheets/sections/nav_sidebar.scss | 1 + app/views/layouts/nav/_project.html.haml | 2 +- features/project/active_tab.feature | 17 ++++++----------- features/steps/project/active_tab.rb | 8 ++++---- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss index de97be30b7d..3ef2a578b7f 100644 --- a/app/assets/stylesheets/sections/nav_sidebar.scss +++ b/app/assets/stylesheets/sections/nav_sidebar.scss @@ -49,6 +49,7 @@ &.no-highlight { background: none; + border: none; } i { diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 96d156e00d6..caf319899f8 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -6,7 +6,7 @@ %span Back to project - = nav_link(html_options: {class: "separate-item"}) do + = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do = link_to edit_project_path(@project), title: 'Settings', class: "stat-tab tab no-highlight" do %i.fa.fa-cogs %span diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index ed548177837..05faad4e645 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -106,24 +106,19 @@ Feature: Project Active Tab And no other sub tabs should be active And the active main tab should be Commits - # Sub Tabs: Issues - Scenario: On Project Issues/Browse Given I visit my project's issues page - Then the active sub tab should be Issues - And no other sub tabs should be active - And the active main tab should be Issues + Then the active main tab should be Issues + And no other main tabs should be active Scenario: On Project Issues/Milestones Given I visit my project's issues page And I click the "Milestones" tab - Then the active sub tab should be Milestones - And no other sub tabs should be active - And the active main tab should be Issues + Then the active main tab should be Milestones + And no other main tabs should be active Scenario: On Project Issues/Labels Given I visit my project's issues page And I click the "Labels" tab - Then the active sub tab should be Labels - And no other sub tabs should be active - And the active main tab should be Issues + Then the active main tab should be Labels + And no other main tabs should be active diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index bb42d15eae5..dd3215adb1a 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -93,11 +93,11 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps ensure_active_sub_tab('Issues') end - step 'the active sub tab should be Milestones' do - ensure_active_sub_tab('Milestones') + step 'the active main tab should be Milestones' do + ensure_active_main_tab('Milestones') end - step 'the active sub tab should be Labels' do - ensure_active_sub_tab('Labels') + step 'the active main tab should be Labels' do + ensure_active_main_tab('Labels') end end -- GitLab From ee6c4a2cca65d56c01156950d62dfb2f01839cb9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 18:23:10 -0800 Subject: [PATCH 0977/1609] Improve commits UI --- CHANGELOG | 2 +- app/assets/stylesheets/sections/commits.scss | 21 ++++++++++++++++--- app/views/projects/commits/_commit.html.haml | 7 ++++--- app/views/projects/commits/_commits.html.haml | 9 ++++---- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 35387538d39..575afcbbb6a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ v 7.9.0 (unreleased) - - Fix broken access control for note attachments (Hannes Rosenögger) + - Move labels/milestones tabs to sidebar v 7.8.0 (unreleased) - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss index 2e274d06c12..f6723eb308f 100644 --- a/app/assets/stylesheets/sections/commits.scss +++ b/app/assets/stylesheets/sections/commits.scss @@ -136,10 +136,13 @@ /** * COMMIT ROW */ -li.commit { +ul li.commit { + padding: 8px 0; + .commit-row-title { font-size: $list-font-size; - margin-bottom: 2px; + line-height: 20px; + margin-bottom: 5px; .notes_count { float: right; @@ -199,7 +202,7 @@ li.commit { } .committed_ago { - float: right; + display: inline-block; } } @@ -245,3 +248,15 @@ li.commit { z-index: 2; } } + +.commits-row { + ul { + margin: 0; + } + + .commits-row-date { + font-size: 15px; + line-height: 20px; + margin-bottom: 5px; + } +} diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1eb17f760dc..1bf1ada1680 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,8 +1,6 @@ %li.commit.js-toggle-container .commit-row-title - = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" -   - %span.str-truncated + %strong.str-truncated = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message" - if commit.description? %a.text-expander.js-toggle-button ... @@ -27,5 +25,8 @@ .commit-row-info = commit_author_link(commit, avatar: true, size: 16) + authored .committed_ago #{time_ago_with_tooltip(commit.committed_date)}   + .pull-right + = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 2d0ca671fa0..0cd9ce1f371 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -3,12 +3,13 @@ - @commits.group_by { |c| c.committed_date.to_date }.sort.reverse.each do |day, commits| .row.commits-row - .col-md-2 - %h4 + .col-md-2.hidden-xs.hidden-sm + %h5.commits-row-date %i.fa.fa-calendar %span= day.stamp("28 Aug, 2010") - %p= pluralize(commits.count, 'commit') - .col-md-10 + .light + = pluralize(commits.count, 'commit') + .col-md-10.col-sm-12 %ul.bordered-list = render commits, project: project %hr.lists-separator -- GitLab From 83e2a1ca12372279cf7948b4d4b3e8a11c50e428 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Thu, 19 Feb 2015 21:07:54 -0700 Subject: [PATCH 0978/1609] Update path helper references Use the path helpers for nested project resources, for Rails 4.1.9 compatibility. --- app/views/layouts/nav/_project.html.haml | 4 ++-- app/views/projects/issues/index.html.haml | 8 ++++---- app/views/projects/merge_requests/index.html.haml | 2 +- app/views/projects/merge_requests/show/_diffs.html.haml | 2 +- .../merge_requests/show/_remove_source_branch.html.haml | 2 +- app/views/search/results/_snippet_title.html.haml | 2 +- app/views/snippets/_snippet.html.haml | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 67e2721bb4a..4d859e817ac 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -50,7 +50,7 @@ Graphs = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: 'Milestones' do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do %i.fa.fa-clock-o %span Milestones @@ -73,7 +73,7 @@ %span.count.merge_counter= @project.merge_requests.opened.count = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: 'Labels' do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do %i.fa.fa-tags %span Labels diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 0d0e3e3c82f..7defc8787a9 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -3,10 +3,10 @@ .pull-left - if current_user .hidden-xs.pull-left - = link_to project_issues_path(@project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do + = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do %i.fa.fa-rss - = form_tag project_issues_path(@project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do + = form_tag namespace_project_issues_path(@project.namespace, @project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do .append-right-10.hidden-xs.hidden-sm = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } = hidden_field_tag :state, params['state'] @@ -16,7 +16,7 @@ = hidden_field_tag :label_id, params['label_id'] - if can? current_user, :write_issue, @project - = link_to new_project_issue_path(@project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do %i.fa.fa-plus New Issue @@ -24,7 +24,7 @@ .clearfix .issues_bulk_update.hide - = form_tag bulk_update_project_issues_path(@project), method: :post do + = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do = select_tag('update[status]', options_for_select([['Open', 'open'], ['Closed', 'closed']]), prompt: "Status") = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 35e85156951..e3b9a28033b 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -2,7 +2,7 @@ .append-bottom-10 .pull-right - if can? current_user, :write_merge_request, @project - = link_to new_project_merge_request_path(@project), class: "btn btn-new pull-left", title: "New Merge Request" do + = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new pull-left", title: "New Merge Request" do %i.fa.fa-plus New Merge Request = render 'shared/issuable_filter' diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index d361c5f579a..eb1640891e6 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -8,5 +8,5 @@ Changes view for this comparison is extremely large. %p You can - = link_to "download it", project_merge_request_path(@merge_request.target_project, @merge_request, format: :diff), class: "vlink" + = link_to "download it", namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, format: :diff), class: "vlink" instead. diff --git a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml index 9bf6a9d081c..0a642b7e6d0 100644 --- a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml +++ b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml @@ -4,7 +4,7 @@ - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && @merge_request.merged? .remove_source_branch_widget %p Changes merged into #{@merge_request.target_branch}. You can remove source branch now - = link_to project_branch_path(@merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-small remove_source_branch" do + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-small remove_source_branch" do %i.fa.fa-times Remove Source Branch diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index f7e5ee5e20e..c414acb6a11 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -11,7 +11,7 @@ %small.pull-right.cgray - if snippet_title.project_id? - = link_to snippet_title.project.name_with_namespace, project_path(snippet_title.project) + = link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project) .snippet-info = "##{snippet_title.id}" diff --git a/app/views/snippets/_snippet.html.haml b/app/views/snippets/_snippet.html.haml index c584dd8dfb6..5bb28664349 100644 --- a/app/views/snippets/_snippet.html.haml +++ b/app/views/snippets/_snippet.html.haml @@ -11,7 +11,7 @@ %small.pull-right.cgray - if snippet.project_id? - = link_to snippet.project.name_with_namespace, project_path(snippet.project) + = link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project) .snippet-info = "##{snippet.id}" -- GitLab From 906f8efd29ad7d4abb95e8e3507d5a6aa700d653 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 20:45:21 -0800 Subject: [PATCH 0979/1609] Refactor commits css --- app/assets/stylesheets/sections/commit.scss | 131 ++++++++ app/assets/stylesheets/sections/commits.scss | 302 +++++-------------- app/views/projects/commits/_commit.html.haml | 2 +- 3 files changed, 212 insertions(+), 223 deletions(-) create mode 100644 app/assets/stylesheets/sections/commit.scss diff --git a/app/assets/stylesheets/sections/commit.scss b/app/assets/stylesheets/sections/commit.scss new file mode 100644 index 00000000000..0e2d9571a45 --- /dev/null +++ b/app/assets/stylesheets/sections/commit.scss @@ -0,0 +1,131 @@ +.commit-title{ + display: block; +} + +.commit-title{ + margin-bottom: 10px; +} + +.commit-author, .commit-committer{ + display: block; + color: #999; + font-weight: normal; + font-style: italic; +} + +.commit-author strong, .commit-committer strong{ + font-weight: bold; + font-style: normal; +} + +.commit-description { + background: none; + border: none; + margin: 0; + padding: 0; + margin-top: 10px; +} + +.commit-stat-summary { + color: #666; + font-size: 14px; + font-weight: normal; + padding: 10px 0; +} + +.commit-info-row { + margin-bottom: 10px; + .avatar { + @extend .avatar-inline; + } + .commit-committer-link, + .commit-author-link { + color: #444; + font-weight: bold; + } +} + +.commit-committer-link, +.commit-author-link { + font-size: 13px; + color: #555; + &:hover { + color: #999; + } +} + +.commit-box { + margin: 10px 0; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 20px 0; + + .commit-title { + margin: 0; + } + + .commit-description { + margin-top: 15px; + } +} + +.file-stats a { + color: $style_color; +} + +.file-stats { + .new-file { + a { + color: #090; + } + i { + color: #1BCF00; + } + } + .renamed-file { + i { + color: #FE9300; + } + } + .deleted-file { + a { + color: #B00; + } + i { + color: #EE0000; + } + } + .edit-file{ + i{ + color: #555; + } + } +} + +/* + * Commit message textarea for web editor and + * custom merge request message + */ +.commit-message-container { + background-color: $body-bg; + position: relative; + font-family: $monospace_font; + $left: 12px; + .max-width-marker { + width: 72ch; + color: rgba(0, 0, 0, 0.0); + font-family: inherit; + left: $left; + height: 100%; + border-right: 1px solid mix($input-border, white); + position: absolute; + z-index: 1; + } + > textarea { + background-color: rgba(0, 0, 0, 0.0); + font-family: inherit; + padding-left: $left; + position: relative; + z-index: 2; + } +} diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss index f6723eb308f..fa5a3b09693 100644 --- a/app/assets/stylesheets/sections/commits.scss +++ b/app/assets/stylesheets/sections/commits.scss @@ -1,77 +1,3 @@ -/** - * Commit file - */ -.commit-committer-link, -.commit-author-link { - font-size: 13px; - color: #555; - &:hover { - color: #999; - } -} - -/** COMMIT BLOCK **/ -.commit-title{ - display: block; -} -.commit-title{ - margin-bottom: 10px; -} -.commit-author, .commit-committer{ - display: block; - color: #999; - font-weight: normal; - font-style: italic; -} -.commit-author strong, .commit-committer strong{ - font-weight: bold; - font-style: normal; -} - - -.file-stats a { - color: $style_color; -} - -.file-stats { - .new-file { - a { - color: #090; - } - i { - color: #1BCF00; - } - } - .renamed-file { - i { - color: #FE9300; - } - } - .deleted-file { - a { - color: #B00; - } - i { - color: #EE0000; - } - } - .edit-file{ - i{ - color: #555; - } - } -} - -.label_commit { - @include border-radius(4px); - padding: 2px 4px; - font-size: 13px; - background: #474D57; - color: #fff; - font-family: $monospace_font; -} - - .commits-compare-switch{ background: image-url("switch_icon.png") no-repeat center center; width: 32px; @@ -85,136 +11,104 @@ background-color: #EEE; } -.commit-description { - background: none; - border: none; - margin: 0; - padding: 0; - margin-top: 10px; -} - -.commit-box { - margin: 10px 0; - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - padding: 20px 0; - - .commit-title { - margin: 0; - } - - .commit-description { - margin-top: 15px; - } -} - - -.commit-stat-summary { - color: #666; - font-size: 14px; - font-weight: normal; - padding: 10px 0; -} - -.commit-info-row { - margin-bottom: 10px; - .avatar { - @extend .avatar-inline; - } - .commit-committer-link, - .commit-author-link { - color: #444; - font-weight: bold; - } -} .lists-separator { margin: 10px 0; border-top: 1px dashed #CCC; } -/** - * COMMIT ROW - */ -ul li.commit { - padding: 8px 0; - - .commit-row-title { - font-size: $list-font-size; - line-height: 20px; - margin-bottom: 5px; - - .notes_count { - float: right; - margin-right: 10px; - } - - .commit_short_id { - min-width: 65px; - font-family: $monospace_font; - } - - .str-truncated { - max-width: 70%; - } +.commits-row { + ul { + margin: 0; - .commit-row-message { - color: #333; - &:hover { - color: #444; - text-decoration: underline; + li.commit { + padding: 8px 0; + + .commit-row-title { + font-size: $list-font-size; + line-height: 20px; + margin-bottom: 2px; + + .notes_count { + float: right; + margin-right: 10px; + } + + .commit_short_id { + min-width: 65px; + font-family: $monospace_font; + } + + .str-truncated { + max-width: 70%; + } + + .commit-row-message { + color: #333; + &:hover { + color: #444; + text-decoration: underline; + } + } + + .text-expander { + background: #eee; + color: #555; + padding: 0 5px; + cursor: pointer; + margin-left: 4px; + &:hover { + background-color: #ddd; + } + } } - } - .text-expander { - background: #eee; - color: #555; - padding: 0 5px; - cursor: pointer; - margin-left: 4px; - &:hover { - background-color: #ddd; + .commit-row-description { + font-size: 14px; + border-left: 1px solid #EEE; + padding: 10px 15px; + margin: 5px 0 10px 5px; + background: #f9f9f9; + display: none; + + pre { + border: none; + background: inherit; + padding: 0; + margin: 0; + } } - } - } - .commit-row-description { - font-size: 14px; - border-left: 1px solid #EEE; - padding: 10px 15px; - margin: 5px 0 10px 5px; - background: #f9f9f9; - display: none; + .commit-row-info { + color: #777; + line-height: 24px; - pre { - border: none; - background: inherit; - padding: 0; - margin: 0; - } - } + a { + color: #777; + } - .commit-row-info { - color: #777; + .committed_ago { + display: inline-block; + } + } - a { - color: #777; - } + &.inline-commit { + .commit-row-title { + font-size: 13px; + } - .committed_ago { - display: inline-block; + .committed_ago { + float: right; + @extend .cgray; + } + } } } - &.inline-commit { - .commit-row-title { - font-size: 13px; - } - - .committed_ago { - float: right; - @extend .cgray; - } + .commits-row-date { + font-size: 15px; + line-height: 20px; + margin-bottom: 5px; } } @@ -224,39 +118,3 @@ ul li.commit { padding: 4px 12px; } } - -.commit-message-container { - background-color: $body-bg; - position: relative; - font-family: $monospace_font; - $left: 12px; - .max-width-marker { - width: 72ch; - color: rgba(0, 0, 0, 0.0); - font-family: inherit; - left: $left; - height: 100%; - border-right: 1px solid mix($input-border, white); - position: absolute; - z-index: 1; - } - > textarea { - background-color: rgba(0, 0, 0, 0.0); - font-family: inherit; - padding-left: $left; - position: relative; - z-index: 2; - } -} - -.commits-row { - ul { - margin: 0; - } - - .commits-row-date { - font-size: 15px; - line-height: 20px; - margin-bottom: 5px; - } -} diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1bf1ada1680..64f528f482e 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -24,7 +24,7 @@ = preserve(gfm(escape_once(commit.description))) .commit-row-info - = commit_author_link(commit, avatar: true, size: 16) + = commit_author_link(commit, avatar: true, size: 24) authored .committed_ago #{time_ago_with_tooltip(commit.committed_date)}   -- GitLab From 692aa78380c4c494ab2367516d68c862f35d7c76 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 21:55:47 -0800 Subject: [PATCH 0980/1609] Improve issue and merge request lists UI --- app/assets/stylesheets/sections/commits.scss | 161 +++++++++--------- app/assets/stylesheets/sections/issues.scss | 1 + .../stylesheets/sections/merge_requests.scss | 1 + app/views/projects/issues/_issue.html.haml | 16 +- .../merge_requests/_merge_request.html.haml | 39 +++-- 5 files changed, 115 insertions(+), 103 deletions(-) diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss index fa5a3b09693..20e6011afb2 100644 --- a/app/assets/stylesheets/sections/commits.scss +++ b/app/assets/stylesheets/sections/commits.scss @@ -23,85 +23,6 @@ li.commit { padding: 8px 0; - - .commit-row-title { - font-size: $list-font-size; - line-height: 20px; - margin-bottom: 2px; - - .notes_count { - float: right; - margin-right: 10px; - } - - .commit_short_id { - min-width: 65px; - font-family: $monospace_font; - } - - .str-truncated { - max-width: 70%; - } - - .commit-row-message { - color: #333; - &:hover { - color: #444; - text-decoration: underline; - } - } - - .text-expander { - background: #eee; - color: #555; - padding: 0 5px; - cursor: pointer; - margin-left: 4px; - &:hover { - background-color: #ddd; - } - } - } - - .commit-row-description { - font-size: 14px; - border-left: 1px solid #EEE; - padding: 10px 15px; - margin: 5px 0 10px 5px; - background: #f9f9f9; - display: none; - - pre { - border: none; - background: inherit; - padding: 0; - margin: 0; - } - } - - .commit-row-info { - color: #777; - line-height: 24px; - - a { - color: #777; - } - - .committed_ago { - display: inline-block; - } - } - - &.inline-commit { - .commit-row-title { - font-size: 13px; - } - - .committed_ago { - float: right; - @extend .cgray; - } - } } } @@ -114,7 +35,89 @@ .commits-feed-holder { float: right; + .btn { padding: 4px 12px; } } + +li.commit { + .commit-row-title { + font-size: $list-font-size; + line-height: 20px; + margin-bottom: 2px; + + .notes_count { + float: right; + margin-right: 10px; + } + + .commit_short_id { + min-width: 65px; + font-family: $monospace_font; + } + + .str-truncated { + max-width: 70%; + } + + .commit-row-message { + color: #444; + + &:hover { + text-decoration: underline; + } + } + + .text-expander { + background: #eee; + color: #555; + padding: 0 5px; + cursor: pointer; + margin-left: 4px; + &:hover { + background-color: #ddd; + } + } + } + + .commit-row-description { + font-size: 14px; + border-left: 1px solid #EEE; + padding: 10px 15px; + margin: 5px 0 10px 5px; + background: #f9f9f9; + display: none; + + pre { + border: none; + background: inherit; + padding: 0; + margin: 0; + } + } + + .commit-row-info { + color: #777; + line-height: 24px; + + a { + color: #777; + } + + .committed_ago { + display: inline-block; + } + } + + &.inline-commit { + .commit-row-title { + font-size: 13px; + } + + .committed_ago { + float: right; + @extend .cgray; + } + } +} diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index ccfc9b704a6..356e8864389 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -6,6 +6,7 @@ .issue-title { margin-bottom: 5px; font-size: $list-font-size; + font-weight: bold; } .issue-info { diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index a3eabb5e330..0d2d8b0173e 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -91,6 +91,7 @@ .merge-request-title { margin-bottom: 5px; font-size: $list-font-size; + font-weight: bold; } .merge-request-info { diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index dc6510be858..240fcc2b527 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -6,9 +6,15 @@ .issue-title %span.str-truncated = link_to_gfm issue.title, project_issue_path(issue.project, issue), class: "row_title" - - if issue.closed? - %small.pull-right - CLOSED + .pull-right + - if issue.closed? + %span + CLOSED + - if issue.notes.any? +   + %span + %i.fa.fa-comments + = issue.notes.count .issue-info %span.light= "##{issue.iid}" @@ -16,10 +22,6 @@ assigned to #{link_to_member(@project, issue.assignee)} - if issue.votes_count > 0 = render 'votes/votes_inline', votable: issue - - if issue.notes.any? - %span - %i.fa.fa-comments - = issue.notes.count - if issue.milestone %span %i.fa.fa-clock-o diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 1686ca0e876..be09f3a938d 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,18 +1,26 @@ %li{ class: mr_css_classes(merge_request) } .merge-request-title - = link_to_gfm truncate(merge_request.title, length: 80), project_merge_request_path(merge_request.target_project, merge_request), class: "row_title" - - if merge_request.merged? - %small.pull-right - %i.fa.fa-check - MERGED - - else - %span.pull-right.hidden-xs - - if merge_request.for_fork? - %span.light - #{merge_request.source_project_namespace}: - = truncate merge_request.source_branch, length: 25 - %i.fa.fa-angle-right.light - = merge_request.target_branch + %span.str-truncated + = link_to_gfm merge_request.title, project_merge_request_path(merge_request.target_project, merge_request), class: "row_title" + .pull-right + - if merge_request.merged? + %span + %i.fa.fa-check + MERGED + - elsif merge_request.closed? + %span + %i.fa.fa-close + CLOSED + - else + %span.hidden-xs.hidden-sm + %span.label-branch< + %i.fa.fa-code-fork + %span= merge_request.target_branch + - if merge_request.notes.any? +   + %span + %i.fa.fa-comments + = merge_request.mr_and_commit_notes.count .merge-request-info %span.light= "##{merge_request.iid}" - if merge_request.assignee @@ -21,10 +29,6 @@ Unassigned - if merge_request.votes_count > 0 = render 'votes/votes_inline', votable: merge_request - - if merge_request.notes.any? - %span - %i.fa.fa-comments - = merge_request.mr_and_commit_notes.count - if merge_request.milestone_id? %span %i.fa.fa-clock-o @@ -33,6 +37,7 @@ %span.task-status = merge_request.task_status + .pull-right.hidden-xs %small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')} -- GitLab From 56af5f5cf9b10246af62c4dc7064fffa516709db Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 22:42:54 -0800 Subject: [PATCH 0981/1609] Improve commits page UI --- app/assets/stylesheets/sections/commits.scss | 2 +- app/views/projects/commits/_commit.html.haml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss index 20e6011afb2..683aca73593 100644 --- a/app/assets/stylesheets/sections/commits.scss +++ b/app/assets/stylesheets/sections/commits.scss @@ -14,7 +14,7 @@ .lists-separator { margin: 10px 0; - border-top: 1px dashed #CCC; + border-color: #DDD; } .commits-row { diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 64f528f482e..5774a48d7b8 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -5,7 +5,8 @@ - if commit.description? %a.text-expander.js-toggle-button ... - = link_to_browse_code(project, commit) + .pull-right + = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" .notes_count - if @note_counts @@ -28,5 +29,4 @@ authored .committed_ago #{time_ago_with_tooltip(commit.committed_date)}   - .pull-right - = link_to commit.short_id, project_commit_path(project, commit), class: "commit_short_id" + = link_to_browse_code(project, commit) -- GitLab From a6220d0a9fc80da028aa39a23cce30fe4fd3b685 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 22:49:49 -0800 Subject: [PATCH 0982/1609] Update CHANGELONG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 575afcbbb6a..002c69ea300 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ v 7.9.0 (unreleased) - Move labels/milestones tabs to sidebar + - Improve UI for commits, issues and merge request lists v 7.8.0 (unreleased) - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) -- GitLab From 74ccfa8f7979a297f547be70a2e965d5336aec75 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 23:05:40 -0800 Subject: [PATCH 0983/1609] Remove overflow-y style that cause overflow-x strange behaviour on mac --- app/assets/stylesheets/sections/nav_sidebar.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss index 3ef2a578b7f..5cf82a17663 100644 --- a/app/assets/stylesheets/sections/nav_sidebar.scss +++ b/app/assets/stylesheets/sections/nav_sidebar.scss @@ -12,7 +12,6 @@ .sidebar-wrapper { z-index: 99; - overflow-y: auto; background: #F5F5F5; } -- GitLab From abc65bbbf3bbabeb1f03b3e55dda32732624cfde Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Feb 2015 23:18:21 -0800 Subject: [PATCH 0984/1609] Improve sidebar navigation UI for mobile devices --- app/assets/stylesheets/sections/nav_sidebar.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss index 5cf82a17663..17923ca499b 100644 --- a/app/assets/stylesheets/sections/nav_sidebar.scss +++ b/app/assets/stylesheets/sections/nav_sidebar.scss @@ -40,7 +40,7 @@ .nav-sidebar li { &.active a { color: #333; - background: #FFF; + background: #FFF !important; font-weight: bold; border: 1px solid #EEE; border-right: 1px solid transparent; @@ -77,7 +77,7 @@ &:hover { text-decoration: none; color: #333; - background: #DDD; + background: #EEE; } &:active, &:focus { @@ -125,7 +125,6 @@ .sidebar-wrapper { width: 52px; - overflow-x: hidden; .nav-sidebar { margin-top: 20px; @@ -139,6 +138,7 @@ padding: 8px 15px; text-align: center; + & > span { display: none; } -- GitLab From 50df9a7cb91cdaafe569f473dd24d15ff04312c6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Feb 2015 00:11:25 -0800 Subject: [PATCH 0985/1609] Minor css improvements * lighter color for comments count * better UI for issue assigee.milestone block --- app/assets/stylesheets/sections/issuable.scss | 16 +++++++++ .../stylesheets/sections/note_form.scss | 2 +- app/views/projects/commits/_commit.html.haml | 5 +-- .../projects/issues/_discussion.html.haml | 6 ++-- app/views/projects/issues/_issue.html.haml | 2 +- .../projects/issues/_issue_context.html.haml | 20 +++++++---- .../merge_requests/_discussion.html.haml | 3 +- .../merge_requests/_merge_request.html.haml | 2 +- .../merge_requests/show/_context.html.haml | 33 +++++++++++-------- .../show/_participants.html.haml | 2 +- 10 files changed, 59 insertions(+), 32 deletions(-) diff --git a/app/assets/stylesheets/sections/issuable.scss b/app/assets/stylesheets/sections/issuable.scss index 75bd39853bd..d8d12338859 100644 --- a/app/assets/stylesheets/sections/issuable.scss +++ b/app/assets/stylesheets/sections/issuable.scss @@ -23,3 +23,19 @@ } } } + +.issuable-context-title { + font-size: 15px; + line-height: 1.4; + margin-bottom: 5px; + + .avatar { + margin-left: 0; + } + + label { + color: #666; + font-weight: normal; + margin-right: 4px; + } +} diff --git a/app/assets/stylesheets/sections/note_form.scss b/app/assets/stylesheets/sections/note_form.scss index 61a877a5e43..a0522030785 100644 --- a/app/assets/stylesheets/sections/note_form.scss +++ b/app/assets/stylesheets/sections/note_form.scss @@ -169,7 +169,7 @@ color: #999; background: #FFF; padding: 5px; - margin-top: -7px; + margin-top: -11px; border: 1px solid #DDD; font-size: 13px; } diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 5774a48d7b8..e4a22db06d4 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -16,8 +16,9 @@ - note_count = notes.count - if note_count > 0 - %span.label.label-gray - %i.fa.fa-comment= note_count + %span.light + %i.fa.fa-comments + = note_count - if commit.description? .commit-row-description.js-toggle-content diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 3a278058944..89572c9a735 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -7,8 +7,7 @@ .row .col-md-9 .participants - %cite.cgray - = pluralize(@issue.participants.count, 'participant') + %span= pluralize(@issue.participants.count, 'participant') - @issue.participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) @@ -20,8 +19,7 @@ = cross_project_reference(@project, @issue) %hr .context - %cite.cgray - = render partial: 'issue_context', locals: { issue: @issue } + = render partial: 'issue_context', locals: { issue: @issue } %hr .clearfix .votes-holder diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 240fcc2b527..225e85515b2 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -6,7 +6,7 @@ .issue-title %span.str-truncated = link_to_gfm issue.title, project_issue_path(issue.project, issue), class: "row_title" - .pull-right + .pull-right.light - if issue.closed? %span CLOSED diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 3daa18ba346..1ea1c83b135 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -1,19 +1,25 @@ = form_for [@project, @issue], remote: true, html: {class: 'edit-issue inline-update'} do |f| %div.prepend-top-20 - %p - Assignee: + .issuable-context-title + %label + Assignee: - if issue.assignee - = link_to_member(@project, @issue.assignee) + %strong= link_to_member(@project, @issue.assignee, size: 24) - else none - if can?(current_user, :modify_issue, @issue) = project_users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id) - %div.prepend-top-20 - %p - Milestone: + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Milestone: - if issue.milestone - #{link_to @issue.milestone.title, project_milestone_path(@project, @issue.milestone)} + %span.back-to-milestone + = link_to project_milestone_path(@project, @issue.milestone) do + %strong + %i.fa.fa-clock-o + = @issue.milestone.title - else none - if can?(current_user, :modify_issue, @issue) diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 51e65f874c2..ca4ce26c676 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -16,8 +16,7 @@ = cross_project_reference(@project, @merge_request) %hr .context - %cite.cgray - = render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request } + = render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request } %hr .votes-holder %h6 Votes diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index be09f3a938d..1c13e8cf31f 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -2,7 +2,7 @@ .merge-request-title %span.str-truncated = link_to_gfm merge_request.title, project_merge_request_path(merge_request.target_project, merge_request), class: "row_title" - .pull-right + .pull-right.light - if merge_request.merged? %span %i.fa.fa-check diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index 21718ca2acf..e9e00b756d5 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -1,23 +1,30 @@ = form_for [@project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update'} do |f| %div.prepend-top-20 - %p - Assignee: + .issuable-context-title + %label + Assignee: - if @merge_request.assignee - = link_to_member(@project, @merge_request.assignee) + %strong= link_to_member(@project, @merge_request.assignee, size: 24) - else none - - if can?(current_user, :modify_merge_request, @merge_request) - = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id) + .issuable-context-selectbox + - if can?(current_user, :modify_merge_request, @merge_request) + = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id) - %div.prepend-top-20 - %p - Milestone: + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Milestone: - if @merge_request.milestone %span.back-to-milestone - #{link_to @merge_request.milestone.title, project_milestone_path(@project, @merge_request.milestone)} + = link_to project_milestone_path(@project, @merge_request.milestone) do + %strong + %i.fa.fa-clock-o + = @merge_request.milestone.title - else none - - if can?(current_user, :modify_merge_request, @merge_request) - = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) - = hidden_field_tag :merge_request_context - = f.submit class: 'btn' + .issuable-context-selectbox + - if can?(current_user, :modify_merge_request, @merge_request) + = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) + = hidden_field_tag :merge_request_context + = f.submit class: 'btn' diff --git a/app/views/projects/merge_requests/show/_participants.html.haml b/app/views/projects/merge_requests/show/_participants.html.haml index 15a97404cb0..4f34af1737d 100644 --- a/app/views/projects/merge_requests/show/_participants.html.haml +++ b/app/views/projects/merge_requests/show/_participants.html.haml @@ -1,4 +1,4 @@ .participants - %cite.cgray #{@merge_request.participants.count} participants + %span #{@merge_request.participants.count} participants - @merge_request.participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) -- GitLab From 675f59540687d40357182df2582c92d8c2bedb49 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Feb 2015 00:20:17 -0800 Subject: [PATCH 0986/1609] Bigger and bold title for issue/mr show pages --- app/assets/stylesheets/sections/issues.scss | 4 ++-- app/views/projects/issues/show.html.haml | 2 +- app/views/projects/merge_requests/show/_mr_box.html.haml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index 356e8864389..b909725bff5 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -171,9 +171,9 @@ form.edit-issue { } } -h3.issue-title { +h2.issue-title { margin-top: 0; - font-size: 2em; + font-weight: bold; } .context .select2-container { diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index bf343cbb7af..2fa58c0e0b2 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -26,7 +26,7 @@ Edit %hr - %h3.issue-title + %h2.issue-title = gfm escape_once(@issue.title) %div - if @issue.description.present? diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index ab1284547ad..ada9ae58b8f 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -1,4 +1,4 @@ -%h3.issue-title +%h2.issue-title = gfm escape_once(@merge_request.title) %div -- GitLab From 0632e85c82eeb76c9b61e497655c9cf2ef5dc262 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 10:23:34 +0100 Subject: [PATCH 0987/1609] Fix commit comments on first line of diff not rendering in Merge Request Discussion view. --- CHANGELOG | 1 + app/models/note.rb | 18 +++++++++--------- lib/gitlab/diff/parser.rb | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 002c69ea300..0d2c7724899 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ v 7.9.0 (unreleased) - Move labels/milestones tabs to sidebar - Improve UI for commits, issues and merge request lists + - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. v 7.8.0 (unreleased) - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) diff --git a/app/models/note.rb b/app/models/note.rb index ccd9783e7d4..e6c258ffbe9 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -409,19 +409,19 @@ class Note < ActiveRecord::Base prev_lines = [] diff_lines.each do |line| - if generate_line_code(line) != self.line_code - if line.type == "match" - prev_lines.clear - prev_match_line = line - else - prev_lines.push(line) - prev_lines.shift if prev_lines.length >= max_number_of_lines - end + if line.type == "match" + prev_lines.clear + prev_match_line = line else prev_lines << line - return prev_lines + + break if generate_line_code(line) == self.line_code + + prev_lines.shift if prev_lines.length >= max_number_of_lines end end + + prev_lines end def diff_lines diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 887ed76b36c..c1d9520ddf1 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -27,7 +27,7 @@ module Gitlab line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0 line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0 - next if line_old == 1 && line_new == 1 #top of file + next if line_old <= 1 && line_new <= 1 #top of file lines_obj << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 next -- GitLab From 7ce664c88e8d160176e89311d833837f9813de77 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 20 Feb 2015 10:47:01 +0100 Subject: [PATCH 0988/1609] Update CHANGELOG Add myself to the changelog for the Asana service #8580 --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 002c69ea300..40d19983c31 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -64,6 +64,7 @@ v 7.8.0 (unreleased) - Remove deprecated Group#owner_id from API - Show projects user contributed to on user page. Show stars near project on user page. - Improve database performance for GitLab + - Add Asana service (Jeremy Benoist) v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch -- GitLab From 5493e27d53d157c1d43dca6c29815f4bc3a39c5c Mon Sep 17 00:00:00 2001 From: krolik Date: Fri, 20 Feb 2015 13:00:42 +0200 Subject: [PATCH 0989/1609] Fixed merge request diff page after back browser button is pressed. --- app/assets/javascripts/merge_request.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 757592842eb..b891c1352b5 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -123,7 +123,7 @@ class @MergeRequest loadDiff: (event) -> $.ajax type: 'GET' - url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json"; beforeSend: => this.$('.mr-loading-status .loading').show() complete: => -- GitLab From 51b234cfb6582650f362cf2339d0f8b0b5a10345 Mon Sep 17 00:00:00 2001 From: krolik Date: Fri, 20 Feb 2015 13:03:28 +0200 Subject: [PATCH 0990/1609] Removed exceeding semicolon --- app/assets/javascripts/merge_request.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index b891c1352b5..09a7b4b3109 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -123,7 +123,7 @@ class @MergeRequest loadDiff: (event) -> $.ajax type: 'GET' - url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json"; + url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json" beforeSend: => this.$('.mr-loading-status .loading').show() complete: => -- GitLab From eb210f4a1876f0dbf70b8c3ae855b6a986777421 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 12:22:53 +0100 Subject: [PATCH 0991/1609] Modify nginx config to let /uploads go through to unicorn. --- lib/support/nginx/gitlab | 41 +++++++++++++++++++--------------- lib/support/nginx/gitlab-ssl | 43 ++++++++++++++++++++---------------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index c8b769ace8e..a4f0b973e3c 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -1,5 +1,5 @@ ## GitLab -## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller +## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller, DouweM ## ## Lines starting with two hashes (##) are comments with information. ## Lines starting with one hash (#) are configuration parameters that can be uncommented. @@ -50,31 +50,36 @@ server { access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + # gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + location / { ## Serve static files from defined root folder. ## @gitlab is a named location for the upstream fallback, see below. try_files $uri $uri/index.html $uri.html @gitlab; } + ## We route uploads through GitLab to prevent XSS and enforce access control. + location /uploads/ { + proxy_pass http://gitlab; + } + ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - # gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - proxy_pass http://gitlab; } @@ -84,7 +89,7 @@ server { ## See config/application.rb under "Relative url support" for the list of ## other files that need to be changed for relative url support location ~ ^/(assets)/ { - root /home/git/gitlab/public; + gzip on; gzip_static on; # to serve pre-gzipped version expires max; add_header Cache-Control public; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 19af010a9f7..4c88107ce0e 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -1,5 +1,5 @@ ## GitLab -## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller +## Contributors: randx, yin8086, sashkab, orkoden, axilleas, bbodenmiller, DouweM ## ## Modified from nginx http version ## Modified from http://blog.phusion.nl/2012/04/21/tutorial-setting-up-gitlab-on-debian-6/ @@ -94,6 +94,23 @@ server { ## Individual nginx logs for this GitLab vhost access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; + + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Ssl on; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; location / { ## Serve static files from defined root folder. @@ -101,26 +118,14 @@ server { try_files $uri $uri/index.html $uri.html @gitlab; } + ## We route uploads through GitLab to prevent XSS and enforce access control. + location /uploads/ { + proxy_pass http://gitlab; + } + ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Ssl on; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - proxy_pass http://gitlab; } @@ -130,7 +135,7 @@ server { ## See config/application.rb under "Relative url support" for the list of ## other files that need to be changed for relative url support location ~ ^/(assets)/ { - root /home/git/gitlab/public; + gzip on; gzip_static on; # to serve pre-gzipped version expires max; add_header Cache-Control public; -- GitLab From 4310431ee73fdd6aa3874aaccc0a901252e7f61f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 12:44:07 +0100 Subject: [PATCH 0992/1609] Use modified ActionDispatch::Static to let uploads go through to routes. --- config/initializers/static_files.rb | 13 +++++++++++++ lib/gitlab/middleware/static.rb | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 config/initializers/static_files.rb create mode 100644 lib/gitlab/middleware/static.rb diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb new file mode 100644 index 00000000000..e04c29cee4a --- /dev/null +++ b/config/initializers/static_files.rb @@ -0,0 +1,13 @@ +begin + app = Rails.application + + app.config.middleware.swap( + ActionDispatch::Static, + Gitlab::Middleware::Static, + app.paths["public"].first, + app.config.static_cache_control + ) +rescue + # If ActionDispatch::Static wasn't loaded onto the stack (like in production), + # an exception is raised. +end \ No newline at end of file diff --git a/lib/gitlab/middleware/static.rb b/lib/gitlab/middleware/static.rb new file mode 100644 index 00000000000..b92319c95d4 --- /dev/null +++ b/lib/gitlab/middleware/static.rb @@ -0,0 +1,13 @@ +module Gitlab + module Middleware + class Static < ActionDispatch::Static + UPLOADS_REGEX = /\A\/uploads(\/|\z)/.freeze + + def call(env) + return @app.call(env) if env['PATH_INFO'] =~ UPLOADS_REGEX + + super + end + end + end +end \ No newline at end of file -- GitLab From 00ca490259de684f4240de4f61728b8eaefbb13e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 13:13:48 +0100 Subject: [PATCH 0993/1609] Use controllers to serve uploads, with XSS prevention and access control. --- .../projects/uploads_controller.rb | 19 +++++++++++++++++++ app/controllers/uploads_controller.rb | 17 +++++++++++++++++ config/routes.rb | 12 ++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 app/controllers/projects/uploads_controller.rb create mode 100644 app/controllers/uploads_controller.rb diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb new file mode 100644 index 00000000000..b922b56418a --- /dev/null +++ b/app/controllers/projects/uploads_controller.rb @@ -0,0 +1,19 @@ +class Projects::UploadsController < Projects::ApplicationController + layout "project" + + before_filter :project + + def show + path = File.join(project.path_with_namespace, params[:secret]) + uploader = FileUploader.new('uploads', path) + + uploader.retrieve_from_store!(params[:filename]) + + if uploader.file.exists? + # Right now, these are always images, so we can safely render them inline. + send_file uploader.file.path, disposition: 'inline' + else + not_found! + end + end +end \ No newline at end of file diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb new file mode 100644 index 00000000000..d5877977258 --- /dev/null +++ b/app/controllers/uploads_controller.rb @@ -0,0 +1,17 @@ +class UploadsController < ApplicationController + def show + model = params[:model].camelize.constantize.find(params[:id]) + uploader = model.send(params[:mounted_as]) + + if uploader.file_storage? + if !model.respond_to?(:project) || can?(current_user, :read_project, model.project) + disposition = uploader.image? ? 'inline' : 'attachment' + send_file uploader.file.path, disposition: disposition + else + not_found! + end + else + redirect_to uploader.url + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 65786d83566..0e7f7d893d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -69,7 +69,19 @@ Gitlab::Application.routes.draw do end end + # + # Uploads + # + scope path: :uploads do + # Note attachments and User/Group/Project avatars + get ":model/:mounted_as/:id/:filename", to: "uploads#show", + constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } + + # Project markdown uploads + get ":id/:secret/:filename", to: "projects/uploads#show", + constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } + end # # Explore area -- GitLab From 73d12d6e6db807d6d15a665cddc0ca9a47bff4eb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 13:24:51 +0100 Subject: [PATCH 0994/1609] Update changelog. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 002c69ea300..a9ee816b37d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ v 7.9.0 (unreleased) - Improve UI for commits, issues and merge request lists v 7.8.0 (unreleased) + - Fix access control and protection against XSS for note attachments and other uploads. - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) - Include issue/mr participants in list of recipients for reassign/close/reopen emails -- GitLab From 874640123b9b508fef40d4285a7c28d7e4653dd7 Mon Sep 17 00:00:00 2001 From: Derek Campbell Date: Fri, 20 Feb 2015 09:20:42 -0400 Subject: [PATCH 0995/1609] To close an issue you must set 'state_event' to 'close'. I cannot set 'closed' to '1'. --- doc/api/issues.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/issues.md b/doc/api/issues.md index 5a2f6a4c229..a7dd8b74c35 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -208,7 +208,7 @@ If an error occurs, an error number and a message explaining the reason is retur ## Delete existing issue (**Deprecated**) -The function is deprecated and returns a `405 Method Not Allowed` error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with parameter `closed` set to 1. +The function is deprecated and returns a `405 Method Not Allowed` error if called. An issue gets now closed and is done by calling `PUT /projects/:id/issues/:issue_id` with parameter `state_event` set to `close`. ``` DELETE /projects/:id/issues/:issue_id -- GitLab From e0edea4ae949a006c051768d073737436ba50b2b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 14:35:29 +0100 Subject: [PATCH 0996/1609] Fix commits calendar vertical days. --- app/assets/javascripts/calendar.js.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 70940e13858..19ea4ccc4cf 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -16,11 +16,8 @@ class @calendar subDomain: "day" range: 12 tooltip: true - domainDynamicDimension: false - colLimit: 4 label: position: "top" - domainMargin: 1 legend: [ 0 1 -- GitLab From c801df81fb48272b670b7448e3898a98cdb8b742 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 14:39:35 +0100 Subject: [PATCH 0997/1609] Satisfy Rubocop. --- app/controllers/projects/uploads_controller.rb | 2 +- config/initializers/static_files.rb | 2 +- config/routes.rb | 4 ++-- lib/gitlab/middleware/static.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index b922b56418a..2b4da35bc7f 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -16,4 +16,4 @@ class Projects::UploadsController < Projects::ApplicationController not_found! end end -end \ No newline at end of file +end diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index e04c29cee4a..2a6eaec0cc4 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -10,4 +10,4 @@ begin rescue # If ActionDispatch::Static wasn't loaded onto the stack (like in production), # an exception is raised. -end \ No newline at end of file +end diff --git a/config/routes.rb b/config/routes.rb index 0e7f7d893d4..ca56c2d268e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -76,11 +76,11 @@ Gitlab::Application.routes.draw do scope path: :uploads do # Note attachments and User/Group/Project avatars get ":model/:mounted_as/:id/:filename", to: "uploads#show", - constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } + constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } # Project markdown uploads get ":id/:secret/:filename", to: "projects/uploads#show", - constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } + constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } end # diff --git a/lib/gitlab/middleware/static.rb b/lib/gitlab/middleware/static.rb index b92319c95d4..85ffa8aca68 100644 --- a/lib/gitlab/middleware/static.rb +++ b/lib/gitlab/middleware/static.rb @@ -10,4 +10,4 @@ module Gitlab end end end -end \ No newline at end of file +end -- GitLab From 8830cfaa60806fa637785535b3ca35a8c3b9dcff Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 15:06:06 +0100 Subject: [PATCH 0998/1609] Base new MR title on commit title if there's only one. --- app/services/merge_requests/build_service.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 859c3f56b2b..30e0cbae024 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -16,9 +16,6 @@ module MergeRequests return build_failed(merge_request, nil) end - # Generate suggested MR title based on source branch name - merge_request.title = merge_request.source_branch.titleize.humanize - compare_result = CompareService.new.execute( current_user, merge_request.source_project, @@ -52,6 +49,14 @@ module MergeRequests merge_request.compare_failed = false end + commits = merge_request.compare_commits + merge_request.title = \ + if commits && commits.count == 1 + commits.first.title + else + merge_request.source_branch.titleize.humanize + end + merge_request rescue Gitlab::Satellite::BranchesWithoutParent -- GitLab From 4ef6ffaad3e9b7a29b438722e5e101de78521ec7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 15:19:50 +0100 Subject: [PATCH 0999/1609] Split up AttachmentUploader. --- app/models/group.rb | 2 +- app/models/project.rb | 2 +- app/models/user.rb | 2 +- app/uploaders/attachment_uploader.rb | 10 -------- app/uploaders/avatar_uploader.rb | 32 ++++++++++++++++++++++++ app/views/events/event/_note.html.haml | 6 ++--- app/views/projects/notes/_note.html.haml | 6 ++--- features/steps/groups.rb | 2 +- features/steps/profile/profile.rb | 2 +- features/steps/project/project.rb | 2 +- 10 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 app/uploaders/avatar_uploader.rb diff --git a/app/models/group.rb b/app/models/group.rb index d6ec0be6081..da9621a2a1a 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -23,7 +23,7 @@ class Group < Namespace validate :avatar_type, if: ->(user) { user.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - mount_uploader :avatar, AttachmentUploader + mount_uploader :avatar, AvatarUploader after_create :post_create_hook after_destroy :post_destroy_hook diff --git a/app/models/project.rb b/app/models/project.rb index 56e1aa29040..e2c7f76eb09 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -138,7 +138,7 @@ class Project < ActiveRecord::Base if: ->(project) { project.avatar && project.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - mount_uploader :avatar, AttachmentUploader + mount_uploader :avatar, AvatarUploader # Scopes scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } diff --git a/app/models/user.rb b/app/models/user.rb index 21ccc76978e..a723b1289b6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -177,7 +177,7 @@ class User < ActiveRecord::Base end end - mount_uploader :avatar, AttachmentUploader + mount_uploader :avatar, AvatarUplaoder # Scopes scope :admins, -> { where(admin: true) } diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb index b122b6c8658..a9691bee46e 100644 --- a/app/uploaders/attachment_uploader.rb +++ b/app/uploaders/attachment_uploader.rb @@ -3,8 +3,6 @@ class AttachmentUploader < CarrierWave::Uploader::Base storage :file - after :store, :reset_events_cache - def store_dir "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" end @@ -22,15 +20,7 @@ class AttachmentUploader < CarrierWave::Uploader::Base false end - def secure_url - Gitlab.config.gitlab.relative_url_root + "/files/#{model.class.to_s.underscore}/#{model.id}/#{file.filename}" - end - def file_storage? self.class.storage == CarrierWave::Storage::File end - - def reset_events_cache(file) - model.reset_events_cache if model.is_a?(User) - end end diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb new file mode 100644 index 00000000000..7cad044555b --- /dev/null +++ b/app/uploaders/avatar_uploader.rb @@ -0,0 +1,32 @@ +# encoding: utf-8 + +class AvatarUploader < CarrierWave::Uploader::Base + storage :file + + after :store, :reset_events_cache + + def store_dir + "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + end + + def image? + img_ext = %w(png jpg jpeg gif bmp tiff) + if file.respond_to?(:extension) + img_ext.include?(file.extension.downcase) + else + # Not all CarrierWave storages respond to :extension + ext = file.path.split('.').last.downcase + img_ext.include?(ext) + end + rescue + false + end + + def file_storage? + self.class.storage == CarrierWave::Storage::File + end + + def reset_events_cache(file) + model.reset_events_cache if model.is_a?(User) + end +end diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index 0acb8538778..4ef18c09060 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -18,9 +18,9 @@ - note = event.target - if note.attachment.url - if note.attachment.image? - = link_to note.attachment.secure_url, target: '_blank' do - = image_tag note.attachment.secure_url, class: 'note-image-attach' + = link_to note.attachment.url, target: '_blank' do + = image_tag note.attachment.url, class: 'note-image-attach' - else - = link_to note.attachment.secure_url, target: "_blank", class: 'note-file-attach' do + = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do %i.fa.fa-paperclip = note.attachment_identifier diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 88c7b7ccf1a..cfeba00d271 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -57,10 +57,10 @@ - if note.attachment.url .note-attachment - if note.attachment.image? - = link_to note.attachment.secure_url, target: '_blank' do - = image_tag note.attachment.secure_url, class: 'note-image-attach' + = link_to note.attachment.url, target: '_blank' do + = image_tag note.attachment.url, class: 'note-image-attach' .attachment - = link_to note.attachment.secure_url, target: "_blank" do + = link_to note.attachment.url, target: "_blank" do %i.fa.fa-paperclip = note.attachment_identifier = link_to delete_attachment_project_note_path(@project, note), diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 610e7fd3a48..0a9b4ccba53 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -110,7 +110,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see new group "Owned" avatar' do - Group.find_by(name: "Owned").avatar.should be_instance_of AttachmentUploader + Group.find_by(name: "Owned").avatar.should be_instance_of AvatarUploader Group.find_by(name: "Owned").avatar.url.should == "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/gitlab_logo.png" end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index a907b0b7dcf..4efd2176782 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -29,7 +29,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps end step 'I should see new avatar' do - @user.avatar.should be_instance_of AttachmentUploader + @user.avatar.should be_instance_of AvatarUploader @user.avatar.url.should == "/uploads/user/avatar/#{ @user.id }/gitlab_logo.png" end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 033d45e0253..d39c8e7d2db 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -35,7 +35,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see new project avatar' do - @project.avatar.should be_instance_of AttachmentUploader + @project.avatar.should be_instance_of AvatarUploader url = @project.avatar.url url.should == "/uploads/project/avatar/#{ @project.id }/gitlab_logo.png" end -- GitLab From 7f1adc3d9cdc5c3f1c0fcbf6c72d89b8ee062af5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 15:56:12 +0100 Subject: [PATCH 1000/1609] Fix URL to uploaded file. --- app/controllers/projects/uploads_controller.rb | 2 +- app/services/projects/upload_service.rb | 3 +-- app/uploaders/file_uploader.rb | 4 ++++ config/routes.rb | 6 +++--- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 53b92d8643d..9020e86c44e 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -4,7 +4,7 @@ class Projects::UploadsController < Projects::ApplicationController before_filter :project def create - link_to_file = ::Projects::UploadService.new(repository, params[:file]). + link_to_file = ::Projects::UploadService.new(project, params[:file]). execute respond_to do |format| diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb index b2466b52ad9..a186c97628f 100644 --- a/app/services/projects/upload_service.rb +++ b/app/services/projects/upload_service.rb @@ -1,6 +1,5 @@ module Projects class UploadService < BaseService - include Rails.application.routes.url_helpers def initialize(project, file) @project, @file = project, file end @@ -15,7 +14,7 @@ module Projects { 'alt' => filename, - 'url' => project_upload_url(@project, secret: uploader.secret, filename: uploader.file.filename), + 'url' => uploader.secure_url, 'is_image' => uploader.image? } end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 36a28f93c49..f9673abbfe8 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -25,6 +25,10 @@ class FileUploader < CarrierWave::Uploader::Base SecureRandom.hex end + def secure_url + File.join(Gitlab.config.gitlab.url, @project.path_with_namespace, "uploads", @secret, file.filename) + end + def file_storage? self.class.storage == CarrierWave::Storage::File end diff --git a/config/routes.rb b/config/routes.rb index 498716b12e0..b6f58acf1a6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,8 +79,8 @@ Gitlab::Application.routes.draw do constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } # Project markdown uploads - get ":id/:secret/:filename", to: "projects/uploads#show", - constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } + get ":project_id/:secret/:filename", to: "projects/uploads#show", + constraints: { project_id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } end # @@ -264,7 +264,7 @@ Gitlab::Application.routes.draw do resources :uploads, only: [:create] do collection do - get ":secret/:filename", action: :show, constraints: { filename: /.+/ } + get ":secret/:filename", action: :show, as: :show, constraints: { filename: /.+/ } end end -- GitLab From 938a1381fc89d39df9c440aad2f95e3b93d80f3b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 14:39:35 +0100 Subject: [PATCH 1001/1609] Satisfy Rubocop. --- app/controllers/projects/uploads_controller.rb | 2 +- config/initializers/static_files.rb | 2 +- config/routes.rb | 10 ++++++---- lib/gitlab/middleware/static.rb | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index b922b56418a..2b4da35bc7f 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -16,4 +16,4 @@ class Projects::UploadsController < Projects::ApplicationController not_found! end end -end \ No newline at end of file +end diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index e04c29cee4a..2a6eaec0cc4 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -10,4 +10,4 @@ begin rescue # If ActionDispatch::Static wasn't loaded onto the stack (like in production), # an exception is raised. -end \ No newline at end of file +end diff --git a/config/routes.rb b/config/routes.rb index 0e7f7d893d4..a2ae2f8da04 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,12 +75,14 @@ Gitlab::Application.routes.draw do scope path: :uploads do # Note attachments and User/Group/Project avatars - get ":model/:mounted_as/:id/:filename", to: "uploads#show", - constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } + get ":model/:mounted_as/:id/:filename", + to: "uploads#show", + constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } # Project markdown uploads - get ":id/:secret/:filename", to: "projects/uploads#show", - constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } + get ":id/:secret/:filename", + to: "projects/uploads#show", + constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } end # diff --git a/lib/gitlab/middleware/static.rb b/lib/gitlab/middleware/static.rb index b92319c95d4..85ffa8aca68 100644 --- a/lib/gitlab/middleware/static.rb +++ b/lib/gitlab/middleware/static.rb @@ -10,4 +10,4 @@ module Gitlab end end end -end \ No newline at end of file +end -- GitLab From 2570e4df79fa09d3c4abc1d0ec82c67a322b249e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 16:55:38 +0100 Subject: [PATCH 1002/1609] Fix specs. --- config/routes.rb | 10 ++++++---- spec/controllers/projects/uploads_controller_spec.rb | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index b6f58acf1a6..3d826bf5599 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,12 +75,14 @@ Gitlab::Application.routes.draw do scope path: :uploads do # Note attachments and User/Group/Project avatars - get ":model/:mounted_as/:id/:filename", to: "uploads#show", - constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } + get ":model/:mounted_as/:id/:filename", + to: "uploads#show", + constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } # Project markdown uploads - get ":project_id/:secret/:filename", to: "projects/uploads#show", - constraints: { project_id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } + get ":project_id/:secret/:filename", + to: "projects/uploads#show", + constraints: { project_id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } end # diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index 8c99b5ca528..28d313b7e98 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -29,7 +29,7 @@ describe Projects::UploadsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"rails_sample\"' - expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads" + expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads" expect(response.body).to match '\"is_image\":true' end end @@ -41,7 +41,7 @@ describe Projects::UploadsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"doc_sample.txt\"' - expect(response.body).to match "\"url\":\"/#{project.path_with_namespace}/uploads" + expect(response.body).to match "\"url\":\"http://localhost/#{project.path_with_namespace}/uploads" expect(response.body).to match '\"is_image\":false' end end -- GitLab From 00408f37e34f37f1299df6957f62bfa7ff341749 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 16:30:15 +0100 Subject: [PATCH 1003/1609] Move 'require_non_empty_project' filter to front so 'assign_ref_vars' doesn't 404. --- app/controllers/projects/blame_controller.rb | 2 +- app/controllers/projects/blob_controller.rb | 2 +- app/controllers/projects/branches_controller.rb | 1 - app/controllers/projects/commit_controller.rb | 2 +- app/controllers/projects/commits_controller.rb | 2 +- app/controllers/projects/compare_controller.rb | 2 +- app/controllers/projects/forks_controller.rb | 2 +- app/controllers/projects/graphs_controller.rb | 2 +- app/controllers/projects/network_controller.rb | 2 +- app/controllers/projects/raw_controller.rb | 2 +- app/controllers/projects/refs_controller.rb | 2 +- app/controllers/projects/repositories_controller.rb | 2 +- app/controllers/projects/tree_controller.rb | 2 +- 13 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 106f21b83e6..489a6ae5666 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -2,9 +2,9 @@ class Projects::BlameController < Projects::ApplicationController include ExtractsPath + before_filter :require_non_empty_project before_filter :assign_ref_vars before_filter :authorize_download_code! - before_filter :require_non_empty_project def show @blob = @repository.blob_at(@commit.id, @path) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index dccb96ba1d1..8071f13173d 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -5,8 +5,8 @@ class Projects::BlobController < Projects::ApplicationController # Raised when given an invalid file path class InvalidPathError < StandardError; end - before_filter :authorize_download_code! before_filter :require_non_empty_project, except: [:new, :create] + before_filter :authorize_download_code! before_filter :authorize_push_code!, only: [:destroy] before_filter :assign_blob_vars before_filter :commit, except: [:new, :create] diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index cff1a907dc2..f7bb36c40bb 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -2,7 +2,6 @@ class Projects::BranchesController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper # Authorize before_filter :require_non_empty_project - before_filter :authorize_download_code! before_filter :authorize_push_code!, only: [:create, :destroy] diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 96a782bdf7a..87e39f1363a 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -3,8 +3,8 @@ # Not to be confused with CommitsController, plural. class Projects::CommitController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! before_filter :require_non_empty_project + before_filter :authorize_download_code! before_filter :commit def show diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index b133afe44b5..4b6ab437476 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -3,9 +3,9 @@ require "base64" class Projects::CommitsController < Projects::ApplicationController include ExtractsPath + before_filter :require_non_empty_project before_filter :assign_ref_vars before_filter :authorize_download_code! - before_filter :require_non_empty_project def show @repo = @project.repository diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index ffb8c2e4af1..8a359042d7b 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -1,7 +1,7 @@ class Projects::CompareController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! before_filter :require_non_empty_project + before_filter :authorize_download_code! def index end diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index a0481d11582..414da0bbdc9 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -1,7 +1,7 @@ class Projects::ForksController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! before_filter :require_non_empty_project + before_filter :authorize_download_code! def new @namespaces = current_user.manageable_namespaces diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 4a318cb7d56..752474b4a4c 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -1,7 +1,7 @@ class Projects::GraphsController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! before_filter :require_non_empty_project + before_filter :authorize_download_code! def show respond_to do |format| diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 59f2a745367..83d1c1dacae 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -2,9 +2,9 @@ class Projects::NetworkController < Projects::ApplicationController include ExtractsPath include ApplicationHelper + before_filter :require_non_empty_project before_filter :assign_ref_vars before_filter :authorize_download_code! - before_filter :require_non_empty_project def show respond_to do |format| diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index c4ddc32e8c3..b1a029ce696 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -2,9 +2,9 @@ class Projects::RawController < Projects::ApplicationController include ExtractsPath + before_filter :require_non_empty_project before_filter :assign_ref_vars before_filter :authorize_download_code! - before_filter :require_non_empty_project def show @blob = @repository.blob_at(@commit.id, @path) diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index b80472f8eb4..0adecded17e 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -1,9 +1,9 @@ class Projects::RefsController < Projects::ApplicationController include ExtractsPath + before_filter :require_non_empty_project before_filter :assign_ref_vars before_filter :authorize_download_code! - before_filter :require_non_empty_project def switch respond_to do |format| diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 3a90c1c806d..320c3965265 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -1,7 +1,7 @@ class Projects::RepositoriesController < Projects::ApplicationController # Authorize - before_filter :authorize_download_code! before_filter :require_non_empty_project, except: :create + before_filter :authorize_download_code! before_filter :authorize_admin_project!, only: :create def create diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 5b52640a4e1..70cd5a62ff5 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -2,9 +2,9 @@ class Projects::TreeController < Projects::ApplicationController include ExtractsPath + before_filter :require_non_empty_project, except: [:new, :create] before_filter :assign_ref_vars before_filter :authorize_download_code! - before_filter :require_non_empty_project, except: [:new, :create] def show if tree.entries.empty? -- GitLab From 157b4b4b1f41267375d3b32c9c1606a538eb8488 Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Fri, 20 Feb 2015 17:38:41 +0000 Subject: [PATCH 1004/1609] Add gitorious.org importer --- .../import/gitorious_controller.rb | 43 ++++++++++++ app/views/import/gitorious/status.html.haml | 41 ++++++++++++ app/views/projects/new.html.haml | 7 ++ config/routes.rb | 6 ++ lib/gitlab/gitorious_import/client.rb | 63 +++++++++++++++++ .../gitorious_import/project_creator.rb | 39 +++++++++++ .../import/gitorious_controller_spec.rb | 67 +++++++++++++++++++ .../gitorious_import/project_creator.rb | 23 +++++++ 8 files changed, 289 insertions(+) create mode 100644 app/controllers/import/gitorious_controller.rb create mode 100644 app/views/import/gitorious/status.html.haml create mode 100644 lib/gitlab/gitorious_import/client.rb create mode 100644 lib/gitlab/gitorious_import/project_creator.rb create mode 100644 spec/controllers/import/gitorious_controller_spec.rb create mode 100644 spec/lib/gitlab/gitorious_import/project_creator.rb diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb new file mode 100644 index 00000000000..627b4a171b8 --- /dev/null +++ b/app/controllers/import/gitorious_controller.rb @@ -0,0 +1,43 @@ +class Import::GitoriousController < Import::BaseController + + def new + redirect_to client.authorize_url(callback_import_gitorious_url) + end + + def callback + session[:gitorious_repos] = params[:repos] + redirect_to status_import_gitorious_url + end + + def status + @repos = client.repos + + @already_added_projects = current_user.created_projects.where(import_type: "gitorious") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.to_a.reject! { |repo| already_added_projects_names.include? repo.full_name } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "gitorious").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @repo_id = params[:repo_id] + repo = client.repo(@repo_id) + @target_namespace = params[:new_namespace].presence || repo.namespace + @project_name = repo.name + + namespace = get_or_create_namespace || (render and return) + + @project = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, current_user).execute + end + + private + + def client + @client ||= Gitlab::GitoriousImport::Client.new(session[:gitorious_repos]) + end + +end diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml new file mode 100644 index 00000000000..35ed0a717de --- /dev/null +++ b/app/views/import/gitorious/status.html.haml @@ -0,0 +1,41 @@ +%h3.page-title + %i.fa.fa-gitorious + Import repositories from Gitorious.org + +%p.light + Select projects you want to import. +%hr +%p + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + +%table.table.import-jobs + %thead + %tr + %th From Gitorious + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td= project.import_source + %td + %strong= link_to project.path_with_namespace, project + %td.job-status + - if project.import_status == 'finished' + %span.cgreen + %i.fa.fa-check + done + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td= repo.full_name + %td.import-target + = repo.full_name + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + +:coffeescript + $ -> + new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}") diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 6f5851d61a1..33162ded4a6 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -66,6 +66,13 @@ Import projects from GitLab.com = render 'gitlab_import_modal' + .project-import.form-group + .col-sm-2 + .col-sm-10 + = link_to new_import_gitorious_path do + %i.fa.fa-heart + Import projects from Gitorious.org + %hr.prepend-botton-10 .form-group diff --git a/config/routes.rb b/config/routes.rb index 65786d83566..101c5f3c362 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,6 +67,12 @@ Gitlab::Application.routes.draw do get :callback get :jobs end + + resource :gitorious, only: [:create, :new], controller: :gitorious do + get :status + get :callback + get :jobs + end end diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb new file mode 100644 index 00000000000..5043f6a2ebd --- /dev/null +++ b/lib/gitlab/gitorious_import/client.rb @@ -0,0 +1,63 @@ +module Gitlab + module GitoriousImport + GITORIOUS_HOST = "https://gitorious.org" + + class Client + attr_reader :repo_list + + def initialize(repo_list) + @repo_list = repo_list + end + + def authorize_url(redirect_uri) + "#{GITORIOUS_HOST}/gitlab-import?callback_url=#{redirect_uri}" + end + + def repos + @repos ||= repo_names.map { |full_name| Repository.new(full_name) } + end + + def repo(id) + repos.find { |repo| repo.id == id } + end + + private + + def repo_names + repo_list.to_s.split(',').map(&:strip).reject(&:blank?) + end + end + + Repository = Struct.new(:full_name) do + def id + Digest::SHA1.hexdigest(full_name) + end + + def namespace + segments.first + end + + def path + segments.last + end + + def name + path.titleize + end + + def description + "" + end + + def import_url + "#{GITORIOUS_HOST}/#{full_name}.git" + end + + private + + def segments + full_name.split('/') + end + end + end +end diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb new file mode 100644 index 00000000000..3cbebe53997 --- /dev/null +++ b/lib/gitlab/gitorious_import/project_creator.rb @@ -0,0 +1,39 @@ +module Gitlab + module GitoriousImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + @project = Project.new( + name: repo.name, + path: repo.path, + description: repo.description, + namespace: namespace, + creator: current_user, + visibility_level: Gitlab::VisibilityLevel::PUBLIC, + import_type: "gitorious", + import_source: repo.full_name, + import_url: repo.import_url + ) + + if @project.save! + @project.reload + + if @project.import_failed? + @project.import_retry + else + @project.import_start + end + end + + @project + end + end + end +end diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb new file mode 100644 index 00000000000..07c9484bf1a --- /dev/null +++ b/spec/controllers/import/gitorious_controller_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe Import::GitoriousController do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe "GET new" do + it "redirects to import endpoint on gitorious.org" do + get :new + + expect(controller).to redirect_to("https://gitorious.org/gitlab-import?callback_url=http://test.host/import/gitorious/callback") + end + end + + describe "GET callback" do + it "stores repo list in session" do + get :callback, repos: 'foo/bar,baz/qux' + + expect(session[:gitorious_repos]).to eq('foo/bar,baz/qux') + end + end + + describe "GET status" do + before do + @repo = OpenStruct.new(full_name: 'asd/vim') + end + + it "assigns variables" do + @project = create(:project, import_type: 'gitorious', creator_id: user.id) + controller.stub_chain(:client, :repos).and_return([@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo]) + end + + it "does not show already added project" do + @project = create(:project, import_type: 'gitorious', creator_id: user.id, import_source: 'asd/vim') + controller.stub_chain(:client, :repos).and_return([@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + end + + describe "POST create" do + before do + @repo = Gitlab::GitoriousImport::Repository.new('asd/vim') + end + + it "takes already existing namespace" do + namespace = create(:namespace, name: "asd", owner: user) + expect(Gitlab::GitoriousImport::ProjectCreator). + to receive(:new).with(@repo, namespace, user). + and_return(double(execute: true)) + controller.stub_chain(:client, :repo).and_return(@repo) + + post :create, format: :js + end + end +end diff --git a/spec/lib/gitlab/gitorious_import/project_creator.rb b/spec/lib/gitlab/gitorious_import/project_creator.rb new file mode 100644 index 00000000000..cf2318bb3a2 --- /dev/null +++ b/spec/lib/gitlab/gitorious_import/project_creator.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::GitoriousImport::ProjectCreator do + let(:user) { create(:user) } + let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') } + let(:namespace){ create(:namespace) } + + it 'creates project' do + allow_any_instance_of(Project).to receive(:add_import_job) + + project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user) + project_creator.execute + project = Project.last + + expect(project.name).to eq("Bar Baz Qux") + expect(project.path).to eq("bar-baz-qux") + expect(project.namespace).to eq(namespace) + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(project.import_type).to eq("gitorious") + expect(project.import_source).to eq("foo/bar-baz-qux") + expect(project.import_url).to eq("https://gitorious.org/foo/bar-baz-qux.git") + end +end -- GitLab From 92434b29cc45677fe72bb6a8a5bd09d5ead8d138 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Feb 2015 10:27:37 -0800 Subject: [PATCH 1005/1609] Extend project web hooks with more data * add git_http_url and git_ssh_url to project web hook * add visibility_level to project web hook * add documentation about project visibility_level in API --- CHANGELOG | 1 + doc/api/projects.md | 18 ++++++++++++++++++ doc/web_hooks/web_hooks.md | 20 ++++++++++++++------ lib/gitlab/push_data_builder.rb | 3 +++ spec/lib/gitlab/push_data_builder_spec.rb | 3 +++ 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 40d19983c31..cb0c86a1527 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -65,6 +65,7 @@ v 7.8.0 (unreleased) - Show projects user contributed to on user page. Show stars near project on user page. - Improve database performance for GitLab - Add Asana service (Jeremy Benoist) + - Improve project web hooks with extra data v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/doc/api/projects.md b/doc/api/projects.md index 454f6fa2e91..a1a23051d7e 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1,5 +1,23 @@ # Projects + +### Project visibility level + +Project in GitLab has be either private, internal or public. +You can determine it by `visibility_level` field in project. + +Constants for project visibility levels are next: + +* Private. `visibility_level` is `0`. + Project access must be granted explicitly for each user. + +* Internal. `visibility_level` is `10`. + The project can be cloned by any logged in user. + +* Public. `visibility_level` is `20`. + The project can be cloned without any authentication. + + ## List projects Get a list of projects accessible by the authenticated user. diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index e3399e5f1b8..29ef5b59bac 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -24,16 +24,19 @@ Triggered when you push to the repository except when pushing tags. "project_id": 15, "repository": { "name": "Diaspora", - "url": "git@example.com:diaspora.git", + "url": "git@example.com:mike/diasporadiaspora.git", "description": "", - "homepage": "http://example.com/diaspora" + "homepage": "http://example.com/mike/diaspora", + "git_http_url":"http://example.com/mike/diaspora.git", + "git_ssh_url":"git@example.com:mike/diaspora.git", + "visibility_level":0 }, "commits": [ { "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", "message": "Update Catalan translation to e38cb41.", "timestamp": "2011-12-12T14:27:31+02:00", - "url": "http://example.com/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", "author": { "name": "Jordi Mallach", "email": "jordi@softcatala.org" @@ -43,7 +46,7 @@ Triggered when you push to the repository except when pushing tags. "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "message": "fixed readme", "timestamp": "2012-01-03T23:36:29+02:00", - "url": "http://example.com/diaspora/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "author": { "name": "GitLab dev user", "email": "gitlabdev@dv6700.(none)" @@ -72,8 +75,13 @@ Triggered when you create (or delete) tags to the repository. "name": "jsmith", "url": "ssh://git@example.com/jsmith/example.git", "description": "", - "homepage": "http://example.com/jsmith/example" - } + "homepage": "http://example.com/jsmith/example", + "git_http_url":"http://example.com/jsmith/example.git", + "git_ssh_url":"git@example.com:jsmith/example.git", + "visibility_level":0 + }, + "commits": [], + "total_commits_count": 0 } ``` diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index faea6ae375c..9aa5c8967a7 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -41,6 +41,9 @@ module Gitlab url: project.url_to_repo, description: project.description, homepage: project.web_url, + git_http_url: project.http_url_to_repo, + git_ssh_url: project.ssh_url_to_repo, + visibility_level: project.visibility_level }, commits: [], total_commits_count: commits_count diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb index da25d45f1ff..1b8ba7b4d43 100644 --- a/spec/lib/gitlab/push_data_builder_spec.rb +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -13,6 +13,9 @@ describe 'Gitlab::PushDataBuilder' do it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { expect(data[:ref]).to eq('refs/heads/master') } it { expect(data[:commits].size).to eq(3) } + it { expect(data[:repository][:git_http_url]).to eq(project.http_url_to_repo) } + it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) } + it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) } it { expect(data[:total_commits_count]).to eq(3) } end -- GitLab From 2f76ccdfac59f7bb6875e0a7753a390d4f6f2b38 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 22:17:38 +0100 Subject: [PATCH 1006/1609] Base new MR description on commit description if there's only one. --- app/services/merge_requests/build_service.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 30e0cbae024..a44b91166e8 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -50,12 +50,13 @@ module MergeRequests end commits = merge_request.compare_commits - merge_request.title = \ - if commits && commits.count == 1 - commits.first.title - else - merge_request.source_branch.titleize.humanize - end + if commits && commits.count == 1 + commit = commits.first + merge_request.title = commit.title + merge_request.description = commit.description.try(:strip) + else + merge_request.title = merge_request.source_branch.titleize.humanize + end merge_request -- GitLab From 0c4d27e82da8f438d35fcb47241c41522e6b0dce Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 22:36:19 +0100 Subject: [PATCH 1007/1609] Point out nginx config changes in update guides. --- doc/update/6.x-or-7.x-to-7.8.md | 8 ++++++-- doc/update/7.7-to-7.8.md | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/doc/update/6.x-or-7.x-to-7.8.md b/doc/update/6.x-or-7.x-to-7.8.md index 90d889d5113..2d11ab1d238 100644 --- a/doc/update/6.x-or-7.x-to-7.8.md +++ b/doc/update/6.x-or-7.x-to-7.8.md @@ -164,8 +164,6 @@ git diff 6-0-stable:config/gitlab.yml.example 7-8-stable:config/gitlab.yml.examp * Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/gitlab.yml.example but with your settings. * Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/unicorn.rb.example but with your settings. * Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.4.3/config.yml.example but with your settings. -* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stablef/lib/support/nginx/gitlab-ssl but with your settings. * Copy rack attack middleware config ```bash @@ -178,6 +176,12 @@ sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab ``` +### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stablef/lib/support/nginx/gitlab-ssl but with your settings. +* Take special note of the `location /uploads/` section that has been added, the directives from `# gzip off;` up to `proxy_set_header X-Frame-Options SAMEORIGIN;` that have been moved from `location @gitlab` to `server`, and the `gzip on;` directive that has been added to `location ~ ^/(assets)/`. + ## 9. Start application sudo service gitlab start diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md index 01b4fc4c992..4196eb8023a 100644 --- a/doc/update/7.7-to-7.8.md +++ b/doc/update/7.7-to-7.8.md @@ -75,8 +75,9 @@ git diff origin/7-6-stable:config/gitlab.yml.example origin/7-8-stable:config/gi #### Change Nginx settings -* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your setting +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings. +* Take special note of the `location /uploads/` section that has been added, the directives from `# gzip off;` up to `proxy_set_header X-Frame-Options SAMEORIGIN;` that have been moved from `location @gitlab` to `server`, and the `gzip on;` directive that has been added to `location ~ ^/(assets)/`. #### Setup time zone (optional) -- GitLab From 08874d2b51e71debac61659050ea577dffd89bf8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 23:27:17 +0100 Subject: [PATCH 1008/1609] Make changes to nginx config less likely to break something. --- doc/update/6.x-or-7.x-to-7.8.md | 2 +- doc/update/7.7-to-7.8.md | 2 +- lib/support/nginx/gitlab | 49 ++++++++++++++++++++----------- lib/support/nginx/gitlab-ssl | 52 +++++++++++++++++++++------------ 4 files changed, 68 insertions(+), 37 deletions(-) diff --git a/doc/update/6.x-or-7.x-to-7.8.md b/doc/update/6.x-or-7.x-to-7.8.md index 2d11ab1d238..859f4c1a6d6 100644 --- a/doc/update/6.x-or-7.x-to-7.8.md +++ b/doc/update/6.x-or-7.x-to-7.8.md @@ -180,7 +180,7 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab * HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab but with your settings. * HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stablef/lib/support/nginx/gitlab-ssl but with your settings. -* Take special note of the `location /uploads/` section that has been added, the directives from `# gzip off;` up to `proxy_set_header X-Frame-Options SAMEORIGIN;` that have been moved from `location @gitlab` to `server`, and the `gzip on;` directive that has been added to `location ~ ^/(assets)/`. +* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. ## 9. Start application diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md index 4196eb8023a..7ca0fe65785 100644 --- a/doc/update/7.7-to-7.8.md +++ b/doc/update/7.7-to-7.8.md @@ -77,7 +77,7 @@ git diff origin/7-6-stable:config/gitlab.yml.example origin/7-8-stable:config/gi * HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings. * HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings. -* Take special note of the `location /uploads/` section that has been added, the directives from `# gzip off;` up to `proxy_set_header X-Frame-Options SAMEORIGIN;` that have been moved from `location @gitlab` to `server`, and the `gzip on;` directive that has been added to `location ~ ^/(assets)/`. +* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. #### Setup time zone (optional) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index a4f0b973e3c..b6889bb7d97 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -50,22 +50,6 @@ server { access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - # gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; - location / { ## Serve static files from defined root folder. ## @gitlab is a named location for the upstream fallback, see below. @@ -74,12 +58,44 @@ server { ## We route uploads through GitLab to prevent XSS and enforce access control. location /uploads/ { + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + # gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + proxy_pass http://gitlab; } ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + # gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + proxy_pass http://gitlab; } @@ -89,7 +105,6 @@ server { ## See config/application.rb under "Relative url support" for the list of ## other files that need to be changed for relative url support location ~ ^/(assets)/ { - gzip on; gzip_static on; # to serve pre-gzipped version expires max; add_header Cache-Control public; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 4c88107ce0e..73885e6c22a 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -94,23 +94,6 @@ server { ## Individual nginx logs for this GitLab vhost access_log /var/log/nginx/gitlab_access.log; error_log /var/log/nginx/gitlab_error.log; - - ## If you use HTTPS make sure you disable gzip compression - ## to be safe against BREACH attack. - gzip off; - - ## https://github.com/gitlabhq/gitlabhq/issues/694 - ## Some requests take more than 30 seconds. - proxy_read_timeout 300; - proxy_connect_timeout 300; - proxy_redirect off; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Ssl on; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Frame-Options SAMEORIGIN; location / { ## Serve static files from defined root folder. @@ -120,12 +103,46 @@ server { ## We route uploads through GitLab to prevent XSS and enforce access control. location /uploads/ { + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Ssl on; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + proxy_pass http://gitlab; } ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { + ## If you use HTTPS make sure you disable gzip compression + ## to be safe against BREACH attack. + gzip off; + + ## https://github.com/gitlabhq/gitlabhq/issues/694 + ## Some requests take more than 30 seconds. + proxy_read_timeout 300; + proxy_connect_timeout 300; + proxy_redirect off; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Ssl on; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frame-Options SAMEORIGIN; + proxy_pass http://gitlab; } @@ -135,7 +152,6 @@ server { ## See config/application.rb under "Relative url support" for the list of ## other files that need to be changed for relative url support location ~ ^/(assets)/ { - gzip on; gzip_static on; # to serve pre-gzipped version expires max; add_header Cache-Control public; -- GitLab From 6945f4a299d9b46b9896e431086277bfedf54b7d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 23:30:06 +0100 Subject: [PATCH 1009/1609] Explain `Gitlab::Middleware::Static`. --- config/initializers/static_files.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index 2a6eaec0cc4..bc4fe14bc1a 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -1,6 +1,11 @@ begin app = Rails.application + # The `ActionDispatch::Static` middleware intercepts requests for static files + # by checking if they exist in the `/public` directory. + # We're replacing it with our `Gitlab::Middleware::Static` that does the same, + # except ignoring `/uploads`, letting those go through to the GitLab Rails app. + app.config.middleware.swap( ActionDispatch::Static, Gitlab::Middleware::Static, -- GitLab From 26d57a648c09f40bd1da3c81a0efe3661288b1af Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 23:32:39 +0100 Subject: [PATCH 1010/1609] Restore nginx config a little more. --- lib/support/nginx/gitlab | 1 + lib/support/nginx/gitlab-ssl | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index b6889bb7d97..62a4276536c 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -105,6 +105,7 @@ server { ## See config/application.rb under "Relative url support" for the list of ## other files that need to be changed for relative url support location ~ ^/(assets)/ { + root /home/git/gitlab/public; gzip_static on; # to serve pre-gzipped version expires max; add_header Cache-Control public; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 73885e6c22a..2aefc944698 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -152,6 +152,7 @@ server { ## See config/application.rb under "Relative url support" for the list of ## other files that need to be changed for relative url support location ~ ^/(assets)/ { + root /home/git/gitlab/public; gzip_static on; # to serve pre-gzipped version expires max; add_header Cache-Control public; -- GitLab From 198d75b3a8516e75c595c5baaa6359c239bc800d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 14:58:42 +0100 Subject: [PATCH 1011/1609] Initialize ZenMode on commit show and milestone edit pages. --- app/assets/javascripts/dispatcher.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 1643ca941ff..ed1bdd6ca33 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -26,7 +26,7 @@ class Dispatcher new ZenMode() when 'projects:milestones:show' new Milestone() - when 'projects:milestones:new' + when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() when 'projects:issues:new','projects:issues:edit' GitLab.GfmAutoComplete.setup() @@ -54,6 +54,7 @@ class Dispatcher when 'projects:commit:show' new Commit() new Diff() + new ZenMode() shortcut_handler = new ShortcutsNavigation() when 'projects:commits:show' shortcut_handler = new ShortcutsNavigation() -- GitLab From 452ba19cdd8e61fa568e2f6462c14b8f31c4c07b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 21 Feb 2015 10:58:11 +0100 Subject: [PATCH 1012/1609] Change check to only swap static middleware when it's enabled. --- config/initializers/static_files.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index bc4fe14bc1a..d9042c652bb 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -1,6 +1,6 @@ -begin - app = Rails.application +app = Rails.application +if app.config.serve_static_assets # The `ActionDispatch::Static` middleware intercepts requests for static files # by checking if they exist in the `/public` directory. # We're replacing it with our `Gitlab::Middleware::Static` that does the same, @@ -12,7 +12,4 @@ begin app.paths["public"].first, app.config.static_cache_control ) -rescue - # If ActionDispatch::Static wasn't loaded onto the stack (like in production), - # an exception is raised. end -- GitLab From 71e146999c405ab301cd3c3e3aa03b89d46c461e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Feb 2015 11:13:24 -0800 Subject: [PATCH 1013/1609] Render gitlab.com import block only if host is not gitlab.com --- app/views/projects/new.html.haml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 33162ded4a6..5216f308110 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -53,18 +53,19 @@ Import projects from GitHub = render 'github_import_modal' - .project-import.form-group - .col-sm-2 - .col-sm-10 - - if gitlab_import_enabled? - = link_to status_import_gitlab_path do - %i.fa.fa-heart - Import projects from GitLab.com - - elsif request.host != 'gitlab.com' - = link_to '#', class: 'how_to_import_link light' do - %i.fa.fa-heart - Import projects from GitLab.com - = render 'gitlab_import_modal' + - unless request.host == 'gitlab.com' + .project-import.form-group + .col-sm-2 + .col-sm-10 + - if gitlab_import_enabled? + = link_to status_import_gitlab_path do + %i.fa.fa-heart + Import projects from GitLab.com + - else + = link_to '#', class: 'how_to_import_link light' do + %i.fa.fa-heart + Import projects from GitLab.com + = render 'gitlab_import_modal' .project-import.form-group .col-sm-2 -- GitLab From 64a7ecc9267f6f82d34b9e87a0271216c94cbbfd Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 21 Feb 2015 13:16:08 -0700 Subject: [PATCH 1014/1609] Update CHANGELOG Move Rails 4.1.9 changes to version 7.9. --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2c6f87666cb..2da06118f3f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ v 7.9.0 (unreleased) - Move labels/milestones tabs to sidebar + - Upgrade Rails gem to version 4.1.9. - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. @@ -59,7 +60,6 @@ v 7.8.0 (unreleased) - Show assignees in merge request index page (Kelvin Mutuma) - Link head panel titles to relevant root page. - Allow users that signed up via OAuth to set their password in order to use Git over HTTP(S). - - Upgrade Rails gem to version 4.1.9. - Show users button to share their newly created public or internal projects on twitter - Add quick help links to the GitLab pricing and feature comparison pages. - Fix duplicate authorized applications in user profile and incorrect application client count in admin area. -- GitLab From 52902f54346acb8076594a85e34f16605790b49f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Feb 2015 18:28:32 -0800 Subject: [PATCH 1015/1609] Improve projects UI a bit --- app/assets/stylesheets/generic/avatar.scss | 8 ++++---- app/assets/stylesheets/sections/dashboard.scss | 1 - app/views/dashboard/_projects_filter.html.haml | 4 ++-- app/views/dashboard/projects.html.haml | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss index 700cc7e6947..8595887c3b9 100644 --- a/app/assets/stylesheets/generic/avatar.scss +++ b/app/assets/stylesheets/generic/avatar.scss @@ -35,8 +35,8 @@ &.s16 { font-size: 12px; line-height: 1.33; } &.s24 { font-size: 14px; line-height: 1.8; } &.s26 { font-size: 20px; line-height: 1.33; } - &.s32 { font-size: 24px; line-height: 1.33; } - &.s60 { font-size: 45px; line-height: 1.33; } - &.s90 { font-size: 68px; line-height: 1.33; } - &.s160 { font-size: 120px; line-height: 1.33; } + &.s32 { font-size: 22px; line-height: 32px; } + &.s60 { font-size: 32px; line-height: 60px; } + &.s90 { font-size: 36px; line-height: 90px; } + &.s160 { font-size: 96px; line-height: 1.33; } } diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index feb9a4ad295..d8fd83d44b7 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -84,7 +84,6 @@ margin-left: 10px; float: left; margin-right: 15px; - font-size: 20px; margin-bottom: 15px; i { diff --git a/app/views/dashboard/_projects_filter.html.haml b/app/views/dashboard/_projects_filter.html.haml index 7b5d46072e3..d87ca861aed 100644 --- a/app/views/dashboard/_projects_filter.html.haml +++ b/app/views/dashboard/_projects_filter.html.haml @@ -1,6 +1,6 @@ .dash-projects-filters.append-bottom-20 - .pull-left.append-right-20 - %ul.nav.nav-pills.nav-compact + .append-right-20 + %ul.nav.nav-tabs = nav_tab :scope, nil do = link_to projects_dashboard_filter_path(scope: nil) do All diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml index 21e44fb1c60..69c64d6c71d 100644 --- a/app/views/dashboard/projects.html.haml +++ b/app/views/dashboard/projects.html.haml @@ -20,7 +20,7 @@ .project-access-icon = visibility_level_icon(project.visibility_level) = link_to project_path(project), class: dom_class(project) do - = project.name_with_namespace + %strong= project.name_with_namespace - if project.forked_from_project   -- GitLab From 87b04868a11a840d04a86ea1f8b2af9ec94efbd8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Feb 2015 22:01:27 -0800 Subject: [PATCH 1016/1609] Create Aside js class for handling all sidebars in UI for mobile devices --- Gemfile | 3 -- Gemfile.lock | 3 -- app/assets/javascripts/application.js.coffee | 4 +- app/assets/javascripts/aside.js.coffee | 17 +++++++ app/assets/javascripts/sidebar.js.coffee | 27 ----------- app/assets/stylesheets/application.scss | 5 -- app/assets/stylesheets/generic/mobile.scss | 20 ++++++++ app/assets/stylesheets/generic/sidebar.scss | 46 ------------------- app/views/dashboard/show.html.haml | 7 ++- app/views/groups/show.html.haml | 4 +- .../projects/issues/_discussion.html.haml | 6 ++- .../merge_requests/_discussion.html.haml | 6 ++- app/views/projects/show.html.haml | 4 +- 13 files changed, 56 insertions(+), 96 deletions(-) create mode 100644 app/assets/javascripts/aside.js.coffee delete mode 100644 app/assets/stylesheets/generic/sidebar.scss diff --git a/Gemfile b/Gemfile index c3d8299e944..233b8c8cd7d 100644 --- a/Gemfile +++ b/Gemfile @@ -176,9 +176,6 @@ gem 'ace-rails-ap' # Keyboard shortcuts gem 'mousetrap-rails' -# Semantic UI Sass for Sidebar -gem 'semantic-ui-sass', '~> 1.8.0' - gem "sass-rails", '~> 4.0.2' gem "coffee-rails" gem "uglifier" diff --git a/Gemfile.lock b/Gemfile.lock index a9784f36ac9..034fd7efc83 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -513,8 +513,6 @@ GEM activesupport (>= 3.1, < 4.2) select2-rails (3.5.2) thor (~> 0.14) - semantic-ui-sass (1.8.0.0) - sass (~> 3.2) settingslogic (2.0.9) shoulda-matchers (2.7.0) activesupport (>= 3.0.0) @@ -740,7 +738,6 @@ DEPENDENCIES sdoc seed-fu select2-rails - semantic-ui-sass (~> 1.8.0) settingslogic shoulda-matchers (~> 2.7.0) sidekiq (~> 3.3) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 9c97582e6dd..e9042b56416 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -32,7 +32,6 @@ #= require nprogress #= require nprogress-turbolinks #= require dropzone -#= require semantic-ui/sidebar #= require mousetrap #= require mousetrap/pause #= require shortcuts @@ -115,7 +114,6 @@ if location.hash window.addEventListener "hashchange", shiftWindow $ -> - # Click a .one_click_select field, select the contents $(".one_click_select").on 'click', -> $(@).select() @@ -183,6 +181,8 @@ $ -> form = btn.closest("form") new ConfirmDangerModal(form, text) + new Aside() + (($) -> # Disable an element and add the 'disabled' Bootstrap class $.fn.extend disable: -> diff --git a/app/assets/javascripts/aside.js.coffee b/app/assets/javascripts/aside.js.coffee new file mode 100644 index 00000000000..85473101944 --- /dev/null +++ b/app/assets/javascripts/aside.js.coffee @@ -0,0 +1,17 @@ +class @Aside + constructor: -> + $(document).off "click", "a.show-aside" + $(document).on "click", 'a.show-aside', (e) -> + e.preventDefault() + btn = $(e.currentTarget) + icon = btn.find('i') + console.log('1') + + if icon.hasClass('fa-angle-left') + btn.parent().find('section').hide() + btn.parent().find('aside').fadeIn() + icon.removeClass('fa-angle-left').addClass('fa-angle-right') + else + btn.parent().find('aside').hide() + btn.parent().find('section').fadeIn() + icon.removeClass('fa-angle-right').addClass('fa-angle-left') diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 5013bcdacd0..7febcba0e94 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -1,30 +1,3 @@ -responsive_resize = -> - current_width = $(window).width() - if current_width < 985 - $('.responsive-side').addClass("ui right wide sidebar") - else - $('.responsive-side').removeClass("ui right wide sidebar") - -$ -> - # Depending on window size, set the sidebar offscreen. - responsive_resize() - - $('.sidebar-expand-button').click -> - $('.ui.sidebar') - .sidebar({overlay: true}) - .sidebar('toggle') - - # Hide sidebar on click outside of sidebar - $(document).mouseup (e) -> - container = $(".ui.sidebar") - container.sidebar "hide" if not container.is(e.target) and container.has(e.target).length is 0 - return - -# On resize, check if sidebar should be offscreen. -$(window).resize -> - responsive_resize() - return - $(document).on("click", '.toggle-nav-collapse', (e) -> e.preventDefault() collapsed = 'page-sidebar-collapsed' diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 8f63a7fee64..e5bb5e21bb0 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -55,8 +55,3 @@ * Styles for JS behaviors. */ @import "behaviors.scss"; - -/** -* Styles for responsive sidebar -*/ -@import "semantic-ui/modules/sidebar"; diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index 54e06661161..2bb69f4aa7e 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -50,4 +50,24 @@ .issue_edited_ago, .note_edited_ago { display: none; } + + aside { + display: none; + } + + .show-aside { + display: block !important; + } +} + +.show-aside { + display: none; + position: fixed; + right: 0px; + top: 30%; + padding: 5px 15px; + background: #EEE; + font-size: 20px; + color: #777; + @include box-shadow(0 1px 2px #DDD); } diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss deleted file mode 100644 index f6311ef74e8..00000000000 --- a/app/assets/stylesheets/generic/sidebar.scss +++ /dev/null @@ -1,46 +0,0 @@ -.ui.sidebar { - z-index: 1000 !important; - background: #fff; - padding: 10px; - width: 285px; -} - -.ui.right.sidebar { - border-left: 1px solid #e1e1e1; - border-right: 0; -} - -.sidebar-expand-button { - cursor: pointer; - transition: all 0.4s; - -moz-transition: all 0.4s; - -webkit-transition: all 0.4s; -} - -.fixed.sidebar-expand-button { - background: #f9f9f9; - color: #555; - padding: 9px 12px 6px 14px; - border: 1px solid #E1E1E1; - border-right: 0; - position: fixed; - top: 108px; - right: 0px; - margin-right: 0; - &:hover { - background: #ddd; - color: #333; - padding-right: 25px; - } -} - -.btn.btn-default.sidebar-expand-button { - margin-left: 12px; - display: inline-block !important; -} - -@media (min-width: 767px) { -.btn.btn-default.sidebar-expand-button { - display: none!important; - } -} diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml index 10951af6a09..f973f4829a0 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/show.html.haml @@ -2,11 +2,10 @@ .dashboard.row %section.activities.col-md-8 = render 'activities' - %aside.side.col-md-4.left.responsive-side + %aside.col-md-4 = render 'sidebar' - - .fixed.sidebar-expand-button.hidden-lg.hidden-md - %i.fa.fa-list.fa-2x + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left - else = render "zero_authorized_projects" diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index d5af859ee62..a453889f744 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -9,7 +9,7 @@ = escaped_autolink(@group.description) %hr .row - %section.activities.col-md-8.hidden-sm.hidden-xs + %section.activities.col-md-8 - if current_user = render "events/event_last_push", event: @last_push = render 'shared/event_filter' @@ -17,3 +17,5 @@ = spinner %aside.side.col-md-4 = render "projects", projects: @projects + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 89572c9a735..15f5208a645 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -5,14 +5,14 @@ - else = link_to 'Close Issue', project_issue_path(@project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" .row - .col-md-9 + %section.col-md-9 .participants %span= pluralize(@issue.participants.count, 'participant') - @issue.participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) .voting_notes#notes= render "projects/notes/notes_with_form" - .col-md-3 + %aside.col-md-3 .issuable-affix .clearfix %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} @@ -33,3 +33,5 @@ - @issue.labels.each do |label| = link_to project_issues_path(@project, label_name: label.name) do = render_colored_label(label) + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index ca4ce26c676..69bbdf49396 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -6,10 +6,10 @@ = link_to 'Reopen', project_merge_request_path(@project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" .row - .col-md-9 + %section.col-md-9 = render "projects/merge_requests/show/participants" = render "projects/notes/notes_with_form" - .col-md-3 + %aside.col-md-3 .issuable-affix .clearfix %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} @@ -29,3 +29,5 @@ - @merge_request.labels.each do |label| = link_to project_merge_requests_path(@project, label_name: label.name) do = render_colored_label(label) + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 435b2648404..c71123c4fbf 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -23,12 +23,14 @@ .tab-content .tab-pane.active#tab-activity .row + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left %section.col-md-9 = render "events/event_last_push", event: @last_push = render 'shared/event_filter' .content_list = spinner - %aside.col-md-3.project-side.hidden-sm.hidden-xs + %aside.col-md-3.project-side .clearfix - if @project.archived? .alert.alert-warning -- GitLab From 50305c363b85c0d55b5a0c7bcd5fcb569a437ec1 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Sun, 22 Feb 2015 10:54:28 +0100 Subject: [PATCH 1017/1609] Update gitlab-shell to 2.5.3 in 7-8 update guide, fixes #8838 --- doc/update/7.7-to-7.8.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md index 7ca0fe65785..c6bf5b227e4 100644 --- a/doc/update/7.7-to-7.8.md +++ b/doc/update/7.7-to-7.8.md @@ -37,7 +37,7 @@ sudo -u git -H git checkout 7-8-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.4.3 +sudo -u git -H git checkout v2.5.3 ``` ### 4. Install libs, migrations, etc. @@ -105,10 +105,10 @@ If all items are green, then congratulations upgrade is complete! If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it only contains a root URL (ex. `https://gitlab.example.com/`) -## Things went south? Revert to previous version (7.6) +## Things went south? Revert to previous version (7.7) ### 1. Revert the code to the previous version -Follow the [upgrade guide from 7.5 to 7.6](7.5-to-7.6.md), except for the database migration +Follow the [upgrade guide from 7.6 to 7.7](7.6-to-7.7.md), except for the database migration (The backup is already migrated to the previous version) ### 2. Restore from the backup: -- GitLab From ebe0d34128c31bb88f6eb5aca96fae012c7fcf8b Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Sun, 22 Feb 2015 10:52:30 -0800 Subject: [PATCH 1018/1609] Remove unreleased for 7.8 in changelog. --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2da06118f3f..6702ba2ba46 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,7 +4,7 @@ v 7.9.0 (unreleased) - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. -v 7.8.0 (unreleased) +v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) -- GitLab From 2bf0a690bfd3985b9f8f0394a8a77d3b9dde44d1 Mon Sep 17 00:00:00 2001 From: Patrik Kernstock Date: Sun, 22 Feb 2015 21:39:13 +0100 Subject: [PATCH 1019/1609] Update 7.7-to-7.8.md --- doc/update/7.7-to-7.8.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md index c6bf5b227e4..a8a5c7f66c6 100644 --- a/doc/update/7.7-to-7.8.md +++ b/doc/update/7.7-to-7.8.md @@ -70,7 +70,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. ``` -git diff origin/7-6-stable:config/gitlab.yml.example origin/7-8-stable:config/gitlab.yml.example +git diff origin/7-7-stable:config/gitlab.yml.example origin/7-8-stable:config/gitlab.yml.example ``` #### Change Nginx settings -- GitLab From 31774a3bb739260622b31001cac778468d7dd92f Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Sun, 22 Feb 2015 12:49:00 -0800 Subject: [PATCH 1020/1609] Ensure that people don't view the changelog on the stable branch because we don't update that one. --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 6702ba2ba46..234a5609e0e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +Please view this file on the master branch, on stable branches it's out of date. + v 7.9.0 (unreleased) - Move labels/milestones tabs to sidebar - Upgrade Rails gem to version 4.1.9. -- GitLab From 5f232b5687b447e7eac40f58c56628da22580de6 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 22 Feb 2015 16:01:49 -0700 Subject: [PATCH 1021/1609] Improve error messages when file editing fails Give more specific errors in API responses and web UI flash messages when a file update fails. --- CHANGELOG | 1 + app/services/base_service.rb | 7 +++-- app/services/files/update_service.rb | 14 ++++++---- lib/api/files.rb | 3 +- .../satellite/files/edit_file_action.rb | 28 +++++++++++++++---- lib/gitlab/satellite/satellite.rb | 4 +++ spec/requests/api/files_spec.rb | 28 ++++++++++++++++--- 7 files changed, 66 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6702ba2ba46..6571f0d1de0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ v 7.9.0 (unreleased) - Move labels/milestones tabs to sidebar - Upgrade Rails gem to version 4.1.9. + - Improve error messages for file edit failures - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. diff --git a/app/services/base_service.rb b/app/services/base_service.rb index bb51795df7c..52ab29f1492 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -37,11 +37,14 @@ class BaseService private - def error(message) - { + def error(message, http_status = nil) + result = { message: message, status: :error } + + result[:http_status] = http_status if http_status + result end def success diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index b4986e1c5c6..bcf0e7f3cee 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -20,17 +20,19 @@ module Files end edit_file_action = Gitlab::Satellite::EditFileAction.new(current_user, project, ref, path) - created_successfully = edit_file_action.commit!( + edit_file_action.commit!( params[:content], params[:commit_message], params[:encoding] ) - if created_successfully - success - else - error("Your changes could not be committed. Maybe the file was changed by another process or there was nothing to commit?") - end + success + rescue Gitlab::Satellite::CheckoutFailed => ex + error("Your changes could not be committed because ref '#{ref}' could not be checked out", 400) + rescue Gitlab::Satellite::CommitFailed => ex + error("Your changes could not be committed. Maybe there was nothing to commit?", 409) + rescue Gitlab::Satellite::PushFailed => ex + error("Your changes could not be committed. Maybe the file was changed by another process?", 409) end end end diff --git a/lib/api/files.rb b/lib/api/files.rb index e6e71bac367..3176ef0e256 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -117,7 +117,8 @@ module API branch_name: branch_name } else - render_api_error!(result[:message], 400) + http_status = result[:http_status] || 400 + render_api_error!(result[:message], http_status) end end diff --git a/lib/gitlab/satellite/files/edit_file_action.rb b/lib/gitlab/satellite/files/edit_file_action.rb index 2834b722b27..82d71ab9906 100644 --- a/lib/gitlab/satellite/files/edit_file_action.rb +++ b/lib/gitlab/satellite/files/edit_file_action.rb @@ -15,7 +15,11 @@ module Gitlab prepare_satellite!(repo) # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") + begin + repo.git.checkout({ raise: true, timeout: true, b: true }, ref, "origin/#{ref}") + rescue Grit::Git::CommandFailed => ex + log_and_raise(CheckoutFailed, ex.message) + end # update the file in the satellite's working dir file_path_in_satellite = File.join(repo.working_dir, file_path) @@ -31,19 +35,31 @@ module Gitlab # commit the changes # will raise CommandFailed when commit fails - repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) + begin + repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) + rescue Grit::Git::CommandFailed => ex + log_and_raise(CommitFailed, ex.message) + end # push commit back to bare repo # will raise CommandFailed when push fails - repo.git.push({ raise: true, timeout: true }, :origin, ref) + begin + repo.git.push({ raise: true, timeout: true }, :origin, ref) + rescue Grit::Git::CommandFailed => ex + log_and_raise(PushFailed, ex.message) + end # everything worked true end - rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false + end + + private + + def log_and_raise(errorClass, message) + Gitlab::GitLogger.error(message) + raise(errorClass, message) end end end diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb index 62d1bb364d3..70125d539da 100644 --- a/lib/gitlab/satellite/satellite.rb +++ b/lib/gitlab/satellite/satellite.rb @@ -1,5 +1,9 @@ module Gitlab module Satellite + class CheckoutFailed < StandardError; end + class CommitFailed < StandardError; end + class PushFailed < StandardError; end + class Satellite include Gitlab::Popen diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index cfac7d289ec..bab8888a631 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -98,13 +98,33 @@ describe API::API, api: true do expect(response.status).to eq(400) end - it "should return a 400 if satellite fails to create file" do - Gitlab::Satellite::EditFileAction.any_instance.stub( - commit!: false, - ) + it 'should return a 400 if the checkout fails' do + Gitlab::Satellite::EditFileAction.any_instance.stub(:commit!) + .and_raise(Gitlab::Satellite::CheckoutFailed) put api("/projects/#{project.id}/repository/files", user), valid_params expect(response.status).to eq(400) + + ref = valid_params[:branch_name] + expect(response.body).to match("ref '#{ref}' could not be checked out") + end + + it 'should return a 409 if the file was not modified' do + Gitlab::Satellite::EditFileAction.any_instance.stub(:commit!) + .and_raise(Gitlab::Satellite::CommitFailed) + + put api("/projects/#{project.id}/repository/files", user), valid_params + expect(response.status).to eq(409) + expect(response.body).to match("Maybe there was nothing to commit?") + end + + it 'should return a 409 if the push fails' do + Gitlab::Satellite::EditFileAction.any_instance.stub(:commit!) + .and_raise(Gitlab::Satellite::PushFailed) + + put api("/projects/#{project.id}/repository/files", user), valid_params + expect(response.status).to eq(409) + expect(response.body).to match("Maybe the file was changed by another process?") end end -- GitLab From b19b8c679a136234094443e2d4a345f136a0bcc1 Mon Sep 17 00:00:00 2001 From: Marco Wessel Date: Mon, 23 Feb 2015 02:31:12 +0100 Subject: [PATCH 1022/1609] Give last_activity_at a default value so it will always be set --- CHANGELOG | 1 + app/models/project.rb | 6 ++++++ .../20150223022001_set_missing_last_activity_at.rb | 9 +++++++++ db/schema.rb | 4 ++-- 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20150223022001_set_missing_last_activity_at.rb diff --git a/CHANGELOG b/CHANGELOG index 6702ba2ba46..5bdcc535409 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ v 7.9.0 (unreleased) - Upgrade Rails gem to version 4.1.9. - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. + - Fix ordering of imported but unchanged projects (Marco Wessel) v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. diff --git a/app/models/project.rb b/app/models/project.rb index 91ab788083d..04189839d6d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -48,6 +48,12 @@ class Project < ActiveRecord::Base default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets + # set last_activity_at to the same as updated_at + before_create :set_last_activity_at + def set_last_activity_at + self.last_activity_at = self.updated_at + end + ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags diff --git a/db/migrate/20150223022001_set_missing_last_activity_at.rb b/db/migrate/20150223022001_set_missing_last_activity_at.rb new file mode 100644 index 00000000000..3a3adf18872 --- /dev/null +++ b/db/migrate/20150223022001_set_missing_last_activity_at.rb @@ -0,0 +1,9 @@ +class SetMissingLastActivityAt < ActiveRecord::Migration + def up + execute "UPDATE projects SET last_activity_at = updated_at WHERE last_activity_at IS NULL" + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index e11a068c9c5..d34eab75085 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150213121042) do +ActiveRecord::Schema.define(version: 20150223022001) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -334,12 +334,12 @@ ActiveRecord::Schema.define(version: 20150213121042) do t.string "import_url" t.integer "visibility_level", default: 0, null: false t.boolean "archived", default: false, null: false - t.string "avatar" t.string "import_status" t.float "repository_size", default: 0.0 t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" + t.string "avatar" end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree -- GitLab From d4bfdd34baa9eea2e89223b039f15cd4d4b3e5ae Mon Sep 17 00:00:00 2001 From: Marco Wessel Date: Mon, 23 Feb 2015 04:03:12 +0100 Subject: [PATCH 1023/1609] use update_column and set to created_at like elsewhere --- app/models/project.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 04189839d6d..967e4de22a9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -48,10 +48,10 @@ class Project < ActiveRecord::Base default_value_for :wall_enabled, false default_value_for :snippets_enabled, gitlab_config_features.snippets - # set last_activity_at to the same as updated_at - before_create :set_last_activity_at + # set last_activity_at to the same as created_at + after_create :set_last_activity_at def set_last_activity_at - self.last_activity_at = self.updated_at + update_column(:last_activity_at, self.created_at) end ActsAsTaggableOn.strict_case_match = true -- GitLab From 19327e6535a69000f1cf89b1f92a3c19fc1d546e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Feb 2015 22:06:43 -0800 Subject: [PATCH 1024/1609] Fix dashboard for projects > 30 --- app/views/dashboard/_projects.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index 252dbf78882..0596738342f 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -20,6 +20,6 @@ %span.light #{@projects_limit} of #{pluralize(@projects_count, 'project')} displayed. .pull-right - = link_to namespace_projects_dashboard_path do + = link_to projects_dashboard_path do Show all %i.fa.fa-angle-right -- GitLab From 9459e9db2470e9c50488811d1d0fcdd025a327d0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Feb 2015 22:26:09 -0800 Subject: [PATCH 1025/1609] Fix updating issue 500 error --- app/controllers/projects/issues_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index d1bf842ec1a..73b58285c61 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -79,7 +79,7 @@ class Projects::IssuesController < Projects::ApplicationController format.js format.html do if @issue.valid? - redirect_to [@project.namespace, @project, @issue] + redirect_to [@project.namespace.becomes(Namespace), @project, @issue] else render :edit end -- GitLab From e23110e6f158608c75e4c661fd57a4bb9c96334a Mon Sep 17 00:00:00 2001 From: shafan Date: Mon, 23 Feb 2015 14:08:46 +0000 Subject: [PATCH 1026/1609] Bump GitLab for Docker to version 7.8.0 --- docker/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ec0923bd4c7..cfb89357a67 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update -q \ # If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.7.2-omnibus.5.4.2.ci-1_amd64.deb \ + wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.8.0-omnibus-1_amd64.deb \ && dpkg -i $TMP_FILE \ && rm -f $TMP_FILE @@ -31,4 +31,4 @@ VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"] ADD gitlab.rb /etc/gitlab/ # Default is to run runit & reconfigure -CMD gitlab-ctl reconfigure & /opt/gitlab/embedded/bin/runsvdir-start +CMD gitlab-ctl reconfigure & /opt/gitlab/embedded/bin/runsvdir-start \ No newline at end of file -- GitLab From 48eeb006f08a1bdea9cd8ad4dc49819a8daccf51 Mon Sep 17 00:00:00 2001 From: Marco Wessel Date: Mon, 23 Feb 2015 15:10:56 +0100 Subject: [PATCH 1027/1609] Correct spelling of 'a project avatar' --- app/views/projects/edit.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8240c186616..b4c36beda88 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -86,7 +86,7 @@ - if @project.avatar? You can change your project avatar here - else - You can upload an project avatar here + You can upload a project avatar here %a.choose-btn.btn.btn-small.js-choose-project-avatar-button %i.icon-paper-clip %span Choose File ... -- GitLab From d723bf78b8f86ee19db47725de8d22e8b6d5d6e2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 23 Feb 2015 10:05:18 -0800 Subject: [PATCH 1028/1609] Fix git-over-http --- lib/gitlab/backend/grack_auth.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 3f207c56631..dc4b945f9d4 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -149,6 +149,7 @@ module Grack path_with_namespace = m.last path_with_namespace.gsub!(/\.wiki$/, '') + path_with_namespace[0] = '' if path_with_namespace.start_with?('/') Project.find_with_namespace(path_with_namespace) end end -- GitLab From 846f83177448832bd04ea0167b34541b5d3b71c6 Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Mon, 23 Feb 2015 10:40:06 -0800 Subject: [PATCH 1029/1609] Fixes grammatical consistency and small changes This commit adds consistency to small things like periods, commas, etc. Also gives additional information to buttons and headers. Fixes #2002, #2005, #2003 --- app/views/profiles/accounts/show.html.haml | 8 ++++---- app/views/profiles/applications.html.haml | 4 +++- app/views/profiles/design.html.haml | 4 ++-- app/views/profiles/emails/index.html.haml | 4 ++-- app/views/profiles/groups/index.html.haml | 4 ++-- app/views/profiles/history.html.haml | 4 ++-- app/views/profiles/keys/index.html.haml | 6 +++--- app/views/profiles/notifications/show.html.haml | 7 +++---- app/views/profiles/passwords/edit.html.haml | 4 ++-- app/views/profiles/show.html.haml | 4 ++-- 10 files changed, 25 insertions(+), 24 deletions(-) diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 53a50f6796b..f124637c07b 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,5 +1,5 @@ %h3.page-title - Account settings + Account Settings %p.light You can change your username and private token here. - if current_user.ldap_user? @@ -10,7 +10,7 @@ .account-page %fieldset.update-token %legend - Private token + Reset Private token %div = form_for @user, url: reset_private_token_profile_path, method: :put do |f| .data @@ -25,7 +25,7 @@ - if current_user.private_token = text_field_tag "token", current_user.private_token, class: "form-control" %div - = f.submit 'Reset', data: { confirm: "Are you sure?" }, class: "btn btn-primary btn-build-token" + = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-primary btn-build-token" - else %span You don`t have one yet. Click generate to fix it. = f.submit 'Generate', class: "btn success btn-build-token" @@ -43,7 +43,7 @@ - if show_profile_username_tab? %fieldset.update-username %legend - Username + Change Username = form_for @user, url: update_username_profile_path, method: :put, remote: true do |f| %p Changing your username will change path to all personal projects! diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index 4b5817e10bf..c8c522e9812 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -1,5 +1,7 @@ %h3.page-title - OAuth2 + Application Settings +%p.light + OAuth2 protocol settings below. %fieldset.oauth-applications %legend Your applications diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/design.html.haml index 0d8075b7d43..8d09595fd4f 100644 --- a/app/views/profiles/design.html.haml +++ b/app/views/profiles/design.html.haml @@ -1,7 +1,7 @@ %h3.page-title - My appearance settings + Design Settings %p.light - Appearance settings saved to your profile and available across all devices + Appearance settings will be saved to your profile and made available across all devices. %hr = form_for @user, url: profile_path, remote: true, method: :put do |f| diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 0b30e772336..3bbad6fdf7b 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -1,5 +1,5 @@ %h3.page-title - My email addresses + Email Settings %p.light Your %b Primary Email @@ -34,4 +34,4 @@ .col-sm-10 = f.text_field :email, class: 'form-control' .form-actions - = f.submit 'Add', class: 'btn btn-create' + = f.submit 'Add email address', class: 'btn btn-create' diff --git a/app/views/profiles/groups/index.html.haml b/app/views/profiles/groups/index.html.haml index e9ffca8faf4..daf76636ff2 100644 --- a/app/views/profiles/groups/index.html.haml +++ b/app/views/profiles/groups/index.html.haml @@ -1,12 +1,12 @@ %h3.page-title - Group membership + Group Membership - if current_user.can_create_group? %span.pull-right = link_to new_group_path, class: "btn btn-new" do %i.fa.fa-plus New Group %p.light - Group members have access to all a group's projects + Group members have access to all group projects. %hr .panel.panel-default .panel-heading diff --git a/app/views/profiles/history.html.haml b/app/views/profiles/history.html.haml index 3951c47b5f2..9cafe03b8b3 100644 --- a/app/views/profiles/history.html.haml +++ b/app/views/profiles/history.html.haml @@ -1,7 +1,7 @@ %h3.page-title - Account history + My Account History %p.light - All events created by your account are listed here + All events created by your account are listed below. %hr .profile_history = render @events diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index c83c73ffcf9..965d5e032f9 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -1,12 +1,12 @@ %h3.page-title - My SSH keys (#{@keys.count}) + SSH Keys Settings .pull-right = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new" %p.light - SSH keys allow you to establish a secure connection between your computer and GitLab + My SSH keys: #{@keys.count} %br Before you can add an SSH key you need to - = link_to "generate it", help_page_path("ssh", "README") + = link_to "generate it.", help_page_path("ssh", "README") %hr = render 'key_table' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 28bc5a426ac..e3cd323927e 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -1,10 +1,9 @@ %h3.page-title - Notifications settings + Notifications Settings %p.light These are your global notification settings. %hr - = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f| -if @user.errors.any? %div.alert.alert-danger @@ -60,7 +59,7 @@ %p You can also specify notification level per group or per project. %br - By default all projects and groups uses notification level set above. + By default, all projects and groups will use the notification level set above. %h4 Groups: %ul.bordered-list - @group_members.each do |users_group| @@ -69,7 +68,7 @@ .col-md-6 %p - To specify notification level per project of a group you belong to, + To specify the notification level per project of a group you belong to, %br you need to be a member of the project itself, not only its group. %h4 Projects: diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 6b19db4eb5d..3b1ebbfaf59 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -1,4 +1,4 @@ -%h3.page-title Password +%h3.page-title Password Settings %p.light - if @user.password_automatically_set? Set your password. @@ -12,7 +12,7 @@ - unless @user.password_automatically_set? You must provide current password in order to change it. %br - After a successful password update you will be redirected to login page where you should login with your new password + After a successful password update, you will be redirected to the login page where you can log in with your new password. -if @user.errors.any? .alert.alert-danger %ul diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 640104fdad1..b2808c46c00 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,7 +1,7 @@ %h3.page-title - Profile settings + Profile Settings %p.light - This information appears on your profile. + This information will appear on your profile. - if current_user.ldap_user? Some options are unavailable for LDAP accounts %hr -- GitLab From 5d2dda9744eb902e397410f8f4b94af3a0acb1df Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Mon, 23 Feb 2015 11:39:08 -0800 Subject: [PATCH 1030/1609] Added information to tooltips Tooltips now have meaning by mentioning their function ("Filter by..."). Fixes #1992 --- app/helpers/events_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 063916a8df8..d38b546e1b2 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -30,7 +30,7 @@ module EventsHelper end content_tag :li, class: "filter_icon #{active}" do - link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do + link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => 'Filter by ' + tooltip.downcase do icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip) end end -- GitLab From 5ce2d44b136aae8e9e42397474a0e75bc6b32ded Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Mon, 23 Feb 2015 11:51:18 -0800 Subject: [PATCH 1031/1609] Added Profile tooltip For consistency sake, the profile in the navbar has a tooltip. --- app/views/layouts/_head_panel.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml index 77bfe4f996e..d5928d2ed25 100644 --- a/app/views/layouts/_head_panel.html.haml +++ b/app/views/layouts/_head_panel.html.haml @@ -42,7 +42,7 @@ = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Logout", class: 'has_bottom_tooltip', 'data-original-title' => 'Logout' do %i.fa.fa-sign-out %li.hidden-xs - = link_to current_user, class: "profile-pic", id: 'profile-pic' do + = link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do = image_tag avatar_icon(current_user.email, 60), alt: 'User activity' = render 'shared/outdated_browser' -- GitLab From e35fe204795bbb19d46f34af49c1ad4f7148e68f Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Mon, 23 Feb 2015 13:38:04 -0800 Subject: [PATCH 1032/1609] Filter icons look like proper buttons The filter icons (Push events, Merge events, Comments, Team) now have a border that signifies they can be pushed. This not only keeps it in line with the rest of the application buttons, but it makes it more obvious, especially for the Merge events button with a checkbox icon. --- app/helpers/events_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 063916a8df8..db0d4a26611 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -30,7 +30,7 @@ module EventsHelper end content_tag :li, class: "filter_icon #{active}" do - link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do + link_to request.path, class: 'btn has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip) end end -- GitLab From 0e1d31a734c576421bdb798f3f8e6bf9381c854b Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Mon, 23 Feb 2015 13:52:02 -0800 Subject: [PATCH 1033/1609] Git Clone btn more apparent Added a faint background to the button to show that it is in an active state for the user. White background is often hard to tell if something is being pushed, so adding a "shadow" makes it a bit easier to tell. Fixes #1998 --- app/assets/stylesheets/sections/projects.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/sections/projects.scss index 3bb3779c294..8bad9b139f4 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/sections/projects.scss @@ -111,6 +111,8 @@ color: $link_color; &.active { + background-color: #f5f5f5; + border: 1px solid rgba(0,0,0,0.195); color: #333; font-weight: bold; } -- GitLab From b3fd0ca04d498504e93894378be98dc1bda7e259 Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Mon, 23 Feb 2015 14:27:52 -0800 Subject: [PATCH 1034/1609] Toggle sidebar button more obvious The toggle is now at the top of the sidebar because it is not noticeable near the bottom. By placing it at the top, users will immediately know that they can have more space if they desire versus on the bottom, they will have to search for it and that's not desired. Fixes #2044 --- .../stylesheets/sections/nav_sidebar.scss | 18 +++++++++++++----- app/views/layouts/_collapse_button.html.haml | 4 ++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss index 17923ca499b..8841068b6a6 100644 --- a/app/assets/stylesheets/sections/nav_sidebar.scss +++ b/app/assets/stylesheets/sections/nav_sidebar.scss @@ -108,7 +108,7 @@ width: $sidebar_width; .nav-sidebar { - margin-top: 20px; + margin-top: 29px; position: fixed; top: 45px; width: $sidebar_width; @@ -127,7 +127,7 @@ width: 52px; .nav-sidebar { - margin-top: 20px; + margin-top: 29px; position: fixed; top: 45px; width: 52px; @@ -144,14 +144,22 @@ } } } + + .collapse-nav a { + left: 0px; + padding: 5px 23px 3px 22px; + } } } .collapse-nav a { position: fixed; - bottom: 15px; - padding: 10px; - background: #DDD; + top: 47px; + padding: 5px 13px 3px 13px; + left: 197px; + background: #EEE; + color: black; + border: 1px solid rgba(0,0,0,0.035); } @media (max-width: $screen-md-max) { diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml index b3b338b55bb..2ed51d87ca1 100644 --- a/app/views/layouts/_collapse_button.html.haml +++ b/app/views/layouts/_collapse_button.html.haml @@ -1,4 +1,4 @@ - if nav_menu_collapsed? - = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse' + = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close" - else - = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse' + = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close" -- GitLab From c6860a5828fe569f6a81e2c96bb7e4a32f572a29 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 23 Feb 2015 15:18:45 -0800 Subject: [PATCH 1035/1609] Fix style issue for rubocop --- config/routes.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index a3f047e36a4..f0979eac906 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -81,13 +81,13 @@ Gitlab::Application.routes.draw do scope path: :uploads do # Note attachments and User/Group/Project avatars - get ":model/:mounted_as/:id/:filename", - to: "uploads#show", + get ":model/:mounted_as/:id/:filename", + to: "uploads#show", constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } # Project markdown uploads - get ":id/:secret/:filename", - to: "projects/uploads#show", + get ":id/:secret/:filename", + to: "projects/uploads#show", constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } end @@ -148,7 +148,8 @@ Gitlab::Application.routes.draw do resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do root to: 'projects#index', as: :projects - resources(:projects, path: '/', + resources(:projects, + path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [:index, :show]) do root to: 'projects#show' @@ -268,12 +269,15 @@ Gitlab::Application.routes.draw do post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' scope do - get('/blob/*id/diff', to: 'blob#diff', + get('/blob/*id/diff', + to: 'blob#diff', constraints: { id: /.+/, format: false }, as: :blob_diff) - get('/blob/*id', to: 'blob#show', + get('/blob/*id', + to: 'blob#show', constraints: { id: /.+/, format: false }, as: :blob) - delete('/blob/*id', to: 'blob#destroy', + delete('/blob/*id', + to: 'blob#destroy', constraints: { id: /.+/, format: false }) end -- GitLab From cb65a3f1d965f226f19ac20c69435c3387eb39ed Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Mon, 23 Feb 2015 15:48:00 -0800 Subject: [PATCH 1036/1609] Changed button styles Styles for buttons are changed to match user expectations. --- app/views/profiles/accounts/show.html.haml | 2 +- app/views/profiles/notifications/show.html.haml | 2 +- app/views/profiles/passwords/edit.html.haml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 53a50f6796b..248c9137ca5 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -57,7 +57,7 @@ %p.light = user_url(@user) %div - = f.submit 'Save username', class: "btn btn-save" + = f.submit 'Save username', class: "btn btn-create" - if show_profile_remove_tab? %fieldset.remove-account diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 28bc5a426ac..516c4f82365 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -51,7 +51,7 @@ %p You will receive all notifications from projects in which you participate .form-actions - = f.submit 'Save changes', class: "btn btn-save" + = f.submit 'Save changes', class: "btn btn-create" .clearfix %hr diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 6b19db4eb5d..3941fff5ea1 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -35,4 +35,4 @@ .col-sm-10 = f.password_field :password_confirmation, required: true, class: 'form-control' .form-actions - = f.submit 'Save password', class: "btn btn-save" + = f.submit 'Save password', class: "btn btn-create" -- GitLab From 746dd89ab010299f731c195082087d32f25698df Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Mon, 23 Feb 2015 16:19:03 -0800 Subject: [PATCH 1037/1609] Fix 404 when deleting a project The deletion from the admin section was redirecting to the wrong address. --- app/controllers/projects_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 38341b1c8c6..d1583e6ebfb 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -102,7 +102,7 @@ class ProjectsController < ApplicationController flash[:alert] = 'Project deleted.' if request.referer.include?('/admin') - redirect_to admin_namespace_projects_path + redirect_to admin_namespaces_projects_path else redirect_to projects_dashboard_path end -- GitLab From b821a1bd41166c295ea1625ccf57b9bb48f61649 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 23 Feb 2015 16:29:32 -0800 Subject: [PATCH 1038/1609] Fix markdown image uploader after rails update --- config/routes.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index f0979eac906..c0dbf738c1f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -86,9 +86,9 @@ Gitlab::Application.routes.draw do constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ } # Project markdown uploads - get ":id/:secret/:filename", - to: "projects/uploads#show", - constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/, filename: /.+/ } + get ":namespace_id/:id/:secret/:filename", + to: "projects/uploads#show", + constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, id: /[a-zA-Z.0-9_\-]+/, filename: /.+/ } end # -- GitLab From 0f6221e7365a93a356410f4d38443381924d4cc6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 23 Feb 2015 17:56:05 -0800 Subject: [PATCH 1039/1609] Make services migration more reliable --- ...0907220153_serialize_service_properties.rb | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/db/migrate/20140907220153_serialize_service_properties.rb b/db/migrate/20140907220153_serialize_service_properties.rb index bd75ab1eacb..d45a10465be 100644 --- a/db/migrate/20140907220153_serialize_service_properties.rb +++ b/db/migrate/20140907220153_serialize_service_properties.rb @@ -1,6 +1,9 @@ class SerializeServiceProperties < ActiveRecord::Migration def change - add_column :services, :properties, :text + unless column_exists?(:services, :properties) + add_column :services, :properties, :text + end + Service.reset_column_information associations = @@ -19,18 +22,21 @@ class SerializeServiceProperties < ActiveRecord::Migration :api_version, :jira_issue_transition_id], } - Service.all.each do |service| + Service.find_each(batch_size: 500).each do |service| associations[service.type.to_sym].each do |attribute| service.send("#{attribute}=", service.attributes[attribute.to_s]) end - service.save + + service.save(validate: false) end - remove_column :services, :project_url, :string - remove_column :services, :subdomain, :string - remove_column :services, :room, :string - remove_column :services, :recipients, :text - remove_column :services, :api_key, :string - remove_column :services, :token, :string + if column_exists?(:services, :project_url) + remove_column :services, :project_url, :string + remove_column :services, :subdomain, :string + remove_column :services, :room, :string + remove_column :services, :recipients, :text + remove_column :services, :api_key, :string + remove_column :services, :token, :string + end end end -- GitLab From b0dfe434c60da7d04ddf23f7a3e85af97d377568 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 23 Feb 2015 12:38:30 -0800 Subject: [PATCH 1040/1609] Using gitlab url to build links for gitlab issue tracker and add a spec. Fix rubocop warnings in path. --- .../gitlab_issue_tracker_service.rb | 12 +++- config/routes.rb | 27 +++++---- spec/helpers/gitlab_markdown_helper_spec.rb | 6 +- .../gitlab_issue_tracker_service_spec.rb | 60 +++++++++++++++++++ 4 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 spec/models/project_services/gitlab_issue_tracker_service_spec.rb diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 782cf42ce55..05c048e4e45 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -27,14 +27,20 @@ class GitlabIssueTrackerService < IssueTrackerService end def project_url - namespace_project_issues_path(project.namespace, project) + "#{gitlab_url}#{namespace_project_issues_path(project.namespace, project)}" end def new_issue_url - new_namespace_project_issue_path namespace_id: project.namespace, project_id: project + "#{gitlab_url}#{new_namespace_project_issue_path(namespace_id: project.namespace, project_id: project)}" end def issue_url(iid) - "#{Gitlab.config.gitlab.url}#{namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid)}" + "#{gitlab_url}#{namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid)}" + end + + private + + def gitlab_url + Gitlab.config.gitlab.relative_url_root.chomp("/") if Gitlab.config.gitlab.relative_url_root end end diff --git a/config/routes.rb b/config/routes.rb index c0dbf738c1f..ecd439aecea 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -269,16 +269,23 @@ Gitlab::Application.routes.draw do post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob' scope do - get('/blob/*id/diff', - to: 'blob#diff', - constraints: { id: /.+/, format: false }, - as: :blob_diff) - get('/blob/*id', - to: 'blob#show', - constraints: { id: /.+/, format: false }, as: :blob) - delete('/blob/*id', - to: 'blob#destroy', - constraints: { id: /.+/, format: false }) + get( + '/blob/*id/diff', + to: 'blob#diff', + constraints: { id: /.+/, format: false }, + as: :blob_diff + ) + get( + '/blob/*id', + to: 'blob#show', + constraints: { id: /.+/, format: false }, + as: :blob + ) + delete( + '/blob/*id', + to: 'blob#destroy', + constraints: { id: /.+/, format: false } + ) end scope do diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 68269ad25a8..76fcf888a6a 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -474,7 +474,7 @@ describe GitlabMarkdownHelper do # First issue link expect(groups[1]). - to match(/href="#{namespace_project_issue_url(project.namespace, project, issues[0])}"/) + to match(/href="#{namespace_project_issue_path(project.namespace, project, issues[0])}"/) expect(groups[1]).to match(/##{issues[0].iid}$/) # Internal commit link @@ -483,7 +483,7 @@ describe GitlabMarkdownHelper do # Second issue link expect(groups[3]). - to match(/href="#{namespace_project_issue_url(project.namespace, project, issues[1])}"/) + to match(/href="#{namespace_project_issue_path(project.namespace, project, issues[1])}"/) expect(groups[3]).to match(/##{issues[1].iid}$/) # Trailing commit link @@ -611,7 +611,7 @@ describe GitlabMarkdownHelper do end it "should generate absolute urls for refs" do - expect(markdown("##{issue.iid}")).to include(namespace_project_issue_url(project.namespace, project, issue)) + expect(markdown("##{issue.iid}")).to include(namespace_project_issue_path(project.namespace, project, issue)) end it "should generate absolute urls for emoji" do diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb new file mode 100644 index 00000000000..c474f4a2d95 --- /dev/null +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -0,0 +1,60 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# +require 'spec_helper' + +describe GitlabIssueTrackerService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + + describe 'project and issue urls' do + let(:project) { create(:project) } + + context 'with absolute urls' do + before do + @service = project.create_gitlab_issue_tracker_service(active: true) + end + + after do + @service.destroy! + end + + it 'should give the correct path' do + expect(@service.project_url).to eq("/#{project.path_with_namespace}/issues") + expect(@service.new_issue_url).to eq("/#{project.path_with_namespace}/issues/new") + expect(@service.issue_url(432)).to eq("/#{project.path_with_namespace}/issues/432") + end + end + + context 'with enabled relative urls' do + before do + Settings.gitlab.stub(:relative_url_root).and_return("/gitlab/root") + @service = project.create_gitlab_issue_tracker_service(active: true) + end + + after do + @service.destroy! + end + + it 'should give the correct path' do + expect(@service.project_url).to eq("/gitlab/root/#{project.path_with_namespace}/issues") + expect(@service.new_issue_url).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new") + expect(@service.issue_url(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432") + end + end + end +end -- GitLab From 12589d339070d86b57a4f97778a48b1b9cc5a0a1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 23 Feb 2015 18:43:39 -0800 Subject: [PATCH 1041/1609] Improve sidebar menu for project settings --- app/assets/stylesheets/sections/nav_sidebar.scss | 2 +- app/views/layouts/nav/_project.html.haml | 8 +------- features/steps/shared/project_tab.rb | 4 +++- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss index 17923ca499b..8e02b375074 100644 --- a/app/assets/stylesheets/sections/nav_sidebar.scss +++ b/app/assets/stylesheets/sections/nav_sidebar.scss @@ -47,7 +47,7 @@ border-left: 3px solid $style_color; &.no-highlight { - background: none; + background: none !important; border: none; } diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 4d859e817ac..15b489c7d99 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -6,12 +6,7 @@ %span Back to project - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_namespace_project_path(@project.namespace, @project), title: 'Settings', class: "stat-tab tab no-highlight" do - %i.fa.fa-cogs - %span - Settings - %i.fa.fa-angle-down + %li.separate-item = render 'projects/settings_nav' @@ -98,4 +93,3 @@ %i.fa.fa-cogs %span Settings - %i.fa.fa-angle-down diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 6aa4f1b20df..c5aed19331c 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -41,6 +41,8 @@ module SharedProjectTab end step 'the active main tab should be Settings' do - ensure_active_main_tab('Settings') + within '.nav-sidebar' do + page.should have_content('Back to project') + end end end -- GitLab From 897a2de54c1d5cbead4589d44a3d173c14849f23 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 23 Feb 2015 19:35:42 -0800 Subject: [PATCH 1042/1609] Allow non authenticated access to avatars --- app/controllers/uploads_controller.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index d5877977258..73b124bb34c 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -1,4 +1,7 @@ class UploadsController < ApplicationController + skip_before_filter :authenticate_user!, :reject_blocked + before_filter :authorize_access + def show model = params[:model].camelize.constantize.find(params[:id]) uploader = model.send(params[:mounted_as]) @@ -14,4 +17,10 @@ class UploadsController < ApplicationController redirect_to uploader.url end end + + def authorize_access + unless params[:mounted_as] == 'avatar' + authenticate_user! && reject_blocked + end + end end -- GitLab From c9829146f88ff87460add83a3719db3e2593f278 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 23 Feb 2015 22:21:49 +0100 Subject: [PATCH 1043/1609] LDAP users don't need to set a password to Git over HTTP. --- app/models/user.rb | 5 +++++ app/views/shared/_clone_panel.html.haml | 2 +- app/views/shared/_no_password.html.haml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 21ccc76978e..08ad619a90c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -45,6 +45,7 @@ # last_credential_check_at :datetime # github_access_token :string(255) # notification_email :string(255) +# password_automatically_set :boolean default(FALSE) # require 'carrierwave/orm/activerecord' @@ -350,6 +351,10 @@ class User < ActiveRecord::Base keys.count == 0 end + def require_password? + password_automatically_set? && !ldap_user? + end + def can_change_username? gitlab_config.username_changing_enabled end diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index df0bde76980..a1121750ca3 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -9,7 +9,7 @@ :"data-container" => "body"} SSH %button{ | - class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.password_automatically_set? }", | + class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", | :"data-clone" => project.http_url_to_repo, | :"data-title" => "Set a password on your account
    to pull or push via #{gitlab_config.protocol.upcase}", :"data-html" => "true", diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml index 022097cda16..a43bf33751a 100644 --- a/app/views/shared/_no_password.html.haml +++ b/app/views/shared/_no_password.html.haml @@ -1,4 +1,4 @@ -- if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.password_automatically_set? +- if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password? .no-password-message.alert.alert-warning.hidden-xs You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account -- GitLab From 64ca07c3b95c6b1e9444d9139858b11a6097c1ca Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 23 Feb 2015 21:27:41 -0800 Subject: [PATCH 1044/1609] Better readme title --- app/assets/stylesheets/sections/tree.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/sections/tree.scss b/app/assets/stylesheets/sections/tree.scss index ff9464e217f..60a1c00b04b 100644 --- a/app/assets/stylesheets/sections/tree.scss +++ b/app/assets/stylesheets/sections/tree.scss @@ -120,13 +120,13 @@ } .readme-holder { - border-top: 1px dashed #CCC; - padding-top: 10px; - .readme-file-title { font-size: 14px; + font-weight: bold; margin-bottom: 20px; color: #777; + border-bottom: 1px solid #DDD; + padding: 10px 0; } } -- GitLab From e363f2e67544e210e92acc06a5af90d91c0aa684 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 24 Feb 2015 06:50:40 +0000 Subject: [PATCH 1045/1609] Fix merge request URL passed to Webhooks. Previously the symbol "url" in the object_attributes hash would always be nil. --- CHANGELOG | 1 + lib/gitlab/url_builder.rb | 9 +++++++++ spec/lib/gitlab/url_builder_spec.rb | 8 ++++++++ 3 files changed, 18 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d879ee85728..4a5c27b06ba 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) + - Fix merge request URL passed to Webhooks. (Stan Hu) - Move labels/milestones tabs to sidebar - Upgrade Rails gem to version 4.1.9. - Improve error messages for file edit failures diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index e7153cc3225..7ab3f090a89 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -10,6 +10,8 @@ module Gitlab case @type when :issue issue_url(id) + when :merge_request + merge_request_url(id) end end @@ -22,5 +24,12 @@ module Gitlab project_id: issue.project, host: Gitlab.config.gitlab['url']) end + + def merge_request_url(id) + merge_request = MergeRequest.find(id) + project_merge_request_url(id: merge_request.id, + project_id: merge_request.project, + host: Gitlab.config.gitlab['url']) + end end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 716430340b6..518239fab6d 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -8,4 +8,12 @@ describe Gitlab::UrlBuilder do expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}" end end + + describe 'When asking for an merge request' do + it 'returns the merge request url' do + merge_request = create(:merge_request) + url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.to_param}/merge_requests/#{merge_request.id}" + end + end end -- GitLab From bd6550a5121d5a386c5928bbd74b79ab382c548f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 23 Feb 2015 22:54:32 -0800 Subject: [PATCH 1046/1609] Bump gitlab-shell to 2.5.4 --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index aedc15bb0c6..fe16b348d97 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.5.3 +2.5.4 -- GitLab From 2feaa69cd5831578f99c3833f8b1e3e5a8031c1d Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 23 Feb 2015 23:10:35 -0800 Subject: [PATCH 1047/1609] Update version of gitlab-shell in the installation and update documentation. --- doc/install/installation.md | 4 ++-- doc/update/6.x-or-7.x-to-7.8.md | 4 ++-- doc/update/7.7-to-7.8.md | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index f5dcec2f61e..28597fd39d2 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -141,7 +141,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Try connecting to the new database with the new user sudo -u git -H psql -d gitlabhq_production - + # Quit the database session gitlabhq_production> \q @@ -280,7 +280,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.4.3] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.5.4] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: diff --git a/doc/update/6.x-or-7.x-to-7.8.md b/doc/update/6.x-or-7.x-to-7.8.md index 859f4c1a6d6..5884312c47f 100644 --- a/doc/update/6.x-or-7.x-to-7.8.md +++ b/doc/update/6.x-or-7.x-to-7.8.md @@ -123,7 +123,7 @@ sudo apt-get install libkrb5-dev ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.4.3 +sudo -u git -H git checkout v2.5.4 ``` ## 7. Install libs, migrations, etc. @@ -163,7 +163,7 @@ git diff 6-0-stable:config/gitlab.yml.example 7-8-stable:config/gitlab.yml.examp * Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/gitlab.yml.example but with your settings. * Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/unicorn.rb.example but with your settings. -* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.4.3/config.yml.example but with your settings. +* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.5.4/config.yml.example but with your settings. * Copy rack attack middleware config ```bash diff --git a/doc/update/7.7-to-7.8.md b/doc/update/7.7-to-7.8.md index a8a5c7f66c6..46ca163c1bb 100644 --- a/doc/update/7.7-to-7.8.md +++ b/doc/update/7.7-to-7.8.md @@ -37,7 +37,7 @@ sudo -u git -H git checkout 7-8-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.5.3 +sudo -u git -H git checkout v2.5.4 ``` ### 4. Install libs, migrations, etc. @@ -102,7 +102,7 @@ If all items are green, then congratulations upgrade is complete! ### 8. GitHub settings (if applicable) -If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it +If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it only contains a root URL (ex. `https://gitlab.example.com/`) ## Things went south? Revert to previous version (7.7) -- GitLab From f6add983d332758cd1ae22732da762cff608af1e Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Tue, 24 Feb 2015 09:42:07 +0000 Subject: [PATCH 1048/1609] Use proper Gitorious icons on import pages --- app/assets/images/gitorious-logo-black.png | Bin 0 -> 809 bytes app/assets/images/gitorious-logo-blue.png | Bin 0 -> 495 bytes app/assets/stylesheets/sections/import.scss | 18 ++++++++++++++++++ app/views/import/gitorious/status.html.haml | 2 +- app/views/projects/new.html.haml | 2 +- 5 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 app/assets/images/gitorious-logo-black.png create mode 100644 app/assets/images/gitorious-logo-blue.png create mode 100644 app/assets/stylesheets/sections/import.scss diff --git a/app/assets/images/gitorious-logo-black.png b/app/assets/images/gitorious-logo-black.png new file mode 100644 index 0000000000000000000000000000000000000000..78f17a9af79dc2395acd9bcc2a391589dd6b6ed8 GIT binary patch literal 809 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4i*Lm28M*4p$rTROiAAEE)4(M`_JrWU|`@Z z@Q5sCV9-+rVaAH3_GJtV4D2PIzOL-|nZ)_j1bkfE!WbBsc6quuhGg7(JJZ)ABv9md z{rlb(ky@OBiZUyFUAnF){Ze95R`p!to8Zy1)>}YJscFTxcUhMgEf5o|6`UpzGC??E z!i?EU!v5V$I?Xayx?Xm2pWAPKrndav<_C8h6W{GEzW4L)?{|Oa7vC@bpi|UV=Ec}v zJ|``LbCJcock3^2Tw&$fs@BXs#AYW=z-Lkst&9Ss>jLDti%21$_Q|>kQ z^a|Aj)eLGiQRc^2xxd&Taa8*)TjPC`pPS~FICxolRl2!P>JC}9H6~SqdrN%$@^71U zmiJ0LI8z&Q=VyLLpp&}vCWc3Tub3jG%+I~r(q^`IzBi|DP^jgBxjoCKuZz+(+w2qh z{?hg<2VYzaVpdsPqrmi|pJAEtt9&Qc2_cI%F8G-&#L?icwD5z^wTqsT4?cvg=af=o zXy_}tw*G~foT(R=@r?dT262lNi3907mY5eSS1@UOYB^iH@~6u2r`yhLE+UiQ$qq>MYDI9b}PNE-N|mD_%!WyL9m;_z1IalECP&w zlo`a_jm>8;ir3vd?8=z&g})%+RqkGI>EF3Y2Peclv#h&6_h`mR!*0*_*UOSUIJ!U0 z`>y*#v`^*@7nM(E>Jwr=-hTw;>(ZH_U}>+Rc%bA_RhSMEBhlh&HZ=a&i_SW z!j+{C7c91_FcsQ+^s2Qq%_}{v7j^TL*6QbvR(@(PjQilg9%T6_{v5;H8@G!7y#i$c MPgg&ebxsLQ0E*vQZvX%Q literal 0 HcmV?d00001 diff --git a/app/assets/images/gitorious-logo-blue.png b/app/assets/images/gitorious-logo-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..4962cffba3153927ddd8cd5ff295fe4ca142bfa8 GIT binary patch literal 495 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hEH=O_WMlYe8$3$ej5H@U|_8Aba4!+xb=3{M!`b{ zJg!e=idUovv7HdwlH)LyHzTr%(a6(FS4gXHr*eZ#j<(;w{Md-9X{nKA3s|jU-#uQw z@!>^Q!G%#*mXyrvcyS}neU%6A+ZYBUpqG?Z&Tug3-m@w9WM|BDQCY3~_pMCHJaGp9 z=gZrs#&kTq$f~3ycr%^PV%zn=#R=Oz0~YfxKQM2uee64Br378|hH&2pQS4{to?e<; zX?$Rot?ufql`O@k0c#bc&-3M-+qt&8L4^ZhpIYRrQr&!WP6wBLVG+e!|K1YaCU)sE z>xMh*eE)quWczG)I_WRGF>r?lQ_{W@6){_rcb$pcf1={6WYV*vhG27UZr*rZ^8sJO z%x>on-J|@Ly%(z%sI=S&swuWq_$JqcY#GCZpzI$tnOhaFypY+(z+l0+;^oQHtDd}P zGF`cUuJaeM8$lulpFeFscx$o4?vS%VMRC06L(XNZs247pmNBE+qL#_1cSD?2bHP54 O`#oL#T-G@yGywpa6T8&_ literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/sections/import.scss b/app/assets/stylesheets/sections/import.scss new file mode 100644 index 00000000000..3df4bb84bd2 --- /dev/null +++ b/app/assets/stylesheets/sections/import.scss @@ -0,0 +1,18 @@ +i.icon-gitorious { + display: inline-block; + background-position: 0px 0px; + background-size: contain; + background-repeat: no-repeat; +} + +i.icon-gitorious-small { + background-image: image-url('gitorious-logo-blue.png'); + width: 13px; + height: 13px; +} + +i.icon-gitorious-big { + background-image: image-url('gitorious-logo-black.png'); + width: 18px; + height: 18px; +} diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 35ed0a717de..8ede5c3e840 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -1,5 +1,5 @@ %h3.page-title - %i.fa.fa-gitorious + %i.icon-gitorious.icon-gitorious-big Import repositories from Gitorious.org %p.light diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 33162ded4a6..c37ae8d31d2 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -70,7 +70,7 @@ .col-sm-2 .col-sm-10 = link_to new_import_gitorious_path do - %i.fa.fa-heart + %i.icon-gitorious.icon-gitorious-small Import projects from Gitorious.org %hr.prepend-botton-10 -- GitLab From 71a844cdaee129d4e300c20cbb27db009cf81b73 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Sat, 14 Feb 2015 18:18:05 +0200 Subject: [PATCH 1049/1609] Web Editor: save to new branch --- app/controllers/projects/blob_controller.rb | 26 ++++++++++++++++--- app/services/files/create_service.rb | 3 ++- app/services/files/update_service.rb | 3 ++- app/views/projects/blob/edit.html.haml | 7 +++++ app/views/projects/blob/new.html.haml | 7 +++++ .../satellite/files/edit_file_action.rb | 6 +++-- lib/gitlab/satellite/files/new_file_action.rb | 10 +++++-- 7 files changed, 53 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 1207548eae0..4b7eb4df298 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -1,6 +1,7 @@ # Controller for viewing a file's blame class Projects::BlobController < Projects::ApplicationController include ExtractsPath + include ActionView::Helpers::SanitizeHelper # Raised when given an invalid file path class InvalidPathError < StandardError; end @@ -21,11 +22,18 @@ class Projects::BlobController < Projects::ApplicationController def create file_path = File.join(@path, File.basename(params[:file_name])) - result = Files::CreateService.new(@project, current_user, params, @ref, file_path).execute + result = Files::CreateService.new( + @project, + current_user, + params.merge(new_branch: sanitized_new_branch_name), + @ref, + file_path + ).execute if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@ref, file_path)) + ref = sanitized_new_branch_name.presence || @ref + redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(ref, file_path)) else flash[:alert] = result[:message] render :new @@ -41,7 +49,13 @@ class Projects::BlobController < Projects::ApplicationController def update result = Files::UpdateService. - new(@project, current_user, params, @ref, @path).execute + new( + @project, + current_user, + params.merge(new_branch: sanitized_new_branch_name), + @ref, + @path + ).execute if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" @@ -131,6 +145,8 @@ class Projects::BlobController < Projects::ApplicationController if from_merge_request diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + "#file-path-#{hexdigest(@path)}" + elsif sanitized_new_branch_name.present? + namespace_project_blob_path(@project.namespace, @project, File.join(sanitized_new_branch_name, @path)) else namespace_project_blob_path(@project.namespace, @project, @id) end @@ -140,4 +156,8 @@ class Projects::BlobController < Projects::ApplicationController # If blob edit was initiated from merge request page @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) end + + def sanitized_new_branch_name + @new_branch ||= sanitize(strip_tags(params[:new_branch])) + end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 2c457ef2cef..de5322e990a 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -38,7 +38,8 @@ module Files created_successfully = new_file_action.commit!( params[:content], params[:commit_message], - params[:encoding] + params[:encoding], + params[:new_branch] ) if created_successfully diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index bcf0e7f3cee..328cf3a4b06 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -23,7 +23,8 @@ module Files edit_file_action.commit!( params[:content], params[:commit_message], - params[:encoding] + params[:encoding], + params[:new_branch] ) success diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 6884ad1f2f3..1f61a0b940c 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -14,6 +14,13 @@ = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" + + .form-group.branch + = label_tag 'branch', class: 'control-label' do + Branch + .col-sm-10 + = text_field_tag 'new_branch', @ref, class: "form-control" + = hidden_field_tag 'last_commit', @last_commit = hidden_field_tag 'content', '', id: "file-content" = hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id] diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 45865d552ae..d78a01f6422 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -4,6 +4,13 @@ = render 'projects/blob/editor', ref: @ref = render 'shared/commit_message_container', params: params, placeholder: 'Add new file' + + .form-group.branch + = label_tag 'branch', class: 'control-label' do + Branch + .col-sm-10 + = text_field_tag 'new_branch', @ref, class: "form-control" + = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_tree_path(@project.namespace, @project, @id) diff --git a/lib/gitlab/satellite/files/edit_file_action.rb b/lib/gitlab/satellite/files/edit_file_action.rb index 82d71ab9906..3cb9c0b5ecb 100644 --- a/lib/gitlab/satellite/files/edit_file_action.rb +++ b/lib/gitlab/satellite/files/edit_file_action.rb @@ -10,7 +10,7 @@ module Gitlab # Returns false if committing the change fails # Returns false if pushing from the satellite to bare repo failed or was rejected # Returns true otherwise - def commit!(content, commit_message, encoding) + def commit!(content, commit_message, encoding, new_branch = nil) in_locked_and_timed_satellite do |repo| prepare_satellite!(repo) @@ -42,10 +42,12 @@ module Gitlab end + target_branch = new_branch.present? ? "#{ref}:#{new_branch}" : ref + # push commit back to bare repo # will raise CommandFailed when push fails begin - repo.git.push({ raise: true, timeout: true }, :origin, ref) + repo.git.push({ raise: true, timeout: true }, :origin, target_branch) rescue Grit::Git::CommandFailed => ex log_and_raise(PushFailed, ex.message) end diff --git a/lib/gitlab/satellite/files/new_file_action.rb b/lib/gitlab/satellite/files/new_file_action.rb index 69f7ffa94e4..724dfa0d042 100644 --- a/lib/gitlab/satellite/files/new_file_action.rb +++ b/lib/gitlab/satellite/files/new_file_action.rb @@ -9,7 +9,7 @@ module Gitlab # Returns false if committing the change fails # Returns false if pushing from the satellite to bare repo failed or was rejected # Returns true otherwise - def commit!(content, commit_message, encoding) + def commit!(content, commit_message, encoding, new_branch = nil) in_locked_and_timed_satellite do |repo| prepare_satellite!(repo) @@ -45,9 +45,15 @@ module Gitlab # will raise CommandFailed when commit fails repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) + target_branch = if new_branch.present? && !@project.empty_repo? + "#{ref}:#{new_branch}" + else + "#{current_ref}:#{ref}" + end + # push commit back to bare repo # will raise CommandFailed when push fails - repo.git.push({ raise: true, timeout: true }, :origin, "#{current_ref}:#{ref}") + repo.git.push({ raise: true, timeout: true }, :origin, target_branch) # everything worked true -- GitLab From 3177693c6f47fda14f84abf0b8f694aec3e076fc Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 23 Feb 2015 21:00:30 +0200 Subject: [PATCH 1050/1609] WebEditor: save to new branch: spinach --- features/project/source/browse_files.feature | 22 +++++++++++++++++++ features/steps/project/source/browse_files.rb | 15 ++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index ee8d0bffa9b..90b966dd645 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -34,6 +34,17 @@ Feature: Project Source Browse Files Then I am redirected to the new file And I should see its new content + @javascript + Scenario: I can create and commit file and specify new branch + Given I click on "new file" link in repo + And I edit code + And I fill the new file name + And I fill the commit message + And I fill the new branch name + And I click on "Commit Changes" + Then I am redirected to the new file on new branch + And I should see its new content + @javascript @tricky Scenario: I can create file in empty repo Given I own an empty project @@ -83,6 +94,17 @@ Feature: Project Source Browse Files Then I am redirected to the ".gitignore" And I should see its new content + @javascript + Scenario: I can edit and commit file to new branch + Given I click on ".gitignore" file in repo + And I click button "Edit" + And I edit code + And I fill the commit message + And I fill the new branch name + And I click on "Commit Changes" + Then I am redirected to the ".gitignore" on new branch + And I should see its new content + @javascript @wip Scenario: If I don't change the content of the file I see an error message Given I click on ".gitignore" file in repo diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 98d8a60e1a5..557555aee58 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -69,6 +69,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps fill_in :file_name, with: new_file_name end + step 'I fill the new branch name' do + fill_in :new_branch, with: 'new_branch_name' + end + step 'I fill the new file name with an illegal name' do fill_in :file_name, with: '.git' end @@ -148,6 +152,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'master/.gitignore')) end + step 'I am redirected to the ".gitignore" on new branch' do + expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'new_branch_name/.gitignore')) + end + step 'I am redirected to the permalink URL' do expect(current_path).to( eq(namespace_project_blob_path(@project.namespace, @project, @@ -161,6 +169,11 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps @project.namespace, @project, 'master/' + new_file_name)) end + step 'I am redirected to the new file on new branch' do + expect(current_path).to eq(namespace_project_blob_path( + @project.namespace, @project, 'new_branch_name/' + new_file_name)) + end + step "I don't see the permalink link" do expect(page).not_to have_link('permalink') end @@ -177,7 +190,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps click_link 'add a file' # Remove pre-receive hook so we can push without auth - FileUtils.rm(File.join(@project.repository.path, 'hooks', 'pre-receive')) + FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive')) end private -- GitLab From 51b18fe6f6558f14e5a8123d27f65f3557134822 Mon Sep 17 00:00:00 2001 From: Igor Bogoslavskyi Date: Tue, 24 Feb 2015 10:50:18 +0100 Subject: [PATCH 1051/1609] typo fixed fixed a typo in an url to gitlab-ssl. Now it does not return the 404 error --- doc/update/6.x-or-7.x-to-7.8.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/6.x-or-7.x-to-7.8.md b/doc/update/6.x-or-7.x-to-7.8.md index 5884312c47f..673d9253d62 100644 --- a/doc/update/6.x-or-7.x-to-7.8.md +++ b/doc/update/6.x-or-7.x-to-7.8.md @@ -179,7 +179,7 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab ### Change Nginx settings * HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stablef/lib/support/nginx/gitlab-ssl but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab-ssl but with your settings. * A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. ## 9. Start application -- GitLab From 63c4f3ca7665d076ea143991823ec99c8f996a73 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 24 Feb 2015 13:20:04 +0200 Subject: [PATCH 1052/1609] update gitlab-grack to 2.0.0.rc2 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 093f7bacc06..00d8f4429fc 100644 --- a/Gemfile +++ b/Gemfile @@ -41,7 +41,7 @@ gem "browser" gem "gitlab_git", '7.0.0.rc14' # Ruby/Rack Git Smart-HTTP Server Handler -gem 'gitlab-grack', '~> 2.0.0.pre', require: 'grack' +gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' # LDAP Auth gem 'gitlab_omniauth-ldap', '1.2.0', require: "omniauth-ldap" diff --git a/Gemfile.lock b/Gemfile.lock index 4bc47836e71..07cdbc2d755 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -183,7 +183,7 @@ GEM gitlab-flowdock-git-hook (0.4.2.2) gitlab-grit (>= 2.4.1) multi_json - gitlab-grack (2.0.0.pre) + gitlab-grack (2.0.0.rc2) rack (~> 1.5.1) gitlab-grit (2.7.2) charlock_holmes (~> 0.6) @@ -668,7 +668,7 @@ DEPENDENCIES gemnasium-gitlab-service (~> 0.2) github-markup gitlab-flowdock-git-hook (~> 0.4.2) - gitlab-grack (~> 2.0.0.pre) + gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.0) gitlab_emoji (~> 0.0.1.1) gitlab_git (= 7.0.0.rc14) -- GitLab From 9338c6325263d950966e87ddb23095075f18558e Mon Sep 17 00:00:00 2001 From: kfei Date: Wed, 17 Dec 2014 00:53:17 -0800 Subject: [PATCH 1053/1609] Gracefully shutdown services in Docker container The problem is `docker stop` only sends SIGTERM to the PID 1 inside the container, and the PID 1 (`/bin/sh -c ...`) does not take care of signals. Hence the services (e.g., postgresql, redis, sidekiq, etc) never have chances to graceful shutdown. Docker just kills the container after its 10 seconds timeout by default. What this commit does: 1) Add a wrapper as the default executable of Docker container. Which starts services through `runit`, reconfigure Gitlab by `gitlab-ctl` and gracefully shutdown all services when a SIGTERM is received. 2) Create an `assets` directory for assets. 3) Add `.dockerignore` file. Now you'll see the following log messages after `docker stop`: ``` SIGTERM signal received, try to gracefully shutdown all services... ok: down: logrotate: 1s, normally up ok: down: nginx: 0s, normally up ok: down: postgresql: 1s, normally up ok: down: redis: 0s, normally up ok: down: sidekiq: 0s, normally up ok: down: unicorn: 0s, normally up ``` Signed-off-by: kfei --- docker/.dockerignore | 1 + docker/Dockerfile | 11 +++++++---- docker/{ => assets}/gitlab.rb | 0 docker/assets/wrapper | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 docker/.dockerignore rename docker/{ => assets}/gitlab.rb (100%) create mode 100755 docker/assets/wrapper diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 00000000000..dd449725e18 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1 @@ +*.md diff --git a/docker/Dockerfile b/docker/Dockerfile index cfb89357a67..3a0a55e18e3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,9 +26,12 @@ RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ # Expose web & ssh EXPOSE 80 22 -# Volume & configuration +# Declare volumes VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"] -ADD gitlab.rb /etc/gitlab/ -# Default is to run runit & reconfigure -CMD gitlab-ctl reconfigure & /opt/gitlab/embedded/bin/runsvdir-start \ No newline at end of file +# Copy assets +COPY assets/gitlab.rb /etc/gitlab/ +COPY assets/wrapper /usr/local/bin/ + +# Wrapper to handle signal, trigger runit and reconfigure GitLab +CMD ["/usr/local/bin/wrapper"] diff --git a/docker/gitlab.rb b/docker/assets/gitlab.rb similarity index 100% rename from docker/gitlab.rb rename to docker/assets/gitlab.rb diff --git a/docker/assets/wrapper b/docker/assets/wrapper new file mode 100755 index 00000000000..9e6e7a05903 --- /dev/null +++ b/docker/assets/wrapper @@ -0,0 +1,17 @@ +#!/bin/bash + +function sigterm_handler() { + echo "SIGTERM signal received, try to gracefully shutdown all services..." + gitlab-ctl stop +} + +trap "sigterm_handler; exit" TERM + +function entrypoint() { + # Default is to run runit and reconfigure GitLab + gitlab-ctl reconfigure & + /opt/gitlab/embedded/bin/runsvdir-start & + wait +} + +entrypoint -- GitLab From 1e1a95f5c36357b03df9420903d421edf7ac08c4 Mon Sep 17 00:00:00 2001 From: Bugagazavr Date: Tue, 24 Feb 2015 15:12:25 +0300 Subject: [PATCH 1054/1609] Update Changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d879ee85728..deda7ffc32c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 7.9.0 (unreleased) - Improve error messages for file edit failures - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. + - Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev) v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. -- GitLab From ad6d6232342558705c54ba70a94f9d7ddbd00f8c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Feb 2015 16:59:50 +0100 Subject: [PATCH 1055/1609] Add Bitbucket importer. --- CHANGELOG | 2 + Gemfile | 1 + Gemfile.lock | 5 + .../images/authbuttons/bitbucket_32.png | Bin 0 -> 2713 bytes .../images/authbuttons/bitbucket_64.png | Bin 0 -> 2163 bytes .../import/bitbucket_controller.rb | 74 +++++++++++++++ app/helpers/oauth_helper.rb | 4 +- app/helpers/projects_helper.rb | 4 + app/models/project.rb | 2 +- app/models/user.rb | 1 + app/views/import/base/create.js.haml | 7 ++ app/views/import/bitbucket/status.html.haml | 44 +++++++++ app/views/import/github/status.html.haml | 2 +- app/views/import/gitlab/status.html.haml | 2 +- .../_bitbucket_import_modal.html.haml | 9 ++ app/views/projects/new.html.haml | 13 +++ app/workers/repository_import_worker.rb | 2 + config/gitlab.yml.example | 18 ++-- config/routes.rb | 5 + ...tbucket_access_token_and_secret_to_user.rb | 6 ++ db/schema.rb | 40 ++++---- doc/integration/bitbucket.m | 1 + lib/gitlab/bitbucket_import/client.rb | 88 ++++++++++++++++++ lib/gitlab/bitbucket_import/importer.rb | 52 +++++++++++ lib/gitlab/bitbucket_import/key_adder.rb | 22 +++++ .../bitbucket_import/project_creator.rb | 39 ++++++++ lib/gitlab/github_import/client.rb | 6 +- lib/gitlab/github_import/importer.rb | 2 +- lib/gitlab/gitlab_import/client.rb | 10 +- lib/gitlab/gitlab_import/importer.rb | 2 +- lib/gitlab/import_formatter.rb | 2 +- .../import/bitbucket_controller_spec.rb | 77 +++++++++++++++ .../bitbucket_import/project_creator_spec.rb | 22 +++++ .../project_creator_spec.rb} | 7 +- ...ect_creator.rb => project_creator_spec.rb} | 5 +- 35 files changed, 522 insertions(+), 54 deletions(-) create mode 100644 app/assets/images/authbuttons/bitbucket_32.png create mode 100644 app/assets/images/authbuttons/bitbucket_64.png create mode 100644 app/controllers/import/bitbucket_controller.rb create mode 100644 app/views/import/bitbucket/status.html.haml create mode 100644 app/views/projects/_bitbucket_import_modal.html.haml create mode 100644 db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb create mode 100644 doc/integration/bitbucket.m create mode 100644 lib/gitlab/bitbucket_import/client.rb create mode 100644 lib/gitlab/bitbucket_import/importer.rb create mode 100644 lib/gitlab/bitbucket_import/key_adder.rb create mode 100644 lib/gitlab/bitbucket_import/project_creator.rb create mode 100644 spec/controllers/import/bitbucket_controller_spec.rb create mode 100644 spec/lib/gitlab/bitbucket_import/project_creator_spec.rb rename spec/lib/gitlab/{github/project_creator.rb => github_import/project_creator_spec.rb} (77%) rename spec/lib/gitlab/gitlab_import/{project_creator.rb => project_creator_spec.rb} (91%) diff --git a/CHANGELOG b/CHANGELOG index d879ee85728..5a3a2ca2e24 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -72,6 +72,8 @@ v 7.8.0 - Improve database performance for GitLab - Add Asana service (Jeremy Benoist) - Improve project web hooks with extra data + - Add Bitbucket omniauth provider. + - Add Bitbucket importer. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/Gemfile b/Gemfile index 093f7bacc06..ad01b2b43e0 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,7 @@ gem 'omniauth-github' gem 'omniauth-shibboleth' gem 'omniauth-kerberos' gem 'omniauth-gitlab' +gem 'omniauth-bitbucket' gem 'doorkeeper', '2.1.0' gem "rack-oauth2", "~> 1.0.5" diff --git a/Gemfile.lock b/Gemfile.lock index 4bc47836e71..226591a50af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -338,6 +338,10 @@ GEM omniauth (1.1.4) hashie (>= 1.2, < 3) rack + omniauth-bitbucket (0.0.2) + multi_json (~> 1.7) + omniauth (~> 1.1) + omniauth-oauth (~> 1.0) omniauth-github (1.1.1) omniauth (~> 1.0) omniauth-oauth2 (~> 1.1) @@ -701,6 +705,7 @@ DEPENDENCIES nprogress-rails octokit (= 3.7.0) omniauth (~> 1.1.3) + omniauth-bitbucket omniauth-github omniauth-gitlab omniauth-google-oauth2 diff --git a/app/assets/images/authbuttons/bitbucket_32.png b/app/assets/images/authbuttons/bitbucket_32.png new file mode 100644 index 0000000000000000000000000000000000000000..27702eb973d1c3c0b9a39eee082ca72d38339cc2 GIT binary patch literal 2713 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}jKx9jP7LeL$-HD>VBjq9 zh%9Dc;1&j9Muu5)Bp4W&S7e4nltlRYSS9D@>LsS+C#C9DQ^Kd=o{)8 z=ws7Vl9`5Z9*QoI3{GvS6`44+fn*@sz<~jAqD@6^ft7E5N@iN6OJYf?osof|sjh*M zuAzB|p|O>Lk(G&wjXs(hgb%^Gor_WvOY)0C^7C`-0x~O7b8`R!J5#87WHEI0L8-<0Ii(=uL-R6A z;8vgsqw9)9$lIA4L9Ia+MAs03O#`wZk_J#jSb;+-!X+~|H4p3;VWup(yVxa71$MrGx z?PmrCwk}T>$B+olso|M9A+9q2&sOig-Mp>CJ!>P^BmrLs-z$r9UqD4)uMd;Fpg{=)-TSP7uiB2dG=N7%yTGns=I6v*X?Khp$ zN!yJ-&;MV0cFlo|#d)QiY>R^zxCm^U!Q86l;LCQTVp1nh(9zB84W+D3rH)<7bF2ei z?QZ(!`;aFlfA&e16|vzhCqBdo>8o9nF`CPgIQ7%xu+tCBZwbs&wO9RWe&VYs6JPOW z@ue3JXZ+7pnQ9Z;xN0hQLg)Gp8|i7Toi@|>K3*+dw?1g4_MI;mmA|hz;e2S3jj({y z0o$n}=O-*^imRCU**m04sb%fUol_^h=@ZazIVXQBMzx2@Ml5yfd|f{GWb3ILjyndf z*yeJw`H$HF{apbeH`~7j+)SqD$bf3Li~QSV&K3&|MM_|nHN=Su%*w6@*cC!{SG-)#H! z_r@hR4Y_7GpYTZX`!_2hI)3r4mGdT;RXq~XxzBsz21AZ|kA$1T{N96=DghGPA5L!V zh*tI4$GK$%lao=R;rxdyOt0-eb28kjh~w*7AE5&p2LvMgtv}_fH^UE$NXZ3X*!DUcBo|gPChruTJ?yl^UFQi{$iNU4fgcwVWY16^yZWX1Mb+mR* zYtx8`=zl!hD&X>tX}6t^XnKo2TC2&@XZ8H;&JRTg7<89Uk5!u1^ ze5b_O+`c(@Ao>o|UYR$o~8XSUAfnJ@2Ne!8qTiThjye}(6ZMHibk*KqHd zzq;M{+0iX|%1ylTk8Xs`JsV$9a@BWz(cXy`t~)cgGQC>UK0hW!TOfS*!{TYi6XVWo za*5YDRK@&3-ep53`_#Y62iy528riJ27%ea8z8iaN{l{bStj1zv`*RC=ibxW0fv~{oW@$Fl7=oml0!xgfY=hCOl7kB<_R;;LX zRpol4<#FL}kyfk0<)gtOK{FCG+~Klsi44ryDEG{qo++ z%x1dkE59-EkJstwmp9)B%4HmG91tBHlta< z`0Vl1bMp6FYka)x^!4m-hvZMa^}7X>_i@@JcZ;3BT+L~gw@>5CVf~ZF)lbeb7A4M< zUGj!Cb7t-f&m<$8<`C{;)@7#*uW!$CeSXb#WkR{{_a`z|XDgpSxAL1d`(4k<`I-k~ zt|nHbOr5W7I=$t|Op8-zo>?6ZnqK6y`P;NG3HQ$npNB6{xx{q*riatLI;pq+HJoCc zouf|Hhdpil`%|QXDg0jERpEP!Klo>QPKfc|T&&c*_2r~Lj~5={g=1@ zxN5t9fP&}3Y`dDjrvLs|OrNkl^LoiS*OS5JQp+aY-J|_&^6NMssTYpL#=@yzKFViA Wr6+Vw|FjX*Ui5VJb6Mw<&;$U)f?HNt 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 denied! Please verify you can add deploy keys to this repository.

    "") - else :plain job = $("tr#repo_#{@repo_id}") job.attr("id", "project_#{@project.id}") + target_field = job.find(".import-target") + target_field.empty() + target_field.append('#{link_to @project.path_with_namespace, @project}') $("table.import-jobs tbody").prepend(job) job.addClass("active").find(".import-actions").html(" started") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml new file mode 100644 index 00000000000..7a2613e4b03 --- /dev/null +++ b/app/views/import/bitbucket/status.html.haml @@ -0,0 +1,44 @@ +%h3.page-title + %i.fa.fa-bitbucket + Import repositories from Bitbucket + +%p.light + Select projects you want to import. +%hr +%p + = button_tag 'Import all projects', class: "btn btn-success js-import-all" + +%table.table.import-jobs + %thead + %tr + %th From Bitbucket + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td= project.import_source + %td + %strong= link_to project.path_with_namespace, project + %td.job-status + - if project.import_status == 'finished' + %span.cgreen + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} + %td= "#{repo["owner"]}/#{repo["slug"]}" + %td.import-target + = "#{repo["owner"]}/#{repo["slug"]}" + %td.import-actions.job-status + = button_tag "Import", class: "btn js-add-to-import" + +:coffeescript + $ -> + new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}") diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 84d9903fe15..b1538b1a41e 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,6 +1,6 @@ %h3.page-title %i.fa.fa-github - Import repositories from GitHub.com + Import repositories from GitHub %p.light Select projects you want to import. diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index d1e48dfad20..43db102994f 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -1,5 +1,5 @@ %h3.page-title - %i.fa.fa-github + %i.fa.fa-heart Import repositories from GitLab.com %p.light diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml new file mode 100644 index 00000000000..dd7aacc7e6e --- /dev/null +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -0,0 +1,9 @@ +%div#bitbucket_import_modal.modal.hide + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3 GitHub OAuth import + .modal-body + You need to setup integration with Bitbucket first. + = link_to 'How to setup integration with Bitbucket', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/bitbucket.md' \ No newline at end of file diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 5216f308110..875c092fd1a 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -53,6 +53,19 @@ Import projects from GitHub = render 'github_import_modal' + .project-import.form-group + .col-sm-2 + .col-sm-10 + - if bitbucket_import_enabled? + = link_to status_import_bitbucket_path do + %i.fa.fa-bitbucket + Import projects from Bitbucket + - else + = link_to '#', class: 'how_to_import_link light' do + %i.fa.fa-bitbucket + Import projects from Bitbucket + = render 'bitbucket_import_modal' + - unless request.host == 'gitlab.com' .project-import.form-group .col-sm-2 diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 5f9970d3795..d7e759fb470 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -14,6 +14,8 @@ class RepositoryImportWorker Gitlab::GithubImport::Importer.new(project).execute elsif project.import_type == 'gitlab' Gitlab::GitlabImport::Importer.new(project).execute + elsif project.import_type == 'bitbucket' + Gitlab::BitbucketImport::Importer.new(project).execute else true end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 044b1f66b25..6dff07cf9df 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -207,17 +207,19 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: - # - { name: 'google_oauth2', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET', + # - { name: 'google_oauth2', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET', # args: { access_type: 'offline', approval_prompt: '' } } - # - { name: 'twitter', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET'} - # - { name: 'github', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET', + # - { name: 'twitter', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET'} + # - { name: 'github', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'user:email' } } - # - { name: 'gitlab', app_id: 'YOUR APP ID', - # app_secret: 'YOUR APP SECRET', + # - { name: 'gitlab', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'api' } } + # - { name: 'bitbucket', app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET'} diff --git a/config/routes.rb b/config/routes.rb index ecd439aecea..57964bdc3b7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -68,6 +68,11 @@ Gitlab::Application.routes.draw do get :jobs end + resource :bitbucket, only: [:create, :new], controller: :bitbucket do + get :status + get :callback + get :jobs + end resource :gitorious, only: [:create, :new], controller: :gitorious do get :status get :callback diff --git a/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb new file mode 100644 index 00000000000..23ac1b399ec --- /dev/null +++ b/db/migrate/20150217123345_add_bitbucket_access_token_and_secret_to_user.rb @@ -0,0 +1,6 @@ +class AddBitbucketAccessTokenAndSecretToUser < ActiveRecord::Migration + def change + add_column :users, :bitbucket_access_token, :string + add_column :users, :bitbucket_access_token_secret, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index e11a068c9c5..8069d95c698 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150213121042) do +ActiveRecord::Schema.define(version: 20150217123345) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -410,12 +410,12 @@ ActiveRecord::Schema.define(version: 20150213121042) do end create_table "users", force: true do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" t.string "current_sign_in_ip" @@ -423,22 +423,22 @@ ActiveRecord::Schema.define(version: 20150213121042) do t.datetime "created_at" t.datetime "updated_at" t.string "name" - t.boolean "admin", default: false, null: false - t.integer "projects_limit", default: 10 - t.string "skype", default: "", null: false - t.string "linkedin", default: "", null: false - t.string "twitter", default: "", null: false + t.boolean "admin", default: false, null: false + t.integer "projects_limit", default: 10 + t.string "skype", default: "", null: false + t.string "linkedin", default: "", null: false + t.string "twitter", default: "", null: false t.string "authentication_token" - t.integer "theme_id", default: 1, null: false + t.integer "theme_id", default: 1, null: false t.string "bio" - t.integer "failed_attempts", default: 0 + t.integer "failed_attempts", default: 0 t.datetime "locked_at" t.string "username" - t.boolean "can_create_group", default: true, null: false - t.boolean "can_create_team", default: true, null: false + t.boolean "can_create_group", default: true, null: false + t.boolean "can_create_team", default: true, null: false t.string "state" - t.integer "color_scheme_id", default: 1, null: false - t.integer "notification_level", default: 1, null: false + t.integer "color_scheme_id", default: 1, null: false + t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" t.datetime "last_credential_check_at" @@ -447,13 +447,15 @@ ActiveRecord::Schema.define(version: 20150213121042) do t.datetime "confirmed_at" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" - t.boolean "hide_no_ssh_key", default: false - t.string "website_url", default: "", null: false + t.boolean "hide_no_ssh_key", default: false + t.string "website_url", default: "", null: false t.string "github_access_token" t.string "gitlab_access_token" t.string "notification_email" - t.boolean "hide_no_password", default: false - t.boolean "password_automatically_set", default: false + t.boolean "hide_no_password", default: false + t.boolean "password_automatically_set", default: false + t.string "bitbucket_access_token" + t.string "bitbucket_access_token_secret" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/integration/bitbucket.m b/doc/integration/bitbucket.m new file mode 100644 index 00000000000..30404ce4c54 --- /dev/null +++ b/doc/integration/bitbucket.m @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb new file mode 100644 index 00000000000..3d2ef78ee7d --- /dev/null +++ b/lib/gitlab/bitbucket_import/client.rb @@ -0,0 +1,88 @@ +module Gitlab + module BitbucketImport + class Client + attr_reader :consumer, :api + + def initialize(access_token = nil, access_token_secret = nil) + @consumer = ::OAuth::Consumer.new( + config.app_id, + config.app_secret, + bitbucket_options + ) + + if access_token && access_token_secret + @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret) + end + end + + def request_token(redirect_uri) + request_token = consumer.get_request_token(oauth_callback: redirect_uri) + + { + oauth_token: request_token.token, + oauth_token_secret: request_token.secret, + oauth_callback_confirmed: request_token.callback_confirmed?.to_s + } + end + + def authorize_url(request_token, redirect_uri) + request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) + + if request_token.callback_confirmed? + request_token.authorize_url + else + request_token.authorize_url(oauth_callback: redirect_uri) + end + end + + def get_token(request_token, oauth_verifier, redirect_uri) + request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash) + + if request_token.callback_confirmed? + request_token.get_access_token(oauth_verifier: oauth_verifier) + else + request_token.get_access_token(oauth_callback: redirect_uri) + end + end + + def user + JSON.parse(api.get("/api/1.0/user").body) + end + + def issues(project_identifier) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues").body) + end + + def issue_comments(project_identifier, issue_id) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) + end + + def project(project_identifier) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body) + end + + def deploy_key(project_identifier) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find { |key| key["label"] =~ /GitLab/ } + end + + def add_deploy_key(project_identifier, key) + JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body) + end + + def projects + JSON.parse(api.get("/api/1.0/user/repositories").body). + select { |repo| repo["scm"] == "git" } + end + + private + + def config + Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"} + end + + def bitbucket_options + OmniAuth::Strategies::Bitbucket.default_options[:client_options] + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb new file mode 100644 index 00000000000..42c93707caa --- /dev/null +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -0,0 +1,52 @@ +module Gitlab + module BitbucketImport + class Importer + attr_reader :project, :client + + def initialize(project) + @project = project + @client = Client.new(project.creator.bitbucket_access_token, project.creator.bitbucket_access_token_secret) + @formatter = Gitlab::ImportFormatter.new + end + + def execute + project_identifier = project.import_source + + return true unless client.project(project_identifier)["has_issues"] + + #Issues && Comments + issues = client.issues(project_identifier) + + issues["issues"].each do |issue| + body = @formatter.author_line(issue["reported_by"]["username"], issue["content"]) + + comments = client.issue_comments(project_identifier, issue["local_id"]) + + if comments.any? + body += @formatter.comments_header + end + + comments.each do |comment| + body += @formatter.comment(comment["author_info"]["username"], comment["utc_created_on"], comment["content"]) + end + + project.issues.create!( + description: body, + title: issue["title"], + state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened', + author_id: gl_user_id(project, issue["reported_by"]["username"]) + ) + end + + true + end + + private + + def gl_user_id(project, bitbucket_id) + user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s) + (user && user.id) || project.creator_id + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb new file mode 100644 index 00000000000..207811237ba --- /dev/null +++ b/lib/gitlab/bitbucket_import/key_adder.rb @@ -0,0 +1,22 @@ +module Gitlab + module BitbucketImport + class KeyAdder + attr_reader :repo, :current_user, :client + + def initialize(repo, current_user) + @repo, @current_user = repo, current_user + @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + end + + def execute + project_identifier = "#{repo["owner"]}/#{repo["slug"]}" + return true if client.deploy_key(project_identifier) + + # TODO: Point to actual public key. + client.add_deploy_key(project_identifier, File.read("/Users/douwemaan/.ssh/id_rsa.pub")) + + true + end + end + end +end diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb new file mode 100644 index 00000000000..db33af2c2da --- /dev/null +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -0,0 +1,39 @@ +module Gitlab + module BitbucketImport + class ProjectCreator + attr_reader :repo, :namespace, :current_user + + def initialize(repo, namespace, current_user) + @repo = repo + @namespace = namespace + @current_user = current_user + end + + def execute + @project = Project.new( + name: repo["name"], + path: repo["slug"], + description: repo["description"], + namespace: namespace, + creator: current_user, + visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, + import_type: "bitbucket", + import_source: "#{repo["owner"]}/#{repo["slug"]}", + import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git" + ) + + if @project.save! + @project.reload + + if @project.import_failed? + @project.import_retry + else + @project.import_start + end + end + + @project + end + end + end +end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index c9904fe8779..676d226bddd 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -46,11 +46,7 @@ module Gitlab end def github_options - { - site: 'https://api.github.com', - authorize_url: 'https://github.com/login/oauth/authorize', - token_url: 'https://github.com/login/oauth/access_token' - } + OmniAuth::Strategies::GitHub.default_options[:client_options] end end end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index bc2b645b2d9..23832b3233c 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -20,7 +20,7 @@ module Gitlab body += @formatter.comments_header client.issue_comments(project.import_source, issue.number).each do |c| - body += @formatter.comment_to_md(c.user.login, c.created_at, c.body) + body += @formatter.comment(c.user.login, c.created_at, c.body) end end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index 2206b68da99..ecf4ff94e39 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -9,7 +9,7 @@ module Gitlab @client = ::OAuth2::Client.new( config.app_id, config.app_secret, - github_options + gitlab_options ) if access_token @@ -70,12 +70,8 @@ module Gitlab Gitlab.config.omniauth.providers.find{|provider| provider.name == "gitlab"} end - def github_options - { - site: 'https://gitlab.com/', - authorize_url: 'oauth/authorize', - token_url: 'oauth/token' - } + def gitlab_options + OmniAuth::Strategies::GitLab.default_options[:client_options] end end end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index 5f9b14399a4..c5304a0699b 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -25,7 +25,7 @@ module Gitlab end comments.each do |comment| - body += @formatter.comment_to_md(comment["author"]["name"], comment["created_at"], comment["body"]) + body += @formatter.comment(comment["author"]["name"], comment["created_at"], comment["body"]) end project.issues.create!( diff --git a/lib/gitlab/import_formatter.rb b/lib/gitlab/import_formatter.rb index ebb4b87f7e3..72e041a90b1 100644 --- a/lib/gitlab/import_formatter.rb +++ b/lib/gitlab/import_formatter.rb @@ -1,6 +1,6 @@ module Gitlab class ImportFormatter - def comment_to_md(author, date, body) + def comment(author, date, body) "\n\n*By #{author} on #{date}*\n\n#{body}" end diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb new file mode 100644 index 00000000000..84e37ae5607 --- /dev/null +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Import::BitbucketController do + let(:user) { create(:user, bitbucket_access_token: 'asd123', bitbucket_access_token_secret: "sekret") } + + before do + sign_in(user) + end + + describe "GET callback" do + before do + session[:oauth_request_token] = {} + end + + it "updates access token" do + token = "asdasd12345" + secret = "sekrettt" + access_token = double(token: token, secret: secret) + Gitlab::BitbucketImport::Client.any_instance.stub(:get_token).and_return(access_token) + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") + + get :callback + + expect(user.reload.bitbucket_access_token).to eq(token) + expect(user.reload.bitbucket_access_token_secret).to eq(secret) + expect(controller).to redirect_to(status_import_bitbucket_url) + end + end + + describe "GET status" do + before do + @repo = OpenStruct.new(slug: 'vim', owner: 'asd') + end + + it "assigns variables" do + @project = create(:project, import_type: 'bitbucket', creator_id: user.id) + controller.stub_chain(:client, :projects).and_return([@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([@repo]) + end + + it "does not show already added project" do + @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim') + controller.stub_chain(:client, :projects).and_return([@repo]) + + get :status + + expect(assigns(:already_added_projects)).to eq([@project]) + expect(assigns(:repos)).to eq([]) + end + end + + describe "POST create" do + before do + @repo = { + slug: 'vim', + owner: "john" + }.with_indifferent_access + end + + it "takes already existing namespace" do + namespace = create(:namespace, name: "john", owner: user) + expect(Gitlab::BitbucketImport::KeyAdder). + to receive(:new).with(@repo, user). + and_return(double(execute: true)) + expect(Gitlab::BitbucketImport::ProjectCreator). + to receive(:new).with(@repo, namespace, user). + and_return(double(execute: true)) + controller.stub_chain(:client, :project).and_return(@repo) + + post :create, format: :js + end + end +end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb new file mode 100644 index 00000000000..f5523105848 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::BitbucketImport::ProjectCreator do + let(:user) { create(:user, bitbucket_access_token: "asdffg", bitbucket_access_token_secret: "sekret") } + let(:repo) { { + name: 'Vim', + slug: 'vim', + is_private: true, + owner: "asd"}.with_indifferent_access + } + let(:namespace){ create(:namespace) } + + it 'creates project' do + allow_any_instance_of(Project).to receive(:add_import_job) + + project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user) + project = project_creator.execute + + expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git") + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end +end diff --git a/spec/lib/gitlab/github/project_creator.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb similarity index 77% rename from spec/lib/gitlab/github/project_creator.rb rename to spec/lib/gitlab/github_import/project_creator_spec.rb index 3686ddbf170..8d594a112d4 100644 --- a/spec/lib/gitlab/github/project_creator.rb +++ b/spec/lib/gitlab/github_import/project_creator_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Github::ProjectCreator do +describe Gitlab::GithubImport::ProjectCreator do let(:user) { create(:user, github_access_token: "asdffg") } let(:repo) { OpenStruct.new( login: 'vim', @@ -15,9 +15,8 @@ describe Gitlab::Github::ProjectCreator do it 'creates project' do allow_any_instance_of(Project).to receive(:add_import_job) - project_creator = Gitlab::Github::ProjectCreator.new(repo, namespace, user) - project_creator.execute - project = Project.last + project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user) + project = project_creator.execute expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git") expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) diff --git a/spec/lib/gitlab/gitlab_import/project_creator.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb similarity index 91% rename from spec/lib/gitlab/gitlab_import/project_creator.rb rename to spec/lib/gitlab/gitlab_import/project_creator_spec.rb index e5d917830b0..4c0d64ed138 100644 --- a/spec/lib/gitlab/gitlab_import/project_creator.rb +++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::GitlabImport::ProjectCreator do let(:user) { create(:user, gitlab_access_token: "asdffg") } - let(:repo) {{ + let(:repo) { { name: 'vim', path: 'vim', visibility_level: Gitlab::VisibilityLevel::PRIVATE, @@ -16,8 +16,7 @@ describe Gitlab::GitlabImport::ProjectCreator do allow_any_instance_of(Project).to receive(:add_import_job) project_creator = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, user) - project_creator.execute - project = Project.last + project = project_creator.execute expect(project.import_url).to eq("https://oauth2:asdffg@gitlab.com/asd/vim.git") expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) -- GitLab From 448817c4de965bf7286f33a3447937987a8864a1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Feb 2015 22:52:32 +0100 Subject: [PATCH 1056/1609] Load public key in initializer. --- app/controllers/application_controller.rb | 13 +++++++++++++ app/controllers/import/bitbucket_controller.rb | 5 +++++ app/controllers/import/github_controller.rb | 5 +++++ app/controllers/import/gitlab_controller.rb | 5 +++++ app/helpers/oauth_helper.rb | 2 ++ app/helpers/projects_helper.rb | 12 ------------ config/initializers/public_key.rb | 2 ++ lib/gitlab/bitbucket_import.rb | 6 ++++++ lib/gitlab/bitbucket_import/key_adder.rb | 9 ++++++--- 9 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 config/initializers/public_key.rb create mode 100644 lib/gitlab/bitbucket_import.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index eb3be08df56..7940b5cb3f4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -16,6 +16,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :abilities, :can?, :current_application_settings + helper_method :github_import_enabled?, :gitlab_import_enabled?, :bitbucket_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -313,4 +314,16 @@ class ApplicationController < ActionController::Base set_filter_values(merge_requests) merge_requests end + + def github_import_enabled? + OauthHelper.enabled_oauth_providers.include?(:github) + end + + def gitlab_import_enabled? + OauthHelper.enabled_oauth_providers.include?(:gitlab) + end + + def bitbucket_import_enabled? + OauthHelper.enabled_oauth_providers.include?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? + end end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 27e91f49f2b..89de5c5205f 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -1,4 +1,5 @@ class Import::BitbucketController < Import::BaseController + before_filter :verify_bitbucket_import_enabled before_filter :bitbucket_auth, except: :callback # rescue_from OAuth::Error, with: :bitbucket_unauthorized @@ -55,6 +56,10 @@ class Import::BitbucketController < Import::BaseController @client ||= Gitlab::BitbucketImport::Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) end + def verify_bitbucket_import_enabled + not_found! unless bitbucket_import_enabled? + end + def bitbucket_auth if current_user.bitbucket_access_token.blank? go_to_bitbucket_for_permissions diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index c869c7c86f3..dc7668ee6fd 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -1,4 +1,5 @@ class Import::GithubController < Import::BaseController + before_filter :verify_github_import_enabled before_filter :github_auth, except: :callback rescue_from Octokit::Unauthorized, with: :github_unauthorized @@ -44,6 +45,10 @@ class Import::GithubController < Import::BaseController @client ||= Gitlab::GithubImport::Client.new(current_user.github_access_token) end + def verify_github_import_enabled + not_found! unless github_import_enabled? + end + def github_auth if current_user.github_access_token.blank? go_to_github_for_permissions diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index a51ea36aff8..74f992b4699 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -1,4 +1,5 @@ class Import::GitlabController < Import::BaseController + before_filter :verify_gitlab_import_enabled before_filter :gitlab_auth, except: :callback rescue_from OAuth2::Error, with: :gitlab_unauthorized @@ -41,6 +42,10 @@ class Import::GitlabController < Import::BaseController @client ||= Gitlab::GitlabImport::Client.new(current_user.gitlab_access_token) end + def verify_gitlab_import_enabled + not_found! unless gitlab_import_enabled? + end + def gitlab_auth if current_user.gitlab_access_token.blank? go_to_gitlab_for_permissions diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb index 848d74c18c3..1a0ad17b607 100644 --- a/app/helpers/oauth_helper.rb +++ b/app/helpers/oauth_helper.rb @@ -20,4 +20,6 @@ module OauthHelper def additional_providers enabled_oauth_providers.reject{|provider| provider.to_s.starts_with?('ldap')} end + + extend self end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 8a48a9d3946..c85ad12634d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -265,16 +265,4 @@ module ProjectsHelper "success" end end - - def github_import_enabled? - enabled_oauth_providers.include?(:github) - end - - def gitlab_import_enabled? - enabled_oauth_providers.include?(:gitlab) - end - - def bitbucket_import_enabled? - enabled_oauth_providers.include?(:bitbucket) - end end diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb new file mode 100644 index 00000000000..d27e6519d1c --- /dev/null +++ b/config/initializers/public_key.rb @@ -0,0 +1,2 @@ +path = File.expand_path("~/.ssh/id_rsa.pub") +Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path) \ No newline at end of file diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb new file mode 100644 index 00000000000..0e53972ac50 --- /dev/null +++ b/lib/gitlab/bitbucket_import.rb @@ -0,0 +1,6 @@ +module Gitlab + module BitbucketImport + mattr_accessor :public_key + @public_key = nil + end +end \ No newline at end of file diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb index 207811237ba..7d0b5fbc8ae 100644 --- a/lib/gitlab/bitbucket_import/key_adder.rb +++ b/lib/gitlab/bitbucket_import/key_adder.rb @@ -9,13 +9,16 @@ module Gitlab end def execute + return false unless BitbucketImport.public_key.present? + project_identifier = "#{repo["owner"]}/#{repo["slug"]}" return true if client.deploy_key(project_identifier) - - # TODO: Point to actual public key. - client.add_deploy_key(project_identifier, File.read("/Users/douwemaan/.ssh/id_rsa.pub")) + + client.add_deploy_key(project_identifier, BitbucketImport.public_key) true + rescue + false end end end -- GitLab From f2b37de54ba3cb0a375fb3a03e7ffd1f18444c39 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 18 Feb 2015 08:21:30 +0100 Subject: [PATCH 1057/1609] Fix specs. --- config/initializers/public_key.rb | 2 +- lib/gitlab/bitbucket_import.rb | 2 +- spec/controllers/import/bitbucket_controller_spec.rb | 1 + spec/controllers/import/github_controller_spec.rb | 1 + spec/controllers/import/gitlab_controller_spec.rb | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb index d27e6519d1c..75d74e3625d 100644 --- a/config/initializers/public_key.rb +++ b/config/initializers/public_key.rb @@ -1,2 +1,2 @@ path = File.expand_path("~/.ssh/id_rsa.pub") -Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path) \ No newline at end of file +Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path) diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb index 0e53972ac50..7298152e7e9 100644 --- a/lib/gitlab/bitbucket_import.rb +++ b/lib/gitlab/bitbucket_import.rb @@ -3,4 +3,4 @@ module Gitlab mattr_accessor :public_key @public_key = nil end -end \ No newline at end of file +end diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 84e37ae5607..5dd4124061c 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -5,6 +5,7 @@ describe Import::BitbucketController do before do sign_in(user) + controller.stub(:bitbucket_import_enabled?).and_return(true) end describe "GET callback" do diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 3b779855d3f..b8820413406 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -5,6 +5,7 @@ describe Import::GithubController do before do sign_in(user) + controller.stub(:github_import_enabled?).and_return(true) end describe "GET callback" do diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 287aa315db5..b6b86b1bcee 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -5,6 +5,7 @@ describe Import::GitlabController do before do sign_in(user) + controller.stub(:gitlab_import_enabled?).and_return(true) end describe "GET callback" do -- GitLab From 6979b3afd50f86550e523ed66ef22fd153e6cbc8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 18 Feb 2015 17:00:26 +0100 Subject: [PATCH 1058/1609] Delete deploy key from Bitbucket after importing. --- app/workers/repository_import_worker.rb | 36 +++++++++++----------- lib/gitlab/bitbucket_import/client.rb | 19 +++++++++--- lib/gitlab/bitbucket_import/key_adder.rb | 2 -- lib/gitlab/bitbucket_import/key_deleter.rb | 23 ++++++++++++++ 4 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 lib/gitlab/bitbucket_import/key_deleter.rb diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index d7e759fb470..437640d2305 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -6,27 +6,27 @@ class RepositoryImportWorker def perform(project_id) project = Project.find(project_id) - result = gitlab_shell.send(:import_repository, + + import_result = gitlab_shell.send(:import_repository, project.path_with_namespace, project.import_url) + return project.import_fail unless import_result - result_of_data_import = if project.import_type == 'github' - Gitlab::GithubImport::Importer.new(project).execute - elsif project.import_type == 'gitlab' - Gitlab::GitlabImport::Importer.new(project).execute - elsif project.import_type == 'bitbucket' - Gitlab::BitbucketImport::Importer.new(project).execute - else - true - end + data_import_result = if project.import_type == 'github' + Gitlab::GithubImport::Importer.new(project).execute + elsif project.import_type == 'gitlab' + Gitlab::GitlabImport::Importer.new(project).execute + elsif project.import_type == 'bitbucket' + Gitlab::BitbucketImport::Importer.new(project).execute + else + true + end + return project.import_fail unless data_import_result - if result && result_of_data_import - project.import_finish - project.save - project.satellite.create unless project.satellite.exists? - project.update_repository_size - else - project.import_fail - end + project.import_finish + project.save + project.satellite.create unless project.satellite.exists? + project.update_repository_size + Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' end end diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 3d2ef78ee7d..5095e592ab7 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -61,17 +61,28 @@ module Gitlab JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body) end - def deploy_key(project_identifier) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find { |key| key["label"] =~ /GitLab/ } + def find_deploy_key(project_identifier, key) + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find { |deploy_key| + deploy_key["key"].chomp == key.chomp + } end def add_deploy_key(project_identifier, key) + deploy_key = find_deploy_key(project_identifier, key) + return if deploy_key + JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body) end + def delete_deploy_key(project_identifier, key) + deploy_key = find_deploy_key(project_identifier, key) + return unless deploy_key + + api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204" + end + def projects - JSON.parse(api.get("/api/1.0/user/repositories").body). - select { |repo| repo["scm"] == "git" } + JSON.parse(api.get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } end private diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb index 7d0b5fbc8ae..9931aa7e029 100644 --- a/lib/gitlab/bitbucket_import/key_adder.rb +++ b/lib/gitlab/bitbucket_import/key_adder.rb @@ -12,8 +12,6 @@ module Gitlab return false unless BitbucketImport.public_key.present? project_identifier = "#{repo["owner"]}/#{repo["slug"]}" - return true if client.deploy_key(project_identifier) - client.add_deploy_key(project_identifier, BitbucketImport.public_key) true diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb new file mode 100644 index 00000000000..1a24a86fc37 --- /dev/null +++ b/lib/gitlab/bitbucket_import/key_deleter.rb @@ -0,0 +1,23 @@ +module Gitlab + module BitbucketImport + class KeyDeleter + attr_reader :project, :current_user, :client + + def initialize(project) + @project = project + @current_user = project.creator + @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + end + + def execute + return false unless BitbucketImport.public_key.present? + + client.delete_deploy_key(project.import_source, BitbucketImport.public_key) + + true + rescue + false + end + end + end +end -- GitLab From a938b3eeb4684f5747f63c23385f870ccbf43d2d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 18 Feb 2015 17:00:39 +0100 Subject: [PATCH 1059/1609] Link to original repo on import status pages. --- app/views/import/bitbucket/status.html.haml | 6 ++++-- app/views/import/github/status.html.haml | 9 +++++++-- app/views/import/gitlab/status.html.haml | 11 ++++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 7a2613e4b03..cb8c29259cf 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -17,7 +17,8 @@ %tbody - @already_added_projects.each do |project| %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td= project.import_source + %td + = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank" %td %strong= link_to project.path_with_namespace, project %td.job-status @@ -33,7 +34,8 @@ - @repos.each do |repo| %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"} - %td= "#{repo["owner"]}/#{repo["slug"]}" + %td + = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank" %td.import-target = "#{repo["owner"]}/#{repo["slug"]}" %td.import-actions.job-status diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index b1538b1a41e..dc8ec5e7ae0 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -17,7 +17,8 @@ %tbody - @already_added_projects.each do |project| %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td= project.import_source + %td + = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank" %td %strong= link_to project.path_with_namespace, project %td.job-status @@ -25,12 +26,16 @@ %span.cgreen %i.fa.fa-check done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started - else = project.human_import_status_name - @repos.each do |repo| %tr{id: "repo_#{repo.id}"} - %td= repo.full_name + %td + = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank" %td.import-target = repo.full_name %td.import-actions.job-status diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 43db102994f..841e660b08f 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -12,12 +12,13 @@ %thead %tr %th From GitLab.com - %th To GitLab private instance + %th To this GitLab instance %th Status %tbody - @already_added_projects.each do |project| %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td= project.import_source + %td + = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" %td %strong= link_to project.path_with_namespace, project %td.job-status @@ -25,12 +26,16 @@ %span.cgreen %i.fa.fa-check done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started - else = project.human_import_status_name - @repos.each do |repo| %tr{id: "repo_#{repo["id"]}"} - %td= repo["path_with_namespace"] + %td + = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" %td.import-target = repo["path_with_namespace"] %td.import-actions.job-status -- GitLab From 3fde1dce1f9058d4b57d17eac55051fb174c6aa4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 18 Feb 2015 18:10:22 +0100 Subject: [PATCH 1060/1609] Satisfy Rubocop. --- lib/gitlab/bitbucket_import/client.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 5095e592ab7..c907bebaef6 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -62,9 +62,9 @@ module Gitlab end def find_deploy_key(project_identifier, key) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find { |deploy_key| + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| deploy_key["key"].chomp == key.chomp - } + end end def add_deploy_key(project_identifier, key) -- GitLab From 20691df230332022cb4d5008d84c7ee6e6c8dbfd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 18 Feb 2015 22:42:52 +0100 Subject: [PATCH 1061/1609] Add Bitbucket integration docs. --- app/views/import/bitbucket/status.html.haml | 2 +- app/views/import/github/status.html.haml | 2 +- app/views/import/gitlab/status.html.haml | 2 +- .../_bitbucket_import_modal.html.haml | 10 +- .../projects/_github_import_modal.html.haml | 10 +- .../projects/_gitlab_import_modal.html.haml | 10 +- doc/integration/bitbucket.m | 1 - doc/integration/bitbucket.md | 97 +++++++++++++++++++ doc/integration/github.md | 29 +++--- doc/integration/gitlab.md | 18 ++-- doc/integration/google.md | 14 +-- doc/integration/omniauth.md | 1 + doc/integration/twitter.md | 14 +-- 13 files changed, 163 insertions(+), 47 deletions(-) delete mode 100644 doc/integration/bitbucket.m create mode 100644 doc/integration/bitbucket.md diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index cb8c29259cf..90c97393b51 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -1,6 +1,6 @@ %h3.page-title %i.fa.fa-bitbucket - Import repositories from Bitbucket + Import projects from Bitbucket %p.light Select projects you want to import. diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index dc8ec5e7ae0..957022f382f 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,6 +1,6 @@ %h3.page-title %i.fa.fa-github - Import repositories from GitHub + Import projects from GitHub %p.light Select projects you want to import. diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 841e660b08f..db161681206 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -1,6 +1,6 @@ %h3.page-title %i.fa.fa-heart - Import repositories from GitLab.com + Import projects from GitLab.com %p.light Select projects you want to import. diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml index dd7aacc7e6e..5c52f91927d 100644 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -3,7 +3,11 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3 GitHub OAuth import + %h3 Import projects from Bitbucket .modal-body - You need to setup integration with Bitbucket first. - = link_to 'How to setup integration with Bitbucket', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/bitbucket.md' \ No newline at end of file + To enable importing projects from Bitbucket, + - if current_user.admin? + you need to + - else + your GitLab administrator needs to + == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/butbucket.md'}. \ No newline at end of file diff --git a/app/views/projects/_github_import_modal.html.haml b/app/views/projects/_github_import_modal.html.haml index 99325e66119..e88a0f7d689 100644 --- a/app/views/projects/_github_import_modal.html.haml +++ b/app/views/projects/_github_import_modal.html.haml @@ -3,7 +3,11 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3 GitHub OAuth import + %h3 Import projects from GitHub .modal-body - You need to setup integration with GitHub first. - = link_to 'How to setup integration with GitHub', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/github.md' \ No newline at end of file + To enable importing projects from GitHub, + - if current_user.admin? + you need to + - else + your GitLab administrator needs to + == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/github.md'}. \ No newline at end of file diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml index e7503f023b1..52212b6ae02 100644 --- a/app/views/projects/_gitlab_import_modal.html.haml +++ b/app/views/projects/_gitlab_import_modal.html.haml @@ -3,7 +3,11 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3 GitLab OAuth import + %h3 Import projects from GitLab.com .modal-body - You need to setup integration with GitLab first. - = link_to 'How to setup integration with GitLab', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/gitlab.md' \ No newline at end of file + To enable importing projects from GitLab.com, + - if current_user.admin? + you need to + - else + your GitLab administrator needs to + == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/gitlab.md'}. \ No newline at end of file diff --git a/doc/integration/bitbucket.m b/doc/integration/bitbucket.m deleted file mode 100644 index 30404ce4c54..00000000000 --- a/doc/integration/bitbucket.m +++ /dev/null @@ -1 +0,0 @@ -TODO \ No newline at end of file diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md new file mode 100644 index 00000000000..9f24ad8c58d --- /dev/null +++ b/doc/integration/bitbucket.md @@ -0,0 +1,97 @@ +# Integrate your server with Bitbucket + +Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account. + +To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket. +Bitbucket will generate an application ID and secret key for you to use. + +1. Sign in to Bitbucket. + +1. Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you. + +1. Select "OAuth" in the left menu. + +1. Select "Add consumer". + +1. Provide the required details. + - Name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. + - Application description: Fill this in if you wish. + - URL: The URL to your GitLab installation. 'https://gitlab.company.com' +1. Select "Save". + +1. You should now see a Key and Secret in the list of OAuth customers. + Keep this page open as you continue configuration. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For instalations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "bitbucket", + "app_id" => "YOUR_KEY", + "app_secret" => "YOUR_APP_SECRET", + "url" => "https://bitbucket.org/" + } + ] + ``` + + For installation from source: + + ``` + - { name: 'bitbucket', app_id: 'YOUR_KEY', + app_secret: 'YOUR_APP_SECRET' } + ``` + +1. Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7. + +1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7. + +1. Save the configuration file. + +1. Restart GitLab for the changes to take effect. + +On the sign in page there should now be a Bitbucket icon below the regular sign in form. +Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application. +If everything goes well the user will be returned to GitLab and will be signed in. + +## Bitbucket project import + +To allow projects to be imported directly into GitLab, Bitbucket requires one extra setup step compared to GitHub and GitLab.com. + +Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key. + +GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/id_rsa.pub`, which will expand to `/home/git/.ssh/id_rsa.pub` in most configurations. + +If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: + +1. Create a new SSH key: + + ```sh + sudo -u git -H ssh-keygen + ``` + + Make sure to use an **empty passphrase**. + +2. Restart GitLab to allow it to find the new public key. + +You should now see the "Import projects from Bitbucket" option on the New Project page enabled. \ No newline at end of file diff --git a/doc/integration/github.md b/doc/integration/github.md index 137d7e9d632..a9f1bc31bb4 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -1,6 +1,9 @@ -# GitHub OAuth2 OmniAuth Provider +# Integrate your server with GitHub -To enable the GitHub OmniAuth provider you must register your application with GitHub. GitHub will generate a client ID and secret key for you to use. +Import projects from GitHub and login to your GitLab instance with your GitHub account. + +To enable the GitHub OmniAuth provider you must register your application with GitHub. +GitHub will generate an application ID and secret key for you to use. 1. Sign in to GitHub. @@ -17,7 +20,9 @@ To enable the GitHub OmniAuth provider you must register your application with G - Authorization callback URL: 'https://gitlab.company.com/' 1. Select "Register application". -1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). Keep this page open as you continue configuration. ![GitHub app](github_app.png) +1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). + Keep this page open as you continue configuration. + ![GitHub app](github_app.png) 1. On your GitLab server, open the configuration file. @@ -35,7 +40,7 @@ To enable the GitHub OmniAuth provider you must register your application with G sudo -u git -H editor config/gitlab.yml ``` -1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for inital settings. +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. 1. Add the provider configuration: @@ -45,8 +50,8 @@ To enable the GitHub OmniAuth provider you must register your application with G gitlab_rails['omniauth_providers'] = [ { "name" => "github", - "app_id" => "YOUR APP ID", - "app_secret" => "YOUR APP SECRET", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET", "url" => "https://github.com/", "args" => { "scope" => "user:email" } } } @@ -56,17 +61,19 @@ To enable the GitHub OmniAuth provider you must register your application with G For installation from source: ``` - - { name: 'github', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET', + - { name: 'github', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', args: { scope: 'user:email' } } ``` -1. Change 'YOUR APP ID' to the client ID from the GitHub application page from step 7. +1. Change 'YOUR_APP_ID' to the client ID from the GitHub application page from step 7. -1. Change 'YOUR APP SECRET' to the client secret from the GitHub application page from step 7. +1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7. 1. Save the configuration file. 1. Restart GitLab for the changes to take effect. -On the sign in page there should now be a GitHub icon below the regular sign in form. Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. +On the sign in page there should now be a GitHub icon below the regular sign in form. +Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application. +If everything goes well the user will be returned to GitLab and will be signed in. \ No newline at end of file diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index 87400bed5b5..49ffaa62af8 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -3,7 +3,7 @@ Import projects from GitLab.com and login to your GitLab instance with your GitLab.com account. To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com. -GitLab.com will generate a application ID and secret key for you to use. +GitLab.com will generate an application ID and secret key for you to use. 1. Sign in to GitLab.com @@ -46,7 +46,7 @@ GitLab.com will generate a application ID and secret key for you to use. sudo -u git -H editor config/gitlab.yml ``` -1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for inital settings. +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. 1. Add the provider configuration: @@ -56,8 +56,8 @@ GitLab.com will generate a application ID and secret key for you to use. gitlab_rails['omniauth_providers'] = [ { "name" => "gitlab", - "app_id" => "YOUR APP ID", - "app_secret" => "YOUR APP SECRET", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET", "args" => { "scope" => "api" } } } ] @@ -66,14 +66,14 @@ GitLab.com will generate a application ID and secret key for you to use. For installations from source: ``` - - { name: 'gitlab', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET', + - { name: 'gitlab', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', args: { scope: 'api' } } ``` -1. Change 'YOUR APP ID' to the Application ID from the GitLab application page. +1. Change 'YOUR_APP_ID' to the Application ID from the GitLab.com application page. -1. Change 'YOUR APP SECRET' to the secret from the GitLab application page. +1. Change 'YOUR_APP_SECRET' to the secret from the GitLab.com application page. 1. Save the configuration file. @@ -81,4 +81,4 @@ GitLab.com will generate a application ID and secret key for you to use. On the sign in page there should now be a GitLab.com icon below the regular sign in form. Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application. -If everything goes well the user will be returned to your GitLab instance and will be signed in. +If everything goes well the user will be returned to your GitLab instance and will be signed in. \ No newline at end of file diff --git a/doc/integration/google.md b/doc/integration/google.md index 168077c2770..d7b741ece69 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -43,7 +43,7 @@ To enable the Google OAuth2 OmniAuth provider you must register your application sudo -u git -H editor config/gitlab.yml ``` -1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings. +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. 1. Add the provider configuration: @@ -53,8 +53,8 @@ To enable the Google OAuth2 OmniAuth provider you must register your application gitlab_rails['omniauth_providers'] = [ { "name" => "google_oauth2", - "app_id" => "YOUR APP ID", - "app_secret" => "YOUR APP SECRET", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET", "args" => { "access_type" => "offline", "approval_prompt" => '' } } } ] @@ -63,14 +63,14 @@ To enable the Google OAuth2 OmniAuth provider you must register your application For installations from source: ``` - - { name: 'google_oauth2', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET', + - { name: 'google_oauth2', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', args: { access_type: 'offline', approval_prompt: '' } } ``` -1. Change 'YOUR APP ID' to the client ID from the Google Developer page from step 10. +1. Change 'YOUR_APP_ID' to the client ID from the Google Developer page from step 10. -1. Change 'YOUR APP SECRET' to the client secret from the Google Developer page from step 10. +1. Change 'YOUR_APP_SECRET' to the client secret from the Google Developer page from step 10. 1. Save the configuration file. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index c92fa3ee4b7..24f7b4bb4b4 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -70,6 +70,7 @@ Now we can choose one or more of the Supported Providers below to continue confi ## Supported Providers - [GitHub](github.md) +- [Bitbucket](bitbucket.md) - [GitLab.com](gitlab.md) - [Google](google.md) - [Shibboleth](shibboleth.md) diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index 2d517b2fbc9..fe9091ad9a8 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -47,7 +47,7 @@ To enable the Twitter OmniAuth provider you must register your application with sudo -u git -H editor config/gitlab.yml ``` -1. See [Initial OmniAuth Configuration](README.md#initial-omniauth-configuration) for inital settings. +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. 1. Add the provider configuration: @@ -57,8 +57,8 @@ To enable the Twitter OmniAuth provider you must register your application with gitlab_rails['omniauth_providers'] = [ { "name" => "twitter", - "app_id" => "YOUR APP ID", - "app_secret" => "YOUR APP SECRET" + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET" } ] ``` @@ -66,13 +66,13 @@ To enable the Twitter OmniAuth provider you must register your application with For installations from source: ``` - - { name: 'twitter', app_id: 'YOUR APP ID', - app_secret: 'YOUR APP SECRET' } + - { name: 'twitter', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET' } ``` -1. Change 'YOUR APP ID' to the API key from Twitter page in step 11. +1. Change 'YOUR_APP_ID' to the API key from Twitter page in step 11. -1. Change 'YOUR APP SECRET' to the API secret from the Twitter page in step 11. +1. Change 'YOUR_APP_SECRET' to the API secret from the Twitter page in step 11. 1. Save the configuration file. -- GitLab From bc80efb1fd2978a328fdc89a3164f60bcf7ed604 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 21 Feb 2015 11:07:35 +0100 Subject: [PATCH 1062/1609] Bring Gitorious import page in line with others. --- app/views/import/gitorious/status.html.haml | 11 ++++++++--- app/views/projects/new.html.haml | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 35ed0a717de..c7617ca43df 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -1,6 +1,6 @@ %h3.page-title %i.fa.fa-gitorious - Import repositories from Gitorious.org + Import projects from Gitorious %p.light Select projects you want to import. @@ -17,7 +17,8 @@ %tbody - @already_added_projects.each do |project| %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td= project.import_source + %td + = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank" %td %strong= link_to project.path_with_namespace, project %td.job-status @@ -25,12 +26,16 @@ %span.cgreen %i.fa.fa-check done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started - else = project.human_import_status_name - @repos.each do |repo| %tr{id: "repo_#{repo.id}"} - %td= repo.full_name + %td + = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank" %td.import-target = repo.full_name %td.import-actions.job-status diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 875c092fd1a..f3d166ffb8f 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -85,7 +85,7 @@ .col-sm-10 = link_to new_import_gitorious_path do %i.fa.fa-heart - Import projects from Gitorious.org + Import projects from Gitorious %hr.prepend-botton-10 -- GitLab From 16c767814a921ab0d7ad3c551bb439a9e270f7b7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 21 Feb 2015 11:08:05 +0100 Subject: [PATCH 1063/1609] Re-enable rescuing from Bitbucket OAuth errors. --- app/controllers/import/bitbucket_controller.rb | 2 +- config/routes.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 89de5c5205f..83ebc5fddca 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -2,7 +2,7 @@ class Import::BitbucketController < Import::BaseController before_filter :verify_bitbucket_import_enabled before_filter :bitbucket_auth, except: :callback - # rescue_from OAuth::Error, with: :bitbucket_unauthorized + rescue_from OAuth::Error, with: :bitbucket_unauthorized def callback request_token = session.delete(:oauth_request_token) diff --git a/config/routes.rb b/config/routes.rb index 57964bdc3b7..450895cbdb7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -73,6 +73,7 @@ Gitlab::Application.routes.draw do get :callback get :jobs end + resource :gitorious, only: [:create, :new], controller: :gitorious do get :status get :callback -- GitLab From fcd3e626b085f6d16ff2c780093bafa898647a85 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 21 Feb 2015 11:08:30 +0100 Subject: [PATCH 1064/1609] Move CHANGELOG entry. --- CHANGELOG | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5a3a2ca2e24..726a415e800 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,8 @@ v 7.9.0 (unreleased) - Improve error messages for file edit failures - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. + - Add Bitbucket omniauth provider. + - Add Bitbucket importer. v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. @@ -72,8 +74,6 @@ v 7.8.0 - Improve database performance for GitLab - Add Asana service (Jeremy Benoist) - Improve project web hooks with extra data - - Add Bitbucket omniauth provider. - - Add Bitbucket importer. v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch -- GitLab From 46bcf40b2cbc818837c855bebaef1b621e7c5283 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 15:08:42 +0100 Subject: [PATCH 1065/1609] Add ".org" back to Gitorious mentions. --- app/views/import/gitorious/status.html.haml | 4 ++-- app/views/projects/new.html.haml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index c7617ca43df..7f1456fef55 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -1,6 +1,6 @@ %h3.page-title %i.fa.fa-gitorious - Import projects from Gitorious + Import projects from Gitorious.org %p.light Select projects you want to import. @@ -11,7 +11,7 @@ %table.table.import-jobs %thead %tr - %th From Gitorious + %th From Gitorious.org %th To GitLab %th Status %tbody diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index f3d166ffb8f..875c092fd1a 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -85,7 +85,7 @@ .col-sm-10 = link_to new_import_gitorious_path do %i.fa.fa-heart - Import projects from Gitorious + Import projects from Gitorious.org %hr.prepend-botton-10 -- GitLab From 769d1ce64bdb7c9fd3981c65bc25dd14eb176c1e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 16:10:55 +0100 Subject: [PATCH 1066/1609] Fix spec. --- spec/controllers/projects/uploads_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index 8774ee0a841..029f48b2d7a 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -25,7 +25,7 @@ describe Projects::UploadsController do context 'with valid image' do before do post :create, - namespace_id: project.namespace.to_param + namespace_id: project.namespace.to_param, project_id: project.to_param, file: jpg, format: :json -- GitLab From 1bf9fa8c7fc027b5273c143949e57eac9bef52a4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 16:28:23 +0100 Subject: [PATCH 1067/1609] Exclude forks from profile contributions list. --- app/controllers/users_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4c2fe4c3c8d..8a13394dbac 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -6,7 +6,9 @@ class UsersController < ApplicationController def show @contributed_projects = Project. where(id: authorized_projects_ids & @user.contributed_projects_ids). - in_group_namespace.includes(:namespace) + in_group_namespace. + includes(:namespace). + reject(&:forked?) @projects = @user.personal_projects. where(id: authorized_projects_ids).includes(:namespace) -- GitLab From ea31726781296efa9c2493c3f01aa62ca77b45fe Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Tue, 24 Feb 2015 09:06:59 -0800 Subject: [PATCH 1068/1609] Added a margin and a couple styles Added a background, rather than an outline, which should reduce clutter on the screen. --- app/assets/stylesheets/sections/events.scss | 10 ++++++++-- app/helpers/events_helper.rb | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/sections/events.scss b/app/assets/stylesheets/sections/events.scss index b7614513216..a477359dc88 100644 --- a/app/assets/stylesheets/sections/events.scss +++ b/app/assets/stylesheets/sections/events.scss @@ -184,6 +184,12 @@ } } -.event_filter li a { - padding: 5px 10px; +.event_filter { + + li a { + padding: 5px 10px; + background: rgba(0,0,0,0.045); + margin-left: 4px; + } + } diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index db0d4a26611..063916a8df8 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -30,7 +30,7 @@ module EventsHelper end content_tag :li, class: "filter_icon #{active}" do - link_to request.path, class: 'btn has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do + link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => tooltip do icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip) end end -- GitLab From f7c948223d000e4488d864d50f3292a6c7afeaf7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Feb 2015 09:08:34 -0800 Subject: [PATCH 1069/1609] Fix access to attachments uploaded with 'Choose file' button for public access --- app/controllers/files_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/files_controller.rb b/app/controllers/files_controller.rb index 9671245d3f4..a130bcba9c9 100644 --- a/app/controllers/files_controller.rb +++ b/app/controllers/files_controller.rb @@ -1,4 +1,6 @@ class FilesController < ApplicationController + skip_before_filter :authenticate_user!, :reject_blocked + def download note = Note.find(params[:id]) uploader = note.attachment -- GitLab From 5179c5830bb3d8602eb25cc00f53be697c6010ac Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 16:28:40 +0100 Subject: [PATCH 1070/1609] Contributed projects either have user pushes or created MRs. --- app/models/user.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 21ccc76978e..0c133f0e1e0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -618,9 +618,10 @@ class User < ActiveRecord::Base def contributed_projects_ids Event.where(author_id: self). where("created_at > ?", Time.now - 1.year). - code_push. + where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)", + pushed: Event::PUSHED, created: Event::CREATED). reorder(project_id: :desc). - select('DISTINCT(project_id)'). - map(&:project_id) + select(:project_id). + uniq end end -- GitLab From 5b6d6bbc195c438de0babb9a1f0b6971bbd5673e Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Tue, 24 Feb 2015 09:42:35 -0800 Subject: [PATCH 1071/1609] Added square caret to Back to Settings button This was done to show difference between Open/Close toggle, which was using the same arrow as Back to Settings button previously. --- app/views/layouts/nav/_project.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 15b489c7d99..ef31537b84e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -2,7 +2,7 @@ - if @project_settings_nav = nav_link do = link_to namespace_project_path(@project.namespace, @project), title: 'Back to project', class: "" do - %i.fa.fa-angle-left + %i.fa.fa-caret-square-o-left %span Back to project -- GitLab From 9c4337e58bf8a5e2c9085a1f4eff0e942d659d2d Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Tue, 24 Feb 2015 10:01:41 -0800 Subject: [PATCH 1072/1609] Fixed tests Tests expected specific string capitalization for headers. --- features/steps/profile/notifications.rb | 2 +- features/steps/profile/profile.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb index df96dddd06e..13e93618eb7 100644 --- a/features/steps/profile/notifications.rb +++ b/features/steps/profile/notifications.rb @@ -7,6 +7,6 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps end step 'I should see global notifications settings' do - page.should have_content "Notifications settings" + page.should have_content "Notifications Settings" end end diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index a907b0b7dcf..3cba2ae1008 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -3,7 +3,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps include SharedPaths step 'I should see my profile info' do - page.should have_content "Profile settings" + page.should have_content "Profile Settings" end step 'I change my profile info' do -- GitLab From 1e42cd2040c788774734e6621b426b3d99362123 Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Tue, 24 Feb 2015 10:09:38 -0800 Subject: [PATCH 1073/1609] Added a warning class to button Because changing username largely affects the application, it should not be green, but be more of a warning change, which is why it's now orange. --- app/views/profiles/accounts/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 248c9137ca5..268e652027a 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -57,7 +57,7 @@ %p.light = user_url(@user) %div - = f.submit 'Save username', class: "btn btn-create" + = f.submit 'Save username', class: "btn btn-warning" - if show_profile_remove_tab? %fieldset.remove-account -- GitLab From 53e404144c8ce9f602ec1e3a642d5d3971dc8217 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 24 Feb 2015 20:24:30 +0200 Subject: [PATCH 1074/1609] changelog && documentation --- CHANGELOG | 1 + doc/workflow/web_editor.md | 4 ++-- doc/workflow/web_editor/edit_file.png | Bin 99624 -> 89039 bytes doc/workflow/web_editor/new_file.png | Bin 100516 -> 85526 bytes 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d879ee85728..0186c034692 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 7.9.0 (unreleased) - Improve error messages for file edit failures - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. + - Save web edit in new branch v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md index bcadf5e8c0d..7fc8f96b9ec 100644 --- a/doc/workflow/web_editor.md +++ b/doc/workflow/web_editor.md @@ -10,7 +10,7 @@ to create the first file and open it in the web editor. ![web editor 1](web_editor/empty_project.png) -Fill in a file name, some content, a commit message and press the commit button. +Fill in a file name, some content, a commit message, branch name and press the commit button. The file will be saved to the repository. ![web editor 2](web_editor/new_file.png) @@ -21,6 +21,6 @@ viewing the file. ![web editor 3](web_editor/show_file.png) Editing a file is almost the same as creating a new file, -with as addition the ability to preview your changes in a separate tab. +with as addition the ability to preview your changes in a separate tab. Also you can save your change to another branch by filling out field `branch` ![web editor 3](web_editor/edit_file.png) diff --git a/doc/workflow/web_editor/edit_file.png b/doc/workflow/web_editor/edit_file.png index 1522c50b62fcf5d4fd1c3d0abdc1232e8e09ca98..f480c69ac3eafbbfd85b30ca0b1324d824f89409 100644 GIT binary patch literal 89039 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

    `IWbI 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 U1kZI*3=9kmp00i_>zopr0HgdG00000 diff --git a/doc/workflow/web_editor/new_file.png b/doc/workflow/web_editor/new_file.png index 80941f37cea0c50d4e16f9df0460cd0fd705b36d..55ebd9e025720ed108f16d5b6d61926701510c10 100644 GIT binary patch literal 85526 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 J7ham(>V?N<`{-XpF6=^JpW(P;xk^;6Fj^2 zzIJ)}N&mm;?*At?wq-xn^#5d|kzG95kJU@+=Uj(@C8%@8Xa5e3}v4xl(0M zVT!h5Z^+IKs}60ftX%Kyey{eVdXz4&nCO!0?0+Ae-Cg1yvBs!(Vd|ozm6qGjFT8hY z!S(ik1z|?po%PiH%eidR1MXB-?%H)YYo+yKC+oT4+?!qZ?V5Be_DxaRWLw`KM=!RwIcvd)(dz=1po9|(6hFeP;w%B{S`J{gFh$_{Hf9B`$<sgzoszQIwq>2yFAXbJ z*6sBoQ++lTSZ%D`=GT4$HmC7lL}A^B^JUAgam-QPu<%L3;+L?B{`6#J*r(OH z_q1=7`*E3pyGx*BtGl@b!egeiH*@^xZLmpO_0}`EE;v&Qc7R%*a6w(czS~A>%8+qR zh_(;XjE}hXXJ)~bas1fpkdUz8>Z^J29$ywmcs*RoWq)ev|J&VFnH&GzE}9JTtOJw? zXMK|CF?DMA@5sJ&VxN>=_RHJ9+sSSf`()SjqK+Mpf~I&h!)F~X2(mnS^yt*7Q(9VD zHS4ZBalO&l8FJ7%x~cN^y_Y*oSLmHOE#{%wsZ#{ zWIkh7!=35sDk=#N%NUP2?wmV?r@p4qT8RDjepdbfuYa2=mN#6py5w^y;$Ebk7i|Bh z9;ZO~jw7x~>%}-i&ibCcbz}L92_MchK4y3Mp3u{K-E4+QugL_H$tKgTi)~UC^|>eY ziS4C_=_1IK!acPE78?24f~SgqGR-YzEcM-X&gyOb);nwME|@?5^!bqwi`6uTwcoBS z_`NRKRBJ1!*|Eo`;UAZNwm?@gE7M-*lA_Ngx6(?#M8!Dz&FOMDE;wPkbMIL##qE0> zt^C%m-~{=r;XCJr_=mN*PMvz;3L)YXI!gRG=zhu-9n24W@w8}uwH(};9q#Wrt)+6T+li$1HMzChL_+1pp(= zZX*VyK~oS6g{`ZTmLScRFfgVwKH^Fj+y)+agrqQs1EGwMxWbQmz}7y1%>bRRbWSM; z#g5fXM?}|kO+-3xmXT!+Ylrq6*BeMvW(?m?Jzu~MgAxxwQ(roV%#iKa0h+>H zCGir)bCC@@4n?-)qSz7Iu;UQn0Ng!c=Q{=lMqN)A#}I`FtEe8Bj_c+U)s|`@u6#@P#!T3J<)Qj)?BN5;X<30T>i{??L6yxw3^J&_DrAZTy(TP}r)! zRV)>}V;NEs9I$5X(B88-7CyrQ3jKYcSr+fLu8~N=^}&zv5!d@1ad;L3B`tkUf$$$e q*@0n5VYml$P&H^$4AqJM<^QJ%Ge})1u3}(dVDNPHb6Mw<&;$VZGSo%@ literal 100516 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

    Bgj+?tE6$n_|!QTn_TTyGHGrd*ZaTNh@Vny?11n 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ⅈw0TqI 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 Date: Tue, 24 Feb 2015 20:46:52 +0200 Subject: [PATCH 1075/1609] update gitlab-linguist --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 093f7bacc06..afe4ad4dda3 100644 --- a/Gemfile +++ b/Gemfile @@ -50,7 +50,7 @@ gem 'gitlab_omniauth-ldap', '1.2.0', require: "omniauth-ldap" gem 'gollum-lib', '~> 4.0.0' # Language detection -gem "gitlab-linguist", "~> 3.0.0", require: "linguist" +gem "gitlab-linguist", "~> 3.0.1", require: "linguist" # API gem "grape", "~> 0.6.1" diff --git a/Gemfile.lock b/Gemfile.lock index 4bc47836e71..602e8f0afd8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -190,7 +190,7 @@ GEM diff-lcs (~> 1.1) mime-types (~> 1.15) posix-spawn (~> 0.3) - gitlab-linguist (3.0.0) + gitlab-linguist (3.0.1) charlock_holmes (~> 0.6.6) escape_utils (~> 0.2.4) mime-types (~> 1.19) @@ -669,7 +669,7 @@ DEPENDENCIES github-markup gitlab-flowdock-git-hook (~> 0.4.2) gitlab-grack (~> 2.0.0.pre) - gitlab-linguist (~> 3.0.0) + gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.0.1.1) gitlab_git (= 7.0.0.rc14) gitlab_meta (= 7.0) -- GitLab From 75048fcd6917cfc25795bcca0929d442f9c65be4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Feb 2015 11:46:42 -0800 Subject: [PATCH 1076/1609] Revert "Update charlock_holmes to 0.7.3" This reverts commit c217860ae74381d75a4932fabb4417ac08b014b4. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 602e8f0afd8..7af8f7abb6c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,7 +78,7 @@ GEM json (>= 1.7) celluloid (0.16.0) timers (~> 4.0.0) - charlock_holmes (0.7.3) + charlock_holmes (0.6.9.4) cliver (0.3.2) coderay (1.1.0) coercible (1.0.0) -- GitLab From c6e9d14ceb03d4f1cf2d3a720f1e64c41d3c43b8 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Tue, 24 Feb 2015 20:30:30 +0100 Subject: [PATCH 1077/1609] Update version sorter to 2.0.0, fixes #8572 --- Gemfile.lock | 2 +- spec/helpers/application_helper_spec.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index dc0285255cc..e28a577c091 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -608,7 +608,7 @@ GEM raindrops (~> 0.7) unicorn-worker-killer (0.4.2) unicorn (~> 4) - version_sorter (1.1.0) + version_sorter (2.0.0) virtus (1.0.1) axiom-types (~> 0.0.5) coercible (~> 1.0) diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 9d99b6e33cb..6abf5b98138 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -192,10 +192,12 @@ describe ApplicationHelper do it 'sorts tags in a natural order' do # Stub repository.tag_names to make sure we get some valid testing data expect(@project.repository).to receive(:tag_names). - and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v1.0.9a']) + and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v1.0.9a', + 'v2.0-rc1', 'v2.0rc2']) expect(options[1][1]). - to eq(['v3.1.4.2', 'v2.0', 'v1.0.10', 'v1.0.9a', 'v1.0.9']) + to eq(['v3.1.4.2', 'v2.0', 'v2.0rc2', 'v2.0-rc1', 'v1.0.10', 'v1.0.9', + 'v1.0.9a']) end end -- GitLab From 56f51bed6b166f12b8b0f4f1bd883142c5e079a6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 00:04:48 +0100 Subject: [PATCH 1078/1609] Expand Bitbucket integration docs. --- doc/integration/bitbucket.md | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index 9f24ad8c58d..cc6389f5aaf 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -76,22 +76,46 @@ If everything goes well the user will be returned to GitLab and will be signed i ## Bitbucket project import -To allow projects to be imported directly into GitLab, Bitbucket requires one extra setup step compared to GitHub and GitLab.com. +To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com. Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key. -GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/id_rsa.pub`, which will expand to `/home/git/.ssh/id_rsa.pub` in most configurations. +### Step 1: Known hosts + +To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so: + +1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use: + + ```sh + ssh git@bitbucket.org + ``` + +1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter): + + ```sh + The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. + RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. + Are you sure you want to continue connecting (yes/no)? + ``` + +1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. + +1. Your GitLab server is now able to connect to Bitbucket over SSH. Continue to step 2: + +### Step 2: Public key + +To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/id_rsa.pub`, which will expand to `/home/git/.ssh/id_rsa.pub` in most configurations. If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: 1. Create a new SSH key: ```sh - sudo -u git -H ssh-keygen + sudo -u git -H ssh-keygen ``` Make sure to use an **empty passphrase**. 2. Restart GitLab to allow it to find the new public key. -You should now see the "Import projects from Bitbucket" option on the New Project page enabled. \ No newline at end of file +You should now see the "Import projects from Bitbucket" option on the New Project page enabled. -- GitLab From 4aeb9165660348d862172965238c366afaca05d5 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 24 Feb 2015 16:53:59 -0800 Subject: [PATCH 1079/1609] Update changelog for 7.8.1 --- CHANGELOG | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f67b45c0582..7a5e2c2fc78 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,13 @@ v 7.9.0 (unreleased) - Save web edit in new branch - Fix ordering of imported but unchanged projects (Marco Wessel) +v 7.8.1 + - Fix run of custom post receive hooks + - Fix migration that caused issues when upgrading to version 7.8 from versions prior to 7.3 + - Fix the warning for LDAP users about need to set password + - Fix avatars which were not shown for non logged in users + - Fix urls for the issues when relative url was enabled + v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) @@ -40,7 +47,7 @@ v 7.8.0 - Allow configuring protection of the default branch upon first push (Marco Wessel) - Add gitlab.com importer - Add an ability to login with gitlab.com - - Add a commit calendar to the user profile (Hannes Rosenögger) + - Add a commit calendar to the user profile (Hannes Rosenögger) - Submit comment on command-enter - Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`. - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" @@ -55,7 +62,7 @@ v 7.8.0 - API: Access groups with their path (Julien Bianchi) - Added link to milestone and keeping resource context on smaller viewports for issues and merge requests (Jason Blanchard) - Allow notification email to be set separately from primary email. - - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) + - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) - Don't have Markdown preview fail for long comments/wiki pages. - When test web hook - show error message instead of 500 error page if connection to hook url was reset - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) -- GitLab From f43c0429c25dff42aec395a24057eb8d37ff449d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Feb 2015 21:31:54 -0800 Subject: [PATCH 1080/1609] Improve admin projects and users pages for mobile devices --- CHANGELOG | 1 + app/assets/stylesheets/generic/mobile.scss | 1 + app/views/admin/projects/index.html.haml | 6 ++++-- app/views/admin/users/index.html.haml | 6 ++++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7a5e2c2fc78..90591684757 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 7.9.0 (unreleased) - Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev) - Save web edit in new branch - Fix ordering of imported but unchanged projects (Marco Wessel) + - Mobile UI improvements: make aside content expandable v 7.8.1 - Fix run of custom post receive hooks diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index 2bb69f4aa7e..b3727c33672 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -69,5 +69,6 @@ background: #EEE; font-size: 20px; color: #777; + z-index: 100; @include box-shadow(0 1px 2px #DDD); } diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 0f9cdfc9e8e..3780500a447 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,5 +1,7 @@ .row - .col-md-3 + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left + %aside.col-md-3 .admin-filter = form_tag admin_namespaces_projects_path, method: :get, class: '' do .form-group @@ -36,7 +38,7 @@ = button_tag "Search", class: "btn submit btn-primary" = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" - .col-md-9 + %section.col-md-9 .panel.panel-default .panel-heading Projects (#{@projects.total_count}) diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 6e15cec467b..4a4f0549ada 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,5 +1,7 @@ .row - .col-md-3 + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left + %aside.col-md-3 .admin-filter %ul.nav.nav-pills.nav-stacked %li{class: "#{'active' unless params[:filter]}"} @@ -27,7 +29,7 @@ %hr = link_to 'Reset', admin_users_path, class: "btn btn-cancel" - .col-md-9 + %section.col-md-9 .panel.panel-default .panel-heading Users (#{@users.total_count}) -- GitLab From 6fe057cc7b4ec4c7483422ea0fe2b4eb28e315df Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Feb 2015 21:40:47 -0800 Subject: [PATCH 1081/1609] Fix header avatar size --- app/assets/stylesheets/sections/header.scss | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss index e255cbcada8..363bc2e9a24 100644 --- a/app/assets/stylesheets/sections/header.scss +++ b/app/assets/stylesheets/sections/header.scss @@ -86,7 +86,7 @@ header { .container { width: 100% !important; - padding-left: 0px; + padding: 0px; } /** @@ -134,14 +134,13 @@ header { } .profile-pic { - position: relative; - top: -1px; - padding-right: 0px !important; + padding: 0px !important; + width: 46px; + height: 46px; + margin-left: 5px; img { - width: 50px; - height: 50px; - margin: -15px; - margin-left: 5px; + width: 46px; + height: 46px; } } -- GitLab From 1faf3676aa023395c468d4e89224726c8e5b9b7d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Feb 2015 22:19:14 -0800 Subject: [PATCH 1082/1609] Refactor gitlab themes css --- app/assets/stylesheets/sections/header.scss | 62 ----------------- app/assets/stylesheets/themes/dark-theme.scss | 69 +++++++++++++++++++ app/assets/stylesheets/themes/ui_color.scss | 40 +---------- app/assets/stylesheets/themes/ui_gray.scss | 30 +------- app/assets/stylesheets/themes/ui_mars.scss | 36 +--------- app/assets/stylesheets/themes/ui_modern.scss | 40 +---------- 6 files changed, 77 insertions(+), 200 deletions(-) create mode 100644 app/assets/stylesheets/themes/dark-theme.scss diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss index 363bc2e9a24..28fbe03ee75 100644 --- a/app/assets/stylesheets/sections/header.scss +++ b/app/assets/stylesheets/sections/header.scss @@ -173,68 +173,6 @@ header { @include transition(all 0.15s ease-in 0s); } } - - - /* - * Dark header - * - */ - &.header-dark { - &.navbar-gitlab { - .navbar-inner { - background: #708090; - border-bottom: 1px solid #AAA; - - .navbar-toggle { color: #fff; } - - .nav > li > a { - color: #AAA; - - &:hover, &:focus, &:active { - background: none; - color: #FFF; - } - } - } - } - - .turbolink-spinner { - color: #FFF; - } - - .search { - .search-input { - background-color: #D2D5DA; - background-color: rgba(255, 255, 255, 0.5); - border: 1px solid #AAA; - - &:focus { - background-color: white; - } - } - } - .search-input::-webkit-input-placeholder { - color: #666; - } - .app_logo { - a { - h1 { - background: image-url('logo-white.png') no-repeat center center; - background-size: 32px; - color: #fff; - } - } - } - .title { - a { - color: #FFF; - &:hover { - text-decoration: underline; - } - } - color: #fff; - } - } } .search .search-input { diff --git a/app/assets/stylesheets/themes/dark-theme.scss b/app/assets/stylesheets/themes/dark-theme.scss new file mode 100644 index 00000000000..abb1ba6686d --- /dev/null +++ b/app/assets/stylesheets/themes/dark-theme.scss @@ -0,0 +1,69 @@ +@mixin dark-theme($color-light, $color, $color-darker, $color-dark) { + header { + &.navbar-gitlab { + .navbar-inner { + background: $color; + + .navbar-toggle { + color: #FFF; + } + + .app_logo, .navbar-toggle { + &:hover { + background-color: $color-darker; + } + + h1 { + background: image-url('logo-white.png') no-repeat center center; + background-size: 32px; + color: #FFF; + } + } + + .app_logo { + background-color: $color-dark; + } + + .title { + color: #FFF; + + a { + color: #FFF; + &:hover { + text-decoration: underline; + } + } + } + + .search { + .search-input { + background-color: $color-light; + background-color: rgba(255, 255, 255, 0.5); + border: 1px solid $color-light; + + &:focus { + background-color: white; + } + } + } + + .search-input::-webkit-input-placeholder { + color: #666; + } + + .nav > li > a { + color: $color-light; + + &:hover, &:focus, &:active { + background: none; + color: #FFF; + } + } + + .search-input { + border-color: $color-light; + } + } + } + } +} diff --git a/app/assets/stylesheets/themes/ui_color.scss b/app/assets/stylesheets/themes/ui_color.scss index 3c441a8e098..7ac6903b2e4 100644 --- a/app/assets/stylesheets/themes/ui_color.scss +++ b/app/assets/stylesheets/themes/ui_color.scss @@ -1,42 +1,6 @@ /** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - * Next items should be placed there - * - link colors - * - header restyles - * + * Violet GitLab UI theme */ .ui_color { - /* - * Application Header - * - */ - header { - @extend .header-dark; - &.navbar-gitlab { - .navbar-inner { - background: #548; - border-bottom: 1px solid #436; - .app_logo, .navbar-toggle { - &:hover { - background-color: #436; - } - } - .app_logo { - background-color: #325; - } - .nav > li > a { - color: #98C; - } - .search-input { - border-color: #98C; - } - } - } - } - - .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { - background: #659; - } + @include dark-theme(#98C, #548, #436, #325); } diff --git a/app/assets/stylesheets/themes/ui_gray.scss b/app/assets/stylesheets/themes/ui_gray.scss index 8df08ccaeec..9257e5f4d40 100644 --- a/app/assets/stylesheets/themes/ui_gray.scss +++ b/app/assets/stylesheets/themes/ui_gray.scss @@ -1,32 +1,6 @@ /** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - * Next items should be placed there - * - link colors - * - header restyles - * + * Gray GitLab UI theme */ .ui_gray { - /* - * Application Header - * - */ - header { - @extend .header-dark; - &.navbar-gitlab { - .navbar-inner { - background: #373737; - border-bottom: 1px solid #272727; - .app_logo, .navbar-toggle { - &:hover { - background-color: #272727; - } - } - .app_logo { - background-color: #222; - } - } - } - } + @include dark-theme(#979797, #373737, #272727, #222222); } diff --git a/app/assets/stylesheets/themes/ui_mars.scss b/app/assets/stylesheets/themes/ui_mars.scss index b08cbda6c4f..4caf5843d9b 100644 --- a/app/assets/stylesheets/themes/ui_mars.scss +++ b/app/assets/stylesheets/themes/ui_mars.scss @@ -1,38 +1,6 @@ /** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - * Next items should be placed there - * - link colors - * - header restyles - * + * Classic GitLab UI theme */ .ui_mars { - /* - * Application Header - * - */ - header { - @extend .header-dark; - &.navbar-gitlab { - .navbar-inner { - background: #474D57; - border-bottom: 1px solid #373D47; - .app_logo, .navbar-toggle { - &:hover { - background-color: #373D47; - } - } - .app_logo { - background-color: #24272D; - } - .nav > li > a { - color: #979DA7; - } - .search-input { - border-color: #979DA7; - } - } - } - } + @include dark-theme(#979DA7, #474D57, #373D47, #24272D); } diff --git a/app/assets/stylesheets/themes/ui_modern.scss b/app/assets/stylesheets/themes/ui_modern.scss index 34f39614ca4..70449882317 100644 --- a/app/assets/stylesheets/themes/ui_modern.scss +++ b/app/assets/stylesheets/themes/ui_modern.scss @@ -1,42 +1,6 @@ /** - * This file represent some UI that can be changed - * during web app restyle or theme select. - * - * Next items should be placed there - * - link colors - * - header restyles - * + * Modern GitLab UI theme */ .ui_modern { - /* - * Application Header - * - */ - header { - @extend .header-dark; - &.navbar-gitlab { - .navbar-inner { - background: #019875; - border-bottom: 1px solid #019875; - .app_logo, .navbar-toggle { - &:hover { - background-color: #018865; - } - } - .app_logo { - background-color: #017855; - } - .nav > li > a { - color: #ADC; - } - .search-input { - border-color: #8ba; - } - } - } - } - - .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { - background: #019875; - } + @include dark-theme(#ADC, #019875, #018865, #017855); } -- GitLab From 6e559be6c68b921e12518816a824724564e3e315 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Feb 2015 22:58:47 -0800 Subject: [PATCH 1083/1609] Refactor header logo and setup expectation on it size --- app/assets/images/logo-black.png | Bin 2608 -> 3897 bytes app/assets/images/logo-white.png | Bin 7331 -> 7699 bytes app/assets/stylesheets/sections/header.scss | 20 +++++++----------- app/assets/stylesheets/themes/dark-theme.scss | 6 ------ app/helpers/appearances_helper.rb | 8 +++++++ app/views/layouts/_head_panel.html.haml | 2 +- .../layouts/_public_head_panel.html.haml | 4 +--- 7 files changed, 18 insertions(+), 22 deletions(-) diff --git a/app/assets/images/logo-black.png b/app/assets/images/logo-black.png index 49cdc16cacd9a6be58ad79813461554bed5db8e2..a58645ed7b0616bb2d9dec551969f782dd849e52 100644 GIT binary patch literal 3897 zcmeAS@N?(olHy`uVBq!ia0y~yVDJE84mJh`hS0a0-5D4d*pj^6T^Rm@;DWu&Co?cG za29w(7Bet#3xhBt!>lEaktaqI2u${BZ4 zOOM+>|9jWEe42{$HpSoj*iUeAItm#otzHo#vM5*4L#1hxr`(0G3){k2UA;vMSiP2Z zDR~Rd61j9`%Y=ru6G>BCcp{I#5pbHe^ZVES&wJ0EO?h`_uIgl#;(5>QKA(xLd;h-t z-sYO~-{*+;oeH$na({ZkbJ6}ZgL4l)Gy0iD2tO=UkX!WIsQH56>)EdtEI;^^Z8LL< zaD^`W%|rTArDItCh}}rqAozo`fUUrXU2EBy2N4f=n!S0eWY)AYg)kkLtht=f>$vt{ zRkJ)(?Wgm#V z`!ZB9hd(;M!*rLSO-g}nz~Zm3MMD%Mo4Yv5ne3VGWfy3!yXtyidxNRNy17%0`F_lD z+|IH8aQT?6bSAo{L4x)tcusWHqd3=;kpJ^kZ1Zti>};Q|(>W*(yzs z)I==<>Hdw{4f>4U$~l+Rc+WekC*&_=6w%6c#IL_#WszaJ?~J%0}HaxBJ7cgU=#v zsa;KZnDan*pZOp54~IF{dl)jl5L&qV=Y4yI-%?r{*&BSt6`XD@@NbwKdHq=%S4D#O zo|f6q-5X!BnY;E2S4$VpE?Qu9sN#WALdTiyPK62OE%r0=4|og8>F)4jnRi6AAh+ct z+i~|D0ryWbbvORzF;m&F`P|*b2X!y-|9H-!K2v?cdbf(O+4HA!CA1~ojZ)m1VP>}%NMSpKOwd~ce+tZ;Q?+J{*?SIf=T>(FJ5dXr*s z_(4hTsXG^L{FUgLDp~LM&cO5Kxvm*`^^T2E58NHno9mwG1!ix!n9z8Tb;lxO2d&UQ zRjO_Jmn?Kv8t#aknYX%f+ujokr1x1y^UUKmSlHR!%21zu;rbnIF^0=swe<`i?5lV~ zYbqApc$d}a(~zB_x}QC6a#5b>yq6jQmPa3OKIms#FSX`+afs>Sw{@FNNZm1*cVM#f z;+5MjJ*aA4&bw@;eofJblG`&v7>lP&GJkOMK{jJs__niNQQ1+c6*D4IHh6w7vF>f@ zY)ao3_U?-K5B&#*lI|Dfeas>>)`n$V^j8lM<&ulyD4%zb?d}<+=5mo^i<~uL8>M;b zc>f6QxO~T2D(bF-j0Bqv+uno%-&HfyC75iOY*=}&S1U8s7_7+SzHr)xb-gKj@AjFq zUp-U_WlR2`5~#hvx~x9qh^`mIR5p8~h&A_3dROw;t!gOW%E`uJA?k5vYB|$AjNZ za{Q7!!9IUy96I98wPL>8&joE^TjE|+iEXfM^x?}hUw?UB_dLFSm77iBjLeNMD;(p} zKj58xYw4U-+hPouZFGD-76i|D=oosQLN8oIGraNgjnS+;V=tgJ^;lfNvO&r)W- z_)Ow~pMq)~?6(%L*=S&Ob>qUhqLZfuxjme)c>lcA_Je<3JPn$h{jBD*ct`bq_b;9& zuU$Sp=Rnlmm;^q98xIUWzrVK1Y<1}J2bB-v8_SnPWuIdAil6w_UX*j%diKw+_c7Jj zY9{V|aCfq)h4>Yv?^gT~vJb8;=~PGy$Sqj^W5!vnxa_n4>krS7y4xV$biGq0yPM}i z-hVf)n`&wWldrA0TD_G~&%f)z9wvbsN9SzV@9diQ*&%T1dmF)2^CvY2lek{-*W8=K zbk_XVt3|tS=mpMjjCtSrtmWsQuu1QOTiVn)cc&cn)em26y!g{XKh9<4UzE14yTQ7+ z-t5wYxzA!}f9+kmr|rQ9qdD&TtgS3^ysTWG_xp0YJ=m#YV9r@{Ct;`EZFQr6E)UY# znxiE;&Kv$HKRPS^#lfHhvoC6Pe|ApDD_Li`)OS_a*@y{dUr%4Od=Y7KX@yQmK-9Xg zb%mGt#WwwR)yc5_y}iBoz~X7i`y|(Qg`9TjV0`B#l*jVRjeV}Ncggq5h2l3@dc2qZ zm@`GLVUxHUs1Q)Rc_;%2em)%t6!Y@#jRzsD2F`HhtNp(&6hau zt!S=&Wg9y4y_cEcy#9%$Y1StiCUI=~Ej6Py zC`hFB)Pvl+(?e&iF`F@+rEl$;CDo~IT@P$GTzvK;b+5vK?%qQ0PT`WzQ<}_I&fI?Q zL7ee1Ngj^{+t+;;J=p#B*~a25@1W$=zsBt;8eL z;-R^_w-z_HD#;$p?BBn1fsOXe=&s2*9pO*)JOH4WXS9IQ` z@>4AQWm0>%p73Zb&Uet)tP=d-(DJ`OFH_e*POCxI`jzp=*Hzaxn{*ZHGZob~T>daO zCa=J+{}<16roMd6z_fi8^`Fn3kl5>{rO+2XJ#n(op|d9&ITuRjy}SLP?TPQR!+y?Z zeuo|Yc*N+)ovT4xe2j`#hB(fc(H4Eyt5~}xM0(5oIV^t;Jy31l?8L~!QQ&mx1ploI zxe+-VR&A6>*|&NA)w6Zlh25F@nwNjgO?6AVx3x0t%kiCA@&3AD?R`SqA{D>!-Vxia z*|5ERQq`>Ua<)mX42us|EWO|De=+(R#Xd<*${U zCIvQmk6xPH%P_rHKYM9ekm0`{*BF*>nOr2KnVxj;?|1R*`zHOB?p76VU)^wCL}T3@ z8@a1n4xcUCxJ)#+OEh(%Y}H>jTjn1BSJ9_a+4i%9vAuH4@YcKGxg$fH@7AYFvK9UJ zUVl(*oz`_Mx9!o~D~FQ4%x{^OzRmUZ-ly~X%{d~NKO`=5%&l$cNuMOgcKlhqgYPB3 zW&t+lqiiC420e!90`GP5)tJr1|CD9reN1MnX10-*-SPC}p@PN}vTMI=J{QTJChjud zW_DZuJ)iD($5zi?Ts?cW@W+YIL@kOr)@`u<`lk5REvs8=rc_T~aq_ctjg7<)hoF{T zZAZ1NRY}Z#7uSh~?)DQoa>VRTm3WO&c?RRPmyCB7ZQNsg>+;m!4;kOKZusvz+wuJo z@oeArqAdr5SDpT}<*nGC|3{y(resUShcfNk`)NwyDR-9Vnjilt>#|Qjdn&@P<#Xl% z4z}Y)@{3w?Uq0ZPmvE2oo&tB&@>0Ltvo^_(BHyn+qv%;Tcgw+6Hhre=k2l|ZdoSIS z=brYN4-pmxS+DQ1EsxZ0Si*JD`@_VYuU!ixwAW2G(@#2|_2HtA4WF&!=@<9H6fbf& zOmCbYad2V3NsEXN|Gb+*RuPx-djE!~v@2`z7`yeMtV6|8&aZ z$6JK?+xpD|r?G#2dBr<);p9SYndV}@^CG+MIEov#&kvHS>u8d6?R{B$BEjc{ghHm$ zIX#mXD`yB_Ns3yUQ2%Hl!^+SDImL3nJ@=T;3%{h$-0j*R?^EubvUc-F#*j;Sj_HpA z9)@qLXS}sBX@cj2TB#Q`t2Rtko;**tY{7~t)t46-%bjQQ-f8Qta@YOT+G8h+tfbEz z-F!9a*1@&6cJ02I6|wU;=R4z%pHp8m?a?aBpPuknZsKG1xC6VTj=Mi!q}HACdI@uB ze$bPMWnLYHOjGokY?Wg8Viu}=@_b<4?(3UjASwUh&)lB@Df{Lc-+3Us?SJCrL%gqg z*>89r2>JZ(L`z!6)OBpdldAS*Dby*vPc2PgFPw2ILh*4ZTfEMSdG69N7dhOFG!=DI zpYC3)DOeoFk`*KK>gR05isfZn58fAb>OUM*a^+>&o_)XM9`y;?ht`&5ME~;V{Nj5t z^Qq(^^VKC$%e#~vOP0Oy`Wzb@XRxLqj_LBGYlf3FQ`hjH-lP}&_uCdx>xIQTx&z)6 zeO9n@-9ZxqOCU{Na{wGDmL*@W?;@ oD`aOow{+5-zb82DrZEBhP#92ybq4-f)ZM~ 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 diff --git a/app/assets/images/logo-white.png b/app/assets/images/logo-white.png index 2299153caba9c4e96ecfbc1c6e60543c79085c38..917bcfcb7e750f8426dd79912ca525dc65183a56 100644 GIT binary patch literal 7699 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 DM4tW> literal 7331 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 ".navbar-collapse", "data-toggle" => "collapse", type: "button"} diff --git a/app/views/layouts/_public_head_panel.html.haml b/app/views/layouts/_public_head_panel.html.haml index e912fea2aee..bd6bb3c720d 100644 --- a/app/views/layouts/_public_head_panel.html.haml +++ b/app/views/layouts/_public_head_panel.html.haml @@ -2,10 +2,8 @@ .navbar-inner .container %div.app_logo - %span.separator = link_to explore_root_path, class: "home" do - %h1 GITLAB - %span.separator + = brand_header_logo %h1.title= title %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"} -- GitLab From ee343661e18ccd95f2c74e7bc0d0116a100270ea Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Feb 2015 23:17:11 -0800 Subject: [PATCH 1084/1609] Get rid of black logo --- app/assets/images/logo-black.png | Bin 3897 -> 0 bytes app/assets/stylesheets/themes/ui_basic.scss | 11 +++++++++-- app/helpers/appearances_helper.rb | 6 +----- 3 files changed, 10 insertions(+), 7 deletions(-) delete mode 100644 app/assets/images/logo-black.png diff --git a/app/assets/images/logo-black.png b/app/assets/images/logo-black.png deleted file mode 100644 index a58645ed7b0616bb2d9dec551969f782dd849e52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3897 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 oD`a li > a { diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 21e8557abc7..bb8d5683807 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -16,10 +16,6 @@ module AppearancesHelper end def brand_header_logo - if theme_type == 'light_theme' - image_tag 'logo-black.png' - else - image_tag 'logo-white.png' - end + image_tag 'logo-white.png' end end -- GitLab From 878e86bf64ce09938c1f2cc4dd1555029969a7c2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Feb 2015 23:26:32 -0800 Subject: [PATCH 1085/1609] Remove unnecessary theme_type from body class --- app/views/layouts/admin.html.haml | 2 +- app/views/layouts/application.html.haml | 2 +- app/views/layouts/errors.html.haml | 2 +- app/views/layouts/explore.html.haml | 2 +- app/views/layouts/group.html.haml | 2 +- app/views/layouts/navless.html.haml | 2 +- app/views/layouts/profile.html.haml | 2 +- app/views/layouts/project_settings.html.haml | 2 +- app/views/layouts/projects.html.haml | 2 +- app/views/layouts/public_group.html.haml | 2 +- app/views/layouts/public_projects.html.haml | 2 +- app/views/layouts/public_users.html.haml | 2 +- app/views/layouts/search.html.haml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index e8751a6987e..ab84e87c300 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,6 +1,6 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Admin area" - %body{class: "#{app_theme} #{theme_type} admin", :'data-page' => body_data_page} + %body{class: "#{app_theme} admin", :'data-page' => body_data_page} = render "layouts/head_panel", title: link_to("Admin area", admin_root_path) = render 'layouts/page', sidebar: 'layouts/nav/admin' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 49123744ffa..6bd8ac4adb8 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,6 +1,6 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Dashboard" - %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page } + %body{class: "#{app_theme} application", :'data-page' => body_data_page } = render "layouts/head_panel", title: link_to("Dashboard", root_path) = render 'layouts/page', sidebar: 'layouts/nav/dashboard' diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index e7d875173e6..e51fd4cb820 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Error" - %body{class: "#{app_theme} #{theme_type} application"} + %body{class: "#{app_theme} application"} = render "layouts/head_panel", title: "" if current_user .container.navless-container = render "layouts/flash" diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml index 09855b222dc..2bd0b8d85c9 100644 --- a/app/views/layouts/explore.html.haml +++ b/app/views/layouts/explore.html.haml @@ -2,7 +2,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: page_title - %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} application", :'data-page' => body_data_page} = render "layouts/broadcast" - if current_user = render "layouts/head_panel", title: link_to(page_title, explore_root_path) diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index fa0ed317ce1..f4a6bee15f6 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,6 +1,6 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: group_head_title - %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} application", :'data-page' => body_data_page} = render "layouts/head_panel", title: link_to(@group.name, group_path(@group)) = render 'layouts/page', sidebar: 'layouts/nav/group' diff --git a/app/views/layouts/navless.html.haml b/app/views/layouts/navless.html.haml index a3b55542bf7..4d0278251a6 100644 --- a/app/views/layouts/navless.html.haml +++ b/app/views/layouts/navless.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: @title - %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} application", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/head_panel", title: defined?(@title_url) ? link_to(@title, @title_url) : @title .container.navless-container diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 19d6efed78e..2b5be7fc372 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,6 +1,6 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Profile" - %body{class: "#{app_theme} #{theme_type} profile", :'data-page' => body_data_page} + %body{class: "#{app_theme} profile", :'data-page' => body_data_page} = render "layouts/head_panel", title: link_to("Profile", profile_path) = render 'layouts/page', sidebar: 'layouts/nav/profile' diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml index d2c9c2a991c..0a0039dec16 100644 --- a/app/views/layouts/project_settings.html.haml +++ b/app/views/layouts/project_settings.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: @project.name_with_namespace - %body{class: "#{app_theme} #{theme_type} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } + %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } = render "layouts/head_panel", title: project_title(@project) = render "layouts/init_auto_complete" - @project_settings_nav = true diff --git a/app/views/layouts/projects.html.haml b/app/views/layouts/projects.html.haml index c44a40c9c12..dde0964f47f 100644 --- a/app/views/layouts/projects.html.haml +++ b/app/views/layouts/projects.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: project_head_title - %body{class: "#{app_theme} #{theme_type} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } + %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } = render "layouts/head_panel", title: project_title(@project) = render "layouts/init_auto_complete" = render 'layouts/page', sidebar: 'layouts/nav/project' diff --git a/app/views/layouts/public_group.html.haml b/app/views/layouts/public_group.html.haml index 4b69329b8fe..b9b1d03e08e 100644 --- a/app/views/layouts/public_group.html.haml +++ b/app/views/layouts/public_group.html.haml @@ -1,6 +1,6 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: group_head_title - %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} application", :'data-page' => body_data_page} = render "layouts/public_head_panel", title: link_to(@group.name, group_path(@group)) = render 'layouts/page', sidebar: 'layouts/nav/group' diff --git a/app/views/layouts/public_projects.html.haml b/app/views/layouts/public_projects.html.haml index 027e9a53139..04fa7c84e73 100644 --- a/app/views/layouts/public_projects.html.haml +++ b/app/views/layouts/public_projects.html.haml @@ -1,6 +1,6 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: @project.name_with_namespace - %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} application", :'data-page' => body_data_page} = render "layouts/public_head_panel", title: project_title(@project) = render 'layouts/page', sidebar: 'layouts/nav/project' diff --git a/app/views/layouts/public_users.html.haml b/app/views/layouts/public_users.html.haml index 3538a8b1699..71c16bd1684 100644 --- a/app/views/layouts/public_users.html.haml +++ b/app/views/layouts/public_users.html.haml @@ -1,6 +1,6 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: @title - %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} application", :'data-page' => body_data_page} = render "layouts/public_head_panel", title: defined?(@title_url) ? link_to(@title, @title_url) : @title = render 'layouts/page' diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml index 177e2073a0d..f9d8db06e10 100644 --- a/app/views/layouts/search.html.haml +++ b/app/views/layouts/search.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en"} = render "layouts/head", title: "Search" - %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} + %body{class: "#{app_theme} application", :'data-page' => body_data_page} = render "layouts/broadcast" = render "layouts/head_panel", title: link_to("Search", search_path) .container.navless-container -- GitLab From 9b8d5c4ab4e5dc544891f37fa6dd91134577e313 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 10:49:51 +0100 Subject: [PATCH 1086/1609] Make test element selection more specific. --- features/steps/project/merge_requests.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index d358f1d875f..263f2ef2438 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -213,7 +213,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see a comment like "Line is wrong" in the second file' do - within '.files [id^=diff]:nth-child(2) .note-text' do + within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do page.should have_visible_content "Line is wrong" end end @@ -225,7 +225,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see a comment like "Line is wrong here" in the second file' do - within '.files [id^=diff]:nth-child(2) .note-text' do + within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do page.should have_visible_content "Line is wrong here" end end @@ -238,7 +238,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_button "Add Comment" end - within ".files [id^=diff]:nth-child(1) .note-text" do + within ".files [id^=diff]:nth-child(1) .note-body > .note-text" do page.should have_content "Line is correct" end end -- GitLab From 9c6f0487950f1b510d7222885ed8591607b4fe9b Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 25 Feb 2015 12:18:15 +0200 Subject: [PATCH 1087/1609] Fix GitLab importer. Hide already imported projects --- app/controllers/import/gitlab_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index a51ea36aff8..c18178abf76 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -16,7 +16,7 @@ class Import::GitlabController < Import::BaseController @already_added_projects = current_user.created_projects.where(import_type: "gitlab") already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos.to_a.reject!{ |repo| already_added_projects_names.include? repo["path_with_namespace"] } + @repos = @repos.to_a.reject{ |repo| already_added_projects_names.include? repo["path_with_namespace"] } end def jobs -- GitLab From 87da9185ff3b973afd0676ed375e7aa98a0fb233 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 12:22:05 +0100 Subject: [PATCH 1088/1609] Autosave title and description of new issues/MRs. --- app/assets/javascripts/dispatcher.js.coffee | 4 +++ .../javascripts/issuable_form.js.coffee | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 app/assets/javascripts/issuable_form.js.coffee diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index ed1bdd6ca33..591a3749a93 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -33,12 +33,16 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() new ZenMode() new DropzoneInput($('.issue-form')) + if page == 'projects:issues:new' + new IssuableForm($('.issue-form')) when 'projects:merge_requests:new', 'projects:merge_requests:edit' GitLab.GfmAutoComplete.setup() new Diff() shortcut_handler = new ShortcutsNavigation() new ZenMode() new DropzoneInput($('.merge-request-form')) + if page == 'projects:merge_requests:new' + new IssuableForm($('.merge-request-form')) when 'projects:merge_requests:show' new Diff() shortcut_handler = new ShortcutsIssueable() diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee new file mode 100644 index 00000000000..abd58bcf978 --- /dev/null +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -0,0 +1,28 @@ +class @IssuableForm + constructor: (@form) -> + @titleField = @form.find("input[name*='[title]']") + @descriptionField = @form.find("textarea[name*='[description]']") + + return unless @titleField.length && @descriptionField.length + + @initAutosave() + + @form.on "submit", @resetAutosave + @form.on "click", ".btn-cancel", @resetAutosave + + initAutosave: -> + new Autosave @titleField, [ + document.location.pathname, + document.location.search, + "title" + ] + + new Autosave @descriptionField, [ + document.location.pathname, + document.location.search, + "description" + ] + + resetAutosave: => + @titleField.data("autosave").reset() + @descriptionField.data("autosave").reset() -- GitLab From 607f0c05feb2cde82493a36ac43ba5ecd6c71620 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 17:43:28 +0100 Subject: [PATCH 1089/1609] Change EmailsOnPush subject to include namespace, repo and branch. See #1827. --- app/mailers/emails/projects.rb | 9 ++++++--- spec/mailers/notify_spec.rb | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 4bc40b35f2d..f2e599ab28b 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -23,21 +23,24 @@ module Emails @commits = Commit.decorate(compare.commits) @diffs = compare.diffs @branch = branch + + @subject = "[#{@project.path_with_namespace}][#{@branch}] " + if @commits.length > 1 @target_url = namespace_project_compare_url(@project.namespace, @project, from: @commits.first, to: @commits.last) - @subject = "#{@commits.length} new commits pushed to repository" + @subject << "#{@commits.length} commits: #{@commits.first.title}" else @target_url = namespace_project_commit_url(@project.namespace, @project, @commits.first) - @subject = @commits.first.title + @subject << @commits.first.title end mail(from: sender(author_id), to: recipient, - subject: subject(@subject)) + subject: @subject) end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 3b09c618f2a..ae2b61262bd 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -583,7 +583,7 @@ describe Notify do end it 'has the correct subject' do - is_expected.to have_subject /#{commits.length} new commits pushed to repository/ + is_expected.to have_subject /\[#{project.path_with_namespace}\]\[master\] #{commits.length} commits:/ end it 'includes commits list' do -- GitLab From 6afb03ee96a2f0c36e69a6da4e10dbe298c5b79f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 17:44:13 +0100 Subject: [PATCH 1090/1609] Remove incorrect footer from EmailsOnPush body. See #1754. --- app/mailers/emails/projects.rb | 2 ++ app/views/layouts/notify.html.haml | 2 +- spec/mailers/notify_spec.rb | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index f2e599ab28b..f3a2ae14d35 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -38,6 +38,8 @@ module Emails @subject << @commits.first.title end + @disable_footer = true + mail(from: sender(author_id), to: recipient, subject: @subject) diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index 8cca80e5248..eb5da470160 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -27,5 +27,5 @@ - if @target_url #{link_to "View it on GitLab", @target_url} = email_action @target_url - - if @project + - if @project && !@disable_footer You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} project team. diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ae2b61262bd..41b0daacded 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -597,6 +597,10 @@ describe Notify do it 'contains a link to the diff' do is_expected.to have_body_text /#{diff_path}/ end + + it 'doesn not contain the misleading footer' do + is_expected.not_to have_body_text /you are a member of/ + end end describe 'email on push with a single commit' do -- GitLab From 7b34c9dc593c15ed60f397b1e43e34bab8702674 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 17:55:23 +0100 Subject: [PATCH 1091/1609] Add list of changed files to EmailsOnPush. See #1906. --- app/views/layouts/notify.html.haml | 9 ++++ .../notify/repository_push_email.html.haml | 44 +++++++++++++++---- .../notify/repository_push_email.text.haml | 23 +++++++--- app/views/projects/diffs/_stats.html.haml | 2 +- 4 files changed, 64 insertions(+), 14 deletions(-) diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index eb5da470160..cd23fd3c280 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -16,6 +16,15 @@ font-size:small; color:#777 } + .file-stats a { + text-decoration: none; + } + .file-stats .new-file { + color: #090; + } + .file-stats .deleted-file { + color: #B00; + } #{add_email_highlight_css} %body %div.content diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index a45d1dedcd1..28b87812bcc 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -11,16 +11,44 @@ %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} %pre #{commit.safe_message} +%h4 #{pluralize @diffs.count, "changed file"}: + +%ul + - @diffs.each_with_index do |diff, i| + %li.file-stats + %a{href: "#diff-#{i}"} + - if diff.deleted_file + %span.deleted-file + − + = diff.old_path + - elsif diff.renamed_file + = diff.old_path + → + = diff.new_path + - elsif diff.new_file + %span.new-file + + + = diff.new_path + - else + = diff.new_path + %h4 Changes: -- @diffs.each do |diff| - %li - %strong - - if diff.old_path == diff.new_path - = diff.new_path - - elsif diff.new_path && diff.old_path - #{diff.old_path} → #{diff.new_path} +- @diffs.each_with_index do |diff, i| + %li{id: "diff-#{i}"} + %a{href: @target_url + "#diff-#{i}"} + - if diff.deleted_file + %strong + = diff.old_path + deleted + - elsif diff.renamed_file + %strong + = diff.old_path + → + %strong + = diff.new_path - else - = diff.new_path || diff.old_path + %strong + = diff.new_path %hr %pre = color_email_diff(diff.diff) diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index fa355cb5269..8ff7a8a99ea 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -8,16 +8,29 @@ Commits: \- - - - - \ \ +#{pluralize @diffs.count, "changed file"}: +\ +- @diffs.each do |diff| + - if diff.deleted_file + \- − #{diff.old_path} + - elsif diff.renamed_file + \- #{diff.old_path} → #{diff.new_path} + - elsif diff.new_file + \- + #{diff.new_path} + - else + \- #{diff.new_path} +\ +\ Changes: - @diffs.each do |diff| \ \===================================== - - if diff.old_path == diff.new_path - = diff.new_path - - elsif diff.new_path && diff.old_path - #{diff.old_path} → #{diff.new_path} + - if diff.deleted_file + #{diff.old_path} deleted + - elsif diff.renamed_file + #{diff.old_path} → #{diff.new_path} - else - = diff.new_path || diff.old_path + = diff.new_path \===================================== != diff.diff \ diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 20e51d18da5..9b5eb84a86d 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -26,7 +26,7 @@ %a{href: "#diff-#{i}"} %i.fa.fa-minus = diff.old_path - \-> + → = diff.new_path - elsif diff.new_file %span.new-file -- GitLab From 0e7d1fd44f057c83c5384618f4599271f9fdd006 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 14:05:03 +0100 Subject: [PATCH 1092/1609] Add optional title field to service properties. --- app/views/admin/services/_form.html.haml | 3 ++- app/views/projects/services/_form.html.haml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 5df8849317b..7394925f01e 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -16,6 +16,7 @@ - @service.fields.each do |field| - name = field[:name] + - title = field[:title] || name.humanize - value = @service.send(name) unless field[:type] == 'password' - type = field[:type] - placeholder = field[:placeholder] @@ -23,7 +24,7 @@ - default_choice = field[:default_choice] .form-group - = f.label name, class: "control-label" + = f.label name, title, class: "control-label" .col-sm-10 - if type == 'text' = f.text_field name, class: "form-control", placeholder: placeholder diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 8db6d67e06b..8008fa2b4b8 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -29,6 +29,7 @@ - @service.fields.each do |field| - name = field[:name] + - title = field[:title] || name.humanize - value = @service.send(name) unless field[:type] == 'password' - type = field[:type] - placeholder = field[:placeholder] @@ -36,7 +37,7 @@ - default_choice = field[:default_choice] .form-group - = f.label name, class: "control-label" + = f.label name, title, class: "control-label" .col-sm-10 - if type == 'text' = f.text_field name, class: "form-control", placeholder: placeholder -- GitLab From e0c186c35735a2dc9e05e88ad9975ae016c815d9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 14:05:45 +0100 Subject: [PATCH 1093/1609] Add option to send EmailsOnPush from committer email if domain matches. See #1809. --- app/controllers/admin/services_controller.rb | 3 +- .../projects/services_controller.rb | 3 +- app/mailers/emails/projects.rb | 6 ++-- app/mailers/notify.rb | 7 ++++- .../emails_on_push_service.rb | 8 ++++- app/workers/emails_on_push_worker.rb | 4 +-- spec/mailers/notify_spec.rb | 30 ++++++++++++++++++- 7 files changed, 51 insertions(+), 10 deletions(-) diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index e80cabd6e18..88106b2418a 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -45,7 +45,8 @@ class Admin::ServicesController < Admin::ApplicationController :room, :recipients, :project_url, :webhook, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch + :description, :issues_url, :new_issue_url, :restrict_to_branch, + :send_from_committer_email ]) end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 5c29a6550f5..dd3987605e3 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -50,7 +50,8 @@ class Projects::ServicesController < Projects::ApplicationController :room, :recipients, :project_url, :webhook, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch + :description, :issues_url, :new_issue_url, :restrict_to_branch, + :send_from_committer_email ) end end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index f3a2ae14d35..3b60aed6f9e 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -16,13 +16,13 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, author_id, branch, compare) + def repository_push_email(project_id, recipient, author_id, branch, compare, send_from_committer_email = false) @project = Project.find(project_id) @author = User.find(author_id) @compare = compare @commits = Commit.decorate(compare.commits) @diffs = compare.diffs - @branch = branch + @branch = branch.gsub("refs/heads/", "") @subject = "[#{@project.path_with_namespace}][#{@branch}] " @@ -40,7 +40,7 @@ module Emails @disable_footer = true - mail(from: sender(author_id), + mail(from: sender(author_id, send_from_committer_email), to: recipient, subject: @subject) end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 46ead62f75f..00d609cd93c 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -45,10 +45,15 @@ class Notify < ActionMailer::Base # Return an email address that displays the name of the sender. # Only the displayed name changes; the actual email address is always the same. - def sender(sender_id) + def sender(sender_id, send_from_user_email = false) if sender = User.find(sender_id) address = default_sender_address address.display_name = sender.name + + if send_from_user_email && sender.email.end_with?("@#{Gitlab.config.gitlab.host}") + address.address = sender.email + end + address.format end end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 86693ad0c7e..a5653665bfb 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -14,6 +14,7 @@ # class EmailsOnPushService < Service + prop_accessor :send_from_committer_email prop_accessor :recipients validates :recipients, presence: true, if: :activated? @@ -29,12 +30,17 @@ class EmailsOnPushService < Service 'emails_on_push' end + def send_from_committer_email? + self.send_from_committer_email == "1" + end + def execute(push_data) - EmailsOnPushWorker.perform_async(project_id, recipients, push_data) + EmailsOnPushWorker.perform_async(project_id, recipients, push_data, self.send_from_committer_email?) end def fields [ + { type: 'checkbox', name: 'send_from_committer_email', title: "Send from committer email if domain matches" }, { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' }, ] end diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index e3f6f3a6aef..3814b17a8a2 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,7 +1,7 @@ class EmailsOnPushWorker include Sidekiq::Worker - def perform(project_id, recipients, push_data) + def perform(project_id, recipients, push_data, send_from_committer_email = false) project = Project.find(project_id) before_sha = push_data["before"] after_sha = push_data["after"] @@ -19,7 +19,7 @@ class EmailsOnPushWorker return false unless compare && compare.commits.present? recipients.split(" ").each do |recipient| - Notify.repository_push_email(project_id, recipient, author_id, branch, compare).deliver + Notify.repository_push_email(project_id, recipient, author_id, branch, compare, send_from_committer_email).deliver end ensure compare = nil diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 41b0daacded..ad2b7c11f84 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -569,8 +569,9 @@ describe Notify do let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits) } let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: commits.first, to: commits.last) } + let(:send_from_committer_email) { false } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare, send_from_committer_email) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -601,6 +602,33 @@ describe Notify do it 'doesn not contain the misleading footer' do is_expected.not_to have_body_text /you are a member of/ end + + context "when set to send from committer email if domain matches" do + + let(:send_from_committer_email) { true } + + context "when the committer email domain matches" do + + before do + allow(Gitlab.config.gitlab).to receive(:host).and_return("gitlab.dev") + user.update_attribute(:email, "user@#{Gitlab.config.gitlab.host}") + user.confirm! + end + + it "is sent from the committer email" do + sender = subject.header[:from].addrs[0] + expect(sender.address).to eq(user.email) + end + end + + context "when the committer email doesn't match" do + + it "is sent from the default email" do + sender = subject.header[:from].addrs[0] + expect(sender.address).to eq(gitlab_sender) + end + end + end end describe 'email on push with a single commit' do -- GitLab From 85af3e82bfe0ebd01e816ee7c5ee6a2c28ce8ff9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 14:29:49 +0100 Subject: [PATCH 1094/1609] Add option to disable code diffs to EmailOnPush. See #1950 --- app/controllers/admin/services_controller.rb | 2 +- .../projects/services_controller.rb | 2 +- app/mailers/emails/projects.rb | 3 +- .../emails_on_push_service.rb | 8 +++- .../notify/repository_push_email.html.haml | 45 ++++++++++--------- .../notify/repository_push_email.text.haml | 30 +++++++------ app/workers/emails_on_push_worker.rb | 12 ++++- 7 files changed, 60 insertions(+), 42 deletions(-) diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 88106b2418a..44a3f1379d8 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -46,7 +46,7 @@ class Admin::ServicesController < Admin::ApplicationController :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :build_type, :description, :issues_url, :new_issue_url, :restrict_to_branch, - :send_from_committer_email + :send_from_committer_email, :disable_diffs ]) end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index dd3987605e3..b7fd5202f95 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -51,7 +51,7 @@ class Projects::ServicesController < Projects::ApplicationController :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :build_type, :description, :issues_url, :new_issue_url, :restrict_to_branch, - :send_from_committer_email + :send_from_committer_email, :disable_diffs ) end end diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 3b60aed6f9e..30959ab6a1e 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -16,13 +16,14 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, author_id, branch, compare, send_from_committer_email = false) + def repository_push_email(project_id, recipient, author_id, branch, compare, send_from_committer_email = false, disable_diffs = false) @project = Project.find(project_id) @author = User.find(author_id) @compare = compare @commits = Commit.decorate(compare.commits) @diffs = compare.diffs @branch = branch.gsub("refs/heads/", "") + @disable_diffs = disable_diffs @subject = "[#{@project.path_with_namespace}][#{@branch}] " diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index a5653665bfb..e5d6c29c645 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -15,6 +15,7 @@ class EmailsOnPushService < Service prop_accessor :send_from_committer_email + prop_accessor :disable_diffs prop_accessor :recipients validates :recipients, presence: true, if: :activated? @@ -34,13 +35,18 @@ class EmailsOnPushService < Service self.send_from_committer_email == "1" end + def disable_diffs? + self.disable_diffs == "1" + end + def execute(push_data) - EmailsOnPushWorker.perform_async(project_id, recipients, push_data, self.send_from_committer_email?) + EmailsOnPushWorker.perform_async(project_id, recipients, push_data, send_from_committer_email?, disable_diffs?) end def fields [ { type: 'checkbox', name: 'send_from_committer_email', title: "Send from committer email if domain matches" }, + { type: 'checkbox', name: 'disable_diffs', title: "Disable code diffs" }, { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' }, ] end diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 28b87812bcc..49688470cc5 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -16,7 +16,7 @@ %ul - @diffs.each_with_index do |diff, i| %li.file-stats - %a{href: "#diff-#{i}"} + %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" } - if diff.deleted_file %span.deleted-file − @@ -32,27 +32,28 @@ - else = diff.new_path -%h4 Changes: -- @diffs.each_with_index do |diff, i| - %li{id: "diff-#{i}"} - %a{href: @target_url + "#diff-#{i}"} - - if diff.deleted_file - %strong - = diff.old_path - deleted - - elsif diff.renamed_file - %strong - = diff.old_path - → - %strong - = diff.new_path - - else - %strong - = diff.new_path - %hr - %pre - = color_email_diff(diff.diff) - %br +- unless @disable_diffs + %h4 Changes: + - @diffs.each_with_index do |diff, i| + %li{id: "diff-#{i}"} + %a{href: @target_url + "#diff-#{i}"} + - if diff.deleted_file + %strong + = diff.old_path + deleted + - elsif diff.renamed_file + %strong + = diff.old_path + → + %strong + = diff.new_path + - else + %strong + = diff.new_path + %hr + %pre + = color_email_diff(diff.diff) + %br - if @compare.timeout %h5 Huge diff. To prevent performance issues changes are hidden diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 8ff7a8a99ea..b081121c53a 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -19,20 +19,22 @@ Commits: \- + #{diff.new_path} - else \- #{diff.new_path} -\ -\ -Changes: -- @diffs.each do |diff| +- unless @disable_diffs \ - \===================================== - - if diff.deleted_file - #{diff.old_path} deleted - - elsif diff.renamed_file - #{diff.old_path} → #{diff.new_path} - - else - = diff.new_path - \===================================== - != diff.diff -\ + \ + Changes: + - @diffs.each do |diff| + \ + \===================================== + - if diff.deleted_file + #{diff.old_path} deleted + - elsif diff.renamed_file + #{diff.old_path} → #{diff.new_path} + - else + = diff.new_path + \===================================== + != diff.diff - if @compare.timeout + \ + \ Huge diff. To prevent performance issues it was hidden diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 3814b17a8a2..309772cb5ce 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,7 +1,7 @@ class EmailsOnPushWorker include Sidekiq::Worker - def perform(project_id, recipients, push_data, send_from_committer_email = false) + def perform(project_id, recipients, push_data, send_from_committer_email = false, disable_diffs = false) project = Project.find(project_id) before_sha = push_data["before"] after_sha = push_data["after"] @@ -19,7 +19,15 @@ class EmailsOnPushWorker return false unless compare && compare.commits.present? recipients.split(" ").each do |recipient| - Notify.repository_push_email(project_id, recipient, author_id, branch, compare, send_from_committer_email).deliver + Notify.repository_push_email( + project_id, + recipient, + author_id, + branch, + compare, + send_from_committer_email, + disable_diffs + ).deliver end ensure compare = nil -- GitLab From ae70a80fc202822a485cabf78da9774d14055617 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 14:32:55 +0100 Subject: [PATCH 1095/1609] Fix links in EmailsOnPush text version. --- app/views/notify/repository_push_email.text.haml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index b081121c53a..7cb2814a492 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -1,9 +1,9 @@ -#{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} +#{@author.name} pushed to #{@branch} at #{@project.name_with_namespace} \ Commits: - @commits.each do |commit| - #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)} by #{commit.author_name} + #{commit.short_id} by #{commit.author_name} #{commit.safe_message} \- - - - - \ @@ -38,3 +38,6 @@ Commits: \ \ Huge diff. To prevent performance issues it was hidden +\ +\ +View it on GitLab: #{@target_url} -- GitLab From 769f137a5344dbc3748c2fea7c1d560392410ca4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 14:37:15 +0100 Subject: [PATCH 1096/1609] Wrap commit message in EmailsOnPush email. See #1867. --- app/views/layouts/notify.html.haml | 3 +++ app/views/notify/repository_push_email.html.haml | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index cd23fd3c280..7eec93abdf6 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -16,6 +16,9 @@ font-size:small; color:#777 } + pre.commit-message { + white-space: pre-wrap; + } .file-stats a { text-decoration: none; } diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 49688470cc5..1a617e2108d 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -9,7 +9,8 @@ %div %span by #{commit.author_name} %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} - %pre #{commit.safe_message} + %pre.commit-message + = commit.safe_message %h4 #{pluralize @diffs.count, "changed file"}: -- GitLab From 5d86332153838252384f9f87a0ae3e34c46eb266 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 15:12:19 +0100 Subject: [PATCH 1097/1609] Send EmailsOnPush when deleting commits using force push. See #1924. --- app/mailers/emails/projects.rb | 6 +++++- app/views/notify/repository_push_email.html.haml | 8 +++++++- app/views/notify/repository_push_email.text.haml | 10 +++++++--- app/workers/emails_on_push_worker.rb | 12 ++++++++++-- spec/mailers/notify_spec.rb | 2 +- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 30959ab6a1e..5c38601c1ba 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -16,9 +16,10 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, author_id, branch, compare, send_from_committer_email = false, disable_diffs = false) + def repository_push_email(project_id, recipient, author_id, branch, compare, reverse_compare = false, send_from_committer_email = false, disable_diffs = false) @project = Project.find(project_id) @author = User.find(author_id) + @reverse_compare = reverse_compare @compare = compare @commits = Commit.decorate(compare.commits) @diffs = compare.diffs @@ -32,10 +33,13 @@ module Emails @project, from: @commits.first, to: @commits.last) + @subject << "Deleted " if @reverse_compare @subject << "#{@commits.length} commits: #{@commits.first.title}" else @target_url = namespace_project_commit_url(@project.namespace, @project, @commits.first) + + @subject << "Deleted 1 commit: " if @reverse_compare @subject << @commits.first.title end diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 1a617e2108d..039b92df2be 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,6 +1,12 @@ %h3 #{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} -%h4 Commits: +- if @reverse_compare + %p + %strong WARNING: + The push did not contain any new commits, but force pushed to delete the commits and changes below. + +%h4 + = @reverse_compare ? "Deleted commits:" : "Commits:" %ul - @commits.each do |commit| diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 7cb2814a492..8d67a42234e 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -1,9 +1,13 @@ #{@author.name} pushed to #{@branch} at #{@project.name_with_namespace} - \ -Commits: +\ +- if @reverse_compare + WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. + \ + \ += @reverse_compare ? "Deleted commits:" : "Commits:" - @commits.each do |commit| - #{commit.short_id} by #{commit.author_name} + #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} #{commit.safe_message} \- - - - - \ diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 309772cb5ce..2e783814824 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -15,8 +15,15 @@ class EmailsOnPushWorker compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - # Do not send emails if git compare failed - return false unless compare && compare.commits.present? + return false if compare.same + + if compare.commits.empty? + compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) + + reverse_compare = true + + return false if compare.commits.empty? + end recipients.split(" ").each do |recipient| Notify.repository_push_email( @@ -25,6 +32,7 @@ class EmailsOnPushWorker author_id, branch, compare, + reverse_compare, send_from_committer_email, disable_diffs ).deliver diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index ad2b7c11f84..9af17942612 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -571,7 +571,7 @@ describe Notify do let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: commits.first, to: commits.last) } let(:send_from_committer_email) { false } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare, send_from_committer_email) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare, false, send_from_committer_email) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] -- GitLab From 00c631573f1f7564cfd8d823dce761fc6c76e2bc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 15:16:24 +0100 Subject: [PATCH 1098/1609] Fix Gitorious import status page hiding of already added projects. --- app/controllers/import/gitorious_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb index 627b4a171b8..6067a87ee04 100644 --- a/app/controllers/import/gitorious_controller.rb +++ b/app/controllers/import/gitorious_controller.rb @@ -15,7 +15,7 @@ class Import::GitoriousController < Import::BaseController @already_added_projects = current_user.created_projects.where(import_type: "gitorious") already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos.to_a.reject! { |repo| already_added_projects_names.include? repo.full_name } + @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } end def jobs -- GitLab From 969de4c15a876ab9096f163ef6182571ca199492 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 15:49:40 +0100 Subject: [PATCH 1099/1609] Fix EmailsOnPush to allow sending from @company.com for GitLab at gitlab.corp.company.com. --- app/mailers/notify.rb | 17 ++++++++++++++++- spec/mailers/notify_spec.rb | 29 +++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 00d609cd93c..65925b61e94 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -34,6 +34,20 @@ class Notify < ActionMailer::Base ) end + # Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com", + # "corp.company.com" and "company.com". + # Respects set tld length so "company.co.uk" won't match "somethingelse.uk" + def self.allowed_email_domains + domain_parts = Gitlab.config.gitlab.host.split(".") + allowed_domains = [] + begin + allowed_domains << domain_parts.join(".") + domain_parts.shift + end while domain_parts.length > ActionDispatch::Http::URL.tld_length + + allowed_domains + end + private # The default email address to send emails from @@ -50,7 +64,8 @@ class Notify < ActionMailer::Base address = default_sender_address address.display_name = sender.name - if send_from_user_email && sender.email.end_with?("@#{Gitlab.config.gitlab.host}") + sender_domain = sender.email.split("@").last + if send_from_user_email && self.class.allowed_email_domains.include?(sender_domain) address.address = sender.email end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 9af17942612..534ab05942c 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -607,11 +607,14 @@ describe Notify do let(:send_from_committer_email) { true } - context "when the committer email domain matches" do + before do + allow(Gitlab.config.gitlab).to receive(:host).and_return("gitlab.corp.company.com") + end + + context "when the committer email domain is within the GitLab domain" do before do - allow(Gitlab.config.gitlab).to receive(:host).and_return("gitlab.dev") - user.update_attribute(:email, "user@#{Gitlab.config.gitlab.host}") + user.update_attribute(:email, "user@company.com") user.confirm! end @@ -621,7 +624,25 @@ describe Notify do end end - context "when the committer email doesn't match" do + context "when the committer email domain is not completely within the GitLab domain" do + + before do + user.update_attribute(:email, "user@something.company.com") + user.confirm! + end + + it "is sent from the default email" do + sender = subject.header[:from].addrs[0] + expect(sender.address).to eq(gitlab_sender) + end + end + + context "when the committer email domain is outside the GitLab domain" do + + before do + user.update_attribute(:email, "user@mpany.com") + user.confirm! + end it "is sent from the default email" do sender = subject.header[:from].addrs[0] -- GitLab From 183f521079873c0d0942f2ad72660822714d0b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Lafoucrie=CC=80re?= Date: Wed, 25 Feb 2015 10:14:10 -0500 Subject: [PATCH 1100/1609] Bump gemnasium service gem --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index dc0285255cc..e4d43bb56b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -174,8 +174,8 @@ GEM dotenv (>= 0.7) thor (>= 0.13.6) formatador (0.2.4) - gemnasium-gitlab-service (0.2.3) - rugged (~> 0.19) + gemnasium-gitlab-service (0.2.4) + rugged (~> 0.21) gherkin-ruby (0.3.1) racc github-markup (1.3.1) @@ -489,7 +489,7 @@ GEM ruby-progressbar (1.7.1) rubyntlm (0.4.0) rubypants (0.2.0) - rugged (0.21.2) + rugged (0.21.4) rugments (1.0.0.beta3) safe_yaml (0.9.7) sanitize (2.1.0) -- GitLab From f0b78a852933a54173bb9b4ceddba44b52dc3cfa Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 15:56:42 +0100 Subject: [PATCH 1101/1609] Clarify EmailsOnPushService options. --- app/models/project_services/emails_on_push_service.rb | 7 +++++-- app/views/admin/services/_form.html.haml | 3 +++ app/views/projects/services/_form.html.haml | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index e5d6c29c645..ec0c55bfd95 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -44,9 +44,12 @@ class EmailsOnPushService < Service end def fields + domains = Notify.allowed_email_domains.map { |domain| "user@#{domain}" }.join(", ") [ - { type: 'checkbox', name: 'send_from_committer_email', title: "Send from committer email if domain matches" }, - { type: 'checkbox', name: 'disable_diffs', title: "Disable code diffs" }, + { type: 'checkbox', name: 'send_from_committer_email', title: "Send from committer", + help: "Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. #{domains})." }, + { type: 'checkbox', name: 'disable_diffs', title: "Disable code diffs", + help: "Don't include possibly sensitive code diffs in notification body." }, { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by whitespace' }, ] end diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 7394925f01e..1cd6b8e75b1 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -22,6 +22,7 @@ - placeholder = field[:placeholder] - choices = field[:choices] - default_choice = field[:default_choice] + - help = field[:help] .form-group = f.label name, title, class: "control-label" @@ -36,6 +37,8 @@ = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - elsif type == 'password' = f.password_field name, class: 'form-control' + - if help + %span.help-block= help .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 8008fa2b4b8..1b7265d56e8 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -35,6 +35,7 @@ - placeholder = field[:placeholder] - choices = field[:choices] - default_choice = field[:default_choice] + - help = field[:help] .form-group = f.label name, title, class: "control-label" @@ -49,6 +50,8 @@ = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - elsif type == 'password' = f.password_field name, class: 'form-control' + - if help + %span.help-block= help .form-actions = f.submit 'Save', class: 'btn btn-save' -- GitLab From a672b4688309dc356922bd3f9c61b8ae9de018f8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 17:34:01 +0100 Subject: [PATCH 1102/1609] Include number of affected people in all/group mention autocomplete item. --- app/services/projects/participants_service.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index 0be50fed7cc..f6f9aceef95 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -35,15 +35,21 @@ module Projects end def sorted(users) - users.uniq.to_a.compact.sort_by(&:username).map { |user| { username: user.username, name: user.name } } + users.uniq.to_a.compact.sort_by(&:username).map do |user| + { username: user.username, name: user.name } + end end def groups - @user.authorized_groups.sort_by(&:path).map { |group| { username: group.path, name: group.name } } + @user.authorized_groups.sort_by(&:path).map do |group| + count = group.users.count + { username: group.path, name: "#{group.name} (#{count})" } + end end def all_members - [{ username: "all", name: "Project and Group Members" }] + count = @project.team.members.flatten.count + [{ username: "all", name: "All Project and Group Members (#{count})" }] end end end -- GitLab From 4658e554b7129c44221a73fe8ec3b73b4b9b8b24 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 17:17:24 +0100 Subject: [PATCH 1103/1609] Fix EmailsOnPush comparison link to include first commit. --- app/mailers/emails/projects.rb | 4 ++-- spec/mailers/notify_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 5c38601c1ba..9ea121d83a4 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -31,8 +31,8 @@ module Emails if @commits.length > 1 @target_url = namespace_project_compare_url(@project.namespace, @project, - from: @commits.first, - to: @commits.last) + from: Commit.new(@compare.base), + to: Commit.new(@compare.head)) @subject << "Deleted " if @reverse_compare @subject << "#{@commits.length} commits: #{@commits.first.title}" else diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 534ab05942c..4090fa46205 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -568,7 +568,7 @@ describe Notify do let(:user) { create(:user) } let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits) } - let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: commits.first, to: commits.last) } + let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base), to: Commit.new(compare.head)) } let(:send_from_committer_email) { false } subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare, false, send_from_committer_email) } -- GitLab From 1c7947ace0e4f79f1c354a3d63b5f2caedbe1e35 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 25 Feb 2015 21:40:27 +0100 Subject: [PATCH 1104/1609] Add UTF-8 character to version_sorter test --- spec/helpers/application_helper_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 6abf5b98138..5a868ad6098 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -192,12 +192,12 @@ describe ApplicationHelper do it 'sorts tags in a natural order' do # Stub repository.tag_names to make sure we get some valid testing data expect(@project.repository).to receive(:tag_names). - and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v1.0.9a', - 'v2.0-rc1', 'v2.0rc2']) + and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v2.0rc1¿', + 'v1.0.9a', 'v2.0-rc1', 'v2.0rc2']) expect(options[1][1]). - to eq(['v3.1.4.2', 'v2.0', 'v2.0rc2', 'v2.0-rc1', 'v1.0.10', 'v1.0.9', - 'v1.0.9a']) + to eq(['v3.1.4.2', 'v2.0', 'v2.0rc2', 'v2.0rc1¿', 'v2.0-rc1', 'v1.0.10', + 'v1.0.9', 'v1.0.9a']) end end -- GitLab From 2c0cc2e1f69f5a90f586be0a14434047f33a9b83 Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Wed, 25 Feb 2015 13:34:04 -0800 Subject: [PATCH 1105/1609] Added hover state And also fixed it being one pixel off. --- app/assets/stylesheets/sections/nav_sidebar.scss | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/sections/nav_sidebar.scss index 335f1379662..c4a9945d7c5 100644 --- a/app/assets/stylesheets/sections/nav_sidebar.scss +++ b/app/assets/stylesheets/sections/nav_sidebar.scss @@ -147,19 +147,26 @@ .collapse-nav a { left: 0px; - padding: 5px 23px 3px 22px; + padding: 7px 23px 3px 22px; } } } .collapse-nav a { position: fixed; - top: 47px; - padding: 5px 13px 3px 13px; + top: 46px; + padding: 5px 13px 5px 13px; left: 197px; background: #EEE; color: black; - border: 1px solid rgba(0,0,0,0.035); + border-left: 1px solid rgba(0,0,0,0.035); + border-right: 1px solid rgba(0,0,0,0.035); +} + +.collapse-nav a:hover { + text-decoration: none; + color: #333; + background: #eaeaea; } @media (max-width: $screen-md-max) { -- GitLab From df31e0a88c5264046fbb0789f67529f023b3f810 Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Wed, 25 Feb 2015 13:41:35 -0800 Subject: [PATCH 1106/1609] Fixed up app_logo Pixel perfection. --- app/assets/stylesheets/sections/header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss index 26b4d04106e..03ecd3913ed 100644 --- a/app/assets/stylesheets/sections/header.scss +++ b/app/assets/stylesheets/sections/header.scss @@ -101,7 +101,7 @@ header { a { float: left; padding: 5px 0; - height: 46px; + height: 48px; width: 52px; text-align: center; -- GitLab From 4803af45dfbd516890b3ab31fa55b93009174d63 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 18:22:08 +0100 Subject: [PATCH 1107/1609] Prevent another migration from failing. --- db/migrate/20141006143943_move_slack_service_to_webhook.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index a8e07033a5d..5836cd6b8db 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -10,7 +10,7 @@ class MoveSlackServiceToWebhook < ActiveRecord::Migration slack_service.properties.delete('subdomain') # Room is configured on the Slack side slack_service.properties.delete('room') - slack_service.save + slack_service.save(validate: false) end end end -- GitLab From e4dc4390e57efa0b8207d63b4446c019580d00aa Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Wed, 25 Feb 2015 14:15:40 -0800 Subject: [PATCH 1108/1609] Reverting pixel change Breaks navbar --- app/assets/stylesheets/sections/header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss index 03ecd3913ed..26b4d04106e 100644 --- a/app/assets/stylesheets/sections/header.scss +++ b/app/assets/stylesheets/sections/header.scss @@ -101,7 +101,7 @@ header { a { float: left; padding: 5px 0; - height: 48px; + height: 46px; width: 52px; text-align: center; -- GitLab From 43f1ab9c12630e85861003c424da43314c2768a8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 23:23:49 +0100 Subject: [PATCH 1109/1609] Move CHANGELOG entry to 7.9. --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dce52b7c700..05413e02a9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ v 7.9.0 (unreleased) - Save web edit in new branch - Fix ordering of imported but unchanged projects (Marco Wessel) - Mobile UI improvements: make aside content expandable + - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) v 7.8.1 - Fix run of custom post receive hooks @@ -20,8 +21,6 @@ v 7.8.1 v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. - - Fix broken access control for note attachments (Hannes Rosenögger) - - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) - Include issue/mr participants in list of recipients for reassign/close/reopen emails -- GitLab From 1511506f724d2ac33c2c8221035109961b36b28a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Feb 2015 15:17:20 -0800 Subject: [PATCH 1110/1609] Fix git syntax issue --- app/models/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 4e45a6723b8..bbf35f04bbc 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -238,7 +238,7 @@ class Repository end def last_commit_for_path(sha, path) - args = %W(git rev-list --max-count 1 #{sha} -- #{path}) + args = %W(git rev-list --max-count=1 #{sha} -- #{path}) sha = Gitlab::Popen.popen(args, path_to_repo).first.strip commit(sha) end -- GitLab From 1da71cc520dd09098d8f756de3f58b8e2f153fcd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Feb 2015 19:34:16 -0800 Subject: [PATCH 1111/1609] Introduce shortcuts for routing helpers --- app/controllers/projects/avatars_controller.rb | 2 +- .../projects/repositories_controller.rb | 2 +- app/controllers/projects_controller.rb | 8 ++++---- app/helpers/gitlab_routing_helper.rb | 18 ++++++++++++++++++ app/helpers/projects_helper.rb | 4 ++-- app/views/admin/projects/show.html.haml | 4 ++-- app/views/dashboard/_project.html.haml | 2 +- app/views/dashboard/projects.html.haml | 2 +- app/views/groups/_projects.html.haml | 2 +- app/views/layouts/nav/_project.html.haml | 6 +++--- app/views/projects/_settings_nav.html.haml | 2 +- app/views/projects/diffs/_warning.html.haml | 4 ++-- .../projects/issues/_discussion.html.haml | 4 ++-- app/views/projects/issues/_issue.html.haml | 8 ++++---- app/views/projects/issues/show.html.haml | 4 ++-- .../merge_requests/_discussion.html.haml | 4 ++-- .../projects/merge_requests/_show.html.haml | 10 +++++----- .../merge_requests/show/_mr_title.html.haml | 4 ++-- app/views/projects/milestones/_issue.html.haml | 2 +- .../milestones/_merge_request.html.haml | 2 +- app/views/projects/no_repo.html.haml | 2 +- 21 files changed, 57 insertions(+), 39 deletions(-) create mode 100644 app/helpers/gitlab_routing_helper.rb diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index b90a95c3aab..a482b90880d 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -24,6 +24,6 @@ class Projects::AvatarsController < Projects::ApplicationController @project.save @project.reset_events_cache - redirect_to edit_namespace_project_path(@project.namespace, @project) + redirect_to edit_project_path(@project) end end diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index 245dfb7bb9a..cbb888b25e8 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -7,7 +7,7 @@ class Projects::RepositoriesController < Projects::ApplicationController def create @project.create_repository - redirect_to namespace_project_path(@project.namespace, @project) + redirect_to project_path(@project) end def archive diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8a055cc2a36..5486a97e51d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -23,7 +23,7 @@ class ProjectsController < ApplicationController if @project.saved? redirect_to( - namespace_project_path(@project.namespace, @project), + project_path(@project), notice: 'Project was successfully created.' ) else @@ -39,7 +39,7 @@ class ProjectsController < ApplicationController flash[:notice] = 'Project was successfully updated.' format.html do redirect_to( - edit_namespace_project_path(@project.namespace, @project), + edit_project_path(@project), notice: 'Project was successfully updated.' ) end @@ -133,7 +133,7 @@ class ProjectsController < ApplicationController @project.archive! respond_to do |format| - format.html { redirect_to namespace_project_path(@project.namespace, @project) } + format.html { redirect_to project_path(@project) } end end @@ -142,7 +142,7 @@ class ProjectsController < ApplicationController @project.unarchive! respond_to do |format| - format.html { redirect_to namespace_project_path(@project.namespace, @project) } + format.html { redirect_to project_path(@project) } end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb new file mode 100644 index 00000000000..932e0d29149 --- /dev/null +++ b/app/helpers/gitlab_routing_helper.rb @@ -0,0 +1,18 @@ +# Shorter routing method for project and project items +module GitlabRoutingHelper + def project_path(project, *args) + namespace_project_path(project.namespace, project, *args) + end + + def edit_project_path(project, *args) + edit_namespace_project_path(project.namespace, project, *args) + end + + def issue_path(entity, *args) + namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) + end + + def merge_request_path(entity, *args) + namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args) + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c85ad12634d..a5d7372bbe5 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -46,7 +46,7 @@ module ProjectsHelper simple_sanitize(project.group.name), group_path(project.group) ) + ' / ' + link_to(simple_sanitize(project.name), - namespace_project_path(project.namespace, project)) + project_path(project)) end else owner = project.namespace.owner @@ -55,7 +55,7 @@ module ProjectsHelper simple_sanitize(owner.name), user_path(owner) ) + ' / ' + link_to(simple_sanitize(project.name), - namespace_project_path(project.namespace, project)) + project_path(project)) end end end diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 3bcf1cc9ede..1421c2ea909 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -1,6 +1,6 @@ %h3.page-title Project: #{@project.name_with_namespace} - = link_to edit_namespace_project_path(@project.namespace, @project), class: "btn pull-right" do + = link_to edit_project_path(@project), class: "btn pull-right" do %i.fa.fa-pencil-square-o Edit %hr @@ -13,7 +13,7 @@ %li %span.light Name: %strong - = link_to @project.name, namespace_project_path(@project.namespace, @project) + = link_to @project.name, project_path(@project) %li %span.light Namespace: %strong diff --git a/app/views/dashboard/_project.html.haml b/app/views/dashboard/_project.html.haml index 3dd69df523d..fa9179cb249 100644 --- a/app/views/dashboard/_project.html.haml +++ b/app/views/dashboard/_project.html.haml @@ -1,4 +1,4 @@ -= link_to namespace_project_path(project.namespace, project), class: dom_class(project) do += link_to project_path(project), class: dom_class(project) do .dash-project-avatar = project_icon(project, alt: '', class: 'avatar project-avatar s40') .dash-project-access-icon diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml index e57e1e0939e..15db8592547 100644 --- a/app/views/dashboard/projects.html.haml +++ b/app/views/dashboard/projects.html.haml @@ -19,7 +19,7 @@ = project_icon("#{project.namespace.to_param}/#{project.to_param}", alt: '', class: 'avatar project-avatar s60') .project-access-icon = visibility_level_icon(project.visibility_level) - = link_to namespace_project_path(project.namespace, project), class: dom_class(project) do + = link_to project_path(project), class: dom_class(project) do %strong= project.name_with_namespace - if project.forked_from_project diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 2f28470f8be..b505760fa8f 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -11,7 +11,7 @@ .nothing-here-block This group has no projects yet - projects.each do |project| %li.project-row - = link_to namespace_project_path(project.namespace, project), class: dom_class(project) do + = link_to project_path(project), class: dom_class(project) do .dash-project-avatar = project_icon(project, alt: '', class: 'avatar s40') .dash-project-access-icon diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index ef31537b84e..d340ab1796a 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,7 +1,7 @@ %ul.project-navigation.nav.nav-sidebar - if @project_settings_nav = nav_link do - = link_to namespace_project_path(@project.namespace, @project), title: 'Back to project', class: "" do + = link_to project_path(@project), title: 'Back to project', class: "" do %i.fa.fa-caret-square-o-left %span Back to project @@ -12,7 +12,7 @@ - else = nav_link(path: 'projects#show', html_options: {class: "home"}) do - = link_to namespace_project_path(@project.namespace, @project), title: 'Project', class: 'shortcuts-project' do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do %i.fa.fa-dashboard %span Project @@ -89,7 +89,7 @@ - if project_nav_tab? :settings = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_namespace_project_path(@project.namespace, @project), title: 'Settings', class: "stat-tab tab no-highlight" do + = link_to edit_project_path(@project), title: 'Settings', class: "stat-tab tab no-highlight" do %i.fa.fa-cogs %span Settings diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml index 1a18bb065ad..7fc3d44034f 100644 --- a/app/views/projects/_settings_nav.html.haml +++ b/app/views/projects/_settings_nav.html.haml @@ -1,6 +1,6 @@ %ul.project-settings-nav.sidebar-subnav = nav_link(path: 'projects#edit') do - = link_to edit_namespace_project_path(@project.namespace, @project), title: 'Project', class: "stat-tab tab " do + = link_to edit_project_path(@project), title: 'Project', class: "stat-tab tab " do %i.fa.fa-pencil-square-o %span Project diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 5725c84600f..c9a6b3ebd9e 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -10,8 +10,8 @@ = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-warning btn-small" = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-small" - elsif @merge_request && @merge_request.persisted? - = link_to "Plain diff", namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :diff), class: "btn btn-warning btn-small" - = link_to "Email patch", namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :patch), class: "btn btn-warning btn-small" + = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-warning btn-small" + = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-small" %p To preserve performance only %strong #{allowed_diff_size} of #{diffs.size} diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 2bd3d8a73e1..fc3e35640dc 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,9 +1,9 @@ - content_for :note_actions do - if can?(current_user, :modify_issue, @issue) - if @issue.closed? - = link_to 'Reopen Issue', namespace_project_issue_path(@project.namespace, @project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue' + = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue' - else - = link_to 'Close Issue', namespace_project_issue_path(@project.namespace, @project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" + = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" .row %section.col-md-9 .participants diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 8af8da1d13e..01e2133e283 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,11 +1,11 @@ -%li{ id: dom_id(issue), class: issue_css_classes(issue), url: namespace_project_issue_path(issue.project.namespace, issue.project, issue) } +%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue) } - if controller.controller_name == 'issues' .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue", disabled: !can?(current_user, :modify_issue, issue) .issue-title %span.str-truncated - = link_to_gfm issue.title, namespace_project_issue_path(issue.project.namespace, issue.project, issue), class: "row_title" + = link_to_gfm issue.title, issue_path(issue), class: "row_title" .pull-right.light - if issue.closed? %span @@ -41,9 +41,9 @@ .issue-actions - if can? current_user, :modify_issue, issue - if issue.closed? - = link_to 'Reopen', namespace_project_issue_path(issue.project.namespace, issue.project, issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-small btn-grouped reopen_issue btn-reopen", remote: true + = link_to 'Reopen', issue_path(issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-small btn-grouped reopen_issue btn-reopen", remote: true - else - = link_to 'Close', namespace_project_issue_path(issue.project.namespace, issue.project, issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-small btn-grouped close_issue btn-close", remote: true + = link_to 'Close', issue_path(issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-small btn-grouped close_issue btn-close", remote: true = link_to edit_namespace_project_issue_path(issue.project.namespace, issue.project, issue), class: "btn btn-small edit-issue-link btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 6849a15e7e9..bd28d8a1db2 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -17,9 +17,9 @@ New Issue - if can?(current_user, :modify_issue, @issue) - if @issue.closed? - = link_to 'Reopen', namespace_project_issue_path(@project.namespace, @project, @issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen" + = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen" - else - = link_to 'Close', namespace_project_issue_path(@project.namespace, @project, @issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue" + = link_to 'Close', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue" = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: "btn btn-grouped issuable-edit" do %i.fa.fa-pencil-square-o diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 2df35aac025..79a093dc775 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -1,9 +1,9 @@ - content_for :note_actions do - if can?(current_user, :modify_merge_request, @merge_request) - if @merge_request.open? - = link_to 'Close', namespace_project_merge_request_path(@project.namespace, @project, @merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" + = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request" - if @merge_request.closed? - = link_to 'Reopen', namespace_project_merge_request_path(@project.namespace, @project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request" .row %section.col-md-9 diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index a53aed2f38a..ca4ceecb225 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,4 +1,4 @@ -.merge-request{'data-url' => namespace_project_merge_request_path(@project.namespace, @project, @merge_request)} +.merge-request{'data-url' => merge_request_path(@merge_request)} .merge-request-details = render "projects/merge_requests/show/mr_title" %hr @@ -28,8 +28,8 @@ Download as %span.caret %ul.dropdown-menu - %li= link_to "Email Patches", namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :patch) - %li= link_to "Plain Diff", namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :diff) + %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) + %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/show/state_widget" @@ -37,12 +37,12 @@ - if @commits.present? %ul.nav.nav-tabs.merge-request-tabs %li.notes-tab{data: {action: 'notes'}} - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request) do + = link_to merge_request_path(@merge_request) do %i.fa.fa-comments Discussion %span.badge= @merge_request.mr_and_commit_notes.count %li.commits-tab{data: {action: 'commits'}} - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), title: 'Commits' do + = link_to merge_request_path(@merge_request), title: 'Commits' do %i.fa.fa-history Commits %span.badge= @commits.size diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index 4c230953cb3..46e92a9c558 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -14,9 +14,9 @@ .issue-btn-group.pull-right - if can?(current_user, :modify_merge_request, @merge_request) - if @merge_request.open? - = link_to 'Close', namespace_project_merge_request_path(@project.namespace, @project, @merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request" + = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request" = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn btn-grouped issuable-edit", id: "edit_merge_request" do %i.fa.fa-pencil-square-o Edit - if @merge_request.closed? - = link_to 'Reopen', namespace_project_merge_request_path(@project.namespace, @project, @merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link", title: "Close merge request" + = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link", title: "Close merge request" diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml index 36463371f4f..26c83841a22 100644 --- a/app/views/projects/milestones/_issue.html.haml +++ b/app/views/projects/milestones/_issue.html.haml @@ -1,4 +1,4 @@ -%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => namespace_project_issue_path(@project.namespace, @project, issue) } +%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } %span.str-truncated = link_to [@project.namespace.becomes(Namespace), @project, issue] do %span.cgray ##{issue.iid} diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml index 3180c1d91b9..46f2df1b183 100644 --- a/app/views/projects/milestones/_merge_request.html.haml +++ b/app/views/projects/milestones/_merge_request.html.haml @@ -1,4 +1,4 @@ -%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => namespace_project_merge_request_path(@project.namespace, @project, merge_request) } +%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid, 'data-url' => merge_request_path(merge_request) } %span.str-truncated = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do %span.cgray ##{merge_request.iid} diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml index e8fd90efd1f..720957e8336 100644 --- a/app/views/projects/no_repo.html.haml +++ b/app/views/projects/no_repo.html.haml @@ -19,4 +19,4 @@ - if can? current_user, :remove_project, @project .prepend-top-20 - = link_to 'Remove project', namespace_project_path(@project.namespace, @project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" + = link_to 'Remove project', project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" -- GitLab From 0a4dec24c8effab297c195301f1213ab09d94633 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Feb 2015 19:41:17 -0800 Subject: [PATCH 1112/1609] Add explanation to routing method --- app/helpers/gitlab_routing_helper.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 932e0d29149..f0eb50a0e17 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -1,4 +1,17 @@ # Shorter routing method for project and project items +# Since update to rails 4.1.9 we are now allowed to use `/` in project routing +# so we use nested routing for project resources which include project and +# project namespace. To avoid writing long methods every time we define shortcuts for +# some of routing. +# +# For example instead of this: +# +# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request) +# +# We can simply use shortcut: +# +# merge_request_path(merge_request) +# module GitlabRoutingHelper def project_path(project, *args) namespace_project_path(project.namespace, project, *args) -- GitLab From 128012dba8737b0dc65d41a3eb1690c9d8797a34 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Feb 2015 19:50:01 -0800 Subject: [PATCH 1113/1609] More use of shortcut routes --- app/controllers/projects/issues_controller.rb | 8 +++----- app/controllers/projects/merge_requests_controller.rb | 4 +--- .../projects/merge_requests/_merge_request.html.haml | 2 +- app/views/projects/merge_requests/show/_diffs.html.haml | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 73b58285c61..6a2af08a199 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -60,8 +60,7 @@ class Projects::IssuesController < Projects::ApplicationController respond_to do |format| format.html do if @issue.valid? - redirect_to namespace_project_issue_path(@project.namespace, - @project, @issue) + redirect_to issue_path(@issue) else render :new end @@ -79,7 +78,7 @@ class Projects::IssuesController < Projects::ApplicationController format.js format.html do if @issue.valid? - redirect_to [@project.namespace.becomes(Namespace), @project, @issue] + redirect_to issue_path(@issue) else render :edit end @@ -129,8 +128,7 @@ class Projects::IssuesController < Projects::ApplicationController issue = @project.issues.find_by(id: params[:id]) if issue - redirect_to namespace_project_issue_path(@project.namespace, @project, - issue) + redirect_to issue_path(issue) return else raise ActiveRecord::RecordNotFound.new diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 98e4775e409..f07923d6d9e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -79,9 +79,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController if @merge_request.valid? redirect_to( - namespace_project_merge_request_path(@merge_request.target_project.namespace, - @merge_request.target_project, - @merge_request), + merge_request_path(@merge_request) notice: 'Merge request was successfully created.' ) else diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index d94636712be..1eba1a96b7b 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,7 +1,7 @@ %li{ class: mr_css_classes(merge_request) } .merge-request-title %span.str-truncated - = link_to_gfm merge_request.title, namespace_project_merge_request_path(merge_request.target_project.namespace, merge_request.target_project, merge_request), class: "row_title" + = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" .pull-right.light - if merge_request.merged? %span diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index eb1640891e6..cfef1d5e4cc 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -8,5 +8,5 @@ Changes view for this comparison is extremely large. %p You can - = link_to "download it", namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, format: :diff), class: "vlink" + = link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink" instead. -- GitLab From a9eba1bde0e0ccd86bbc1df32d2538f986105d55 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Feb 2015 19:51:13 -0800 Subject: [PATCH 1114/1609] No need to block db:rollback for safe migration --- db/migrate/20150223022001_set_missing_last_activity_at.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/db/migrate/20150223022001_set_missing_last_activity_at.rb b/db/migrate/20150223022001_set_missing_last_activity_at.rb index 3a3adf18872..3f6d4d83474 100644 --- a/db/migrate/20150223022001_set_missing_last_activity_at.rb +++ b/db/migrate/20150223022001_set_missing_last_activity_at.rb @@ -4,6 +4,5 @@ class SetMissingLastActivityAt < ActiveRecord::Migration end def down - raise ActiveRecord::IrreversibleMigration end end -- GitLab From e993b59b7dcaf795abb82af3f548f35aff01c6a8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Feb 2015 19:53:45 -0800 Subject: [PATCH 1115/1609] Dont render project entity --- app/views/projects/deploy_keys/_deploy_key.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index 52da85cbdfa..230e164f24c 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -13,7 +13,7 @@ = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :delete, class: "btn btn-remove delete-key btn-small pull-right" - = key_project = deploy_key.projects.include?(@project) ? @project : deploy_key.projects.first + - key_project = deploy_key.projects.include?(@project) ? @project : deploy_key.projects.first = link_to namespace_project_deploy_key_path(key_project.namespace, key_project, deploy_key) do %i.fa.fa-key %strong= deploy_key.title -- GitLab From c254cb03d8ddfb217341c2f83223ba30228f3088 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Feb 2015 20:00:28 -0800 Subject: [PATCH 1116/1609] Fix affix for issue and merge request with image in description --- app/assets/javascripts/issue.js.coffee | 11 ++++++----- app/assets/javascripts/merge_request.js.coffee | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index 9b7c1be8355..f2753170478 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -16,8 +16,9 @@ class @Issue updateTaskState ) - $('.issuable-affix').affix offset: - top: -> - @top = $('.issue-details').outerHeight(true) + 25 - bottom: -> - @bottom = $('.footer').outerHeight(true) + $('.issue-details').waitForImages -> + $('.issuable-affix').affix offset: + top: -> + @top = $('.issue-details').outerHeight(true) + 25 + bottom: -> + @bottom = $('.footer').outerHeight(true) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 757592842eb..ec686315435 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -20,11 +20,12 @@ class @MergeRequest if $("a.btn-close").length $("li.task-list-item input:checkbox").prop("disabled", false) - $('.issuable-affix').affix offset: - top: -> - @top = $('.merge-request-details').outerHeight(true) + 70 - bottom: -> - @bottom = $('.footer').outerHeight(true) + $('.merge-request-details').waitForImages -> + $('.issuable-affix').affix offset: + top: -> + @top = $('.merge-request-details').outerHeight(true) + 91 + bottom: -> + @bottom = $('.footer').outerHeight(true) # Local jQuery finder $: (selector) -> -- GitLab From 8490a8ab2a70828e4bb5587c7cc07c750483ef2e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 Feb 2015 04:20:17 -0500 Subject: [PATCH 1117/1609] Rename bulk_update_context_spec to bulk_update_service_spec --- .../{bulk_update_context_spec.rb => bulk_update_service_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/services/issues/{bulk_update_context_spec.rb => bulk_update_service_spec.rb} (100%) diff --git a/spec/services/issues/bulk_update_context_spec.rb b/spec/services/issues/bulk_update_service_spec.rb similarity index 100% rename from spec/services/issues/bulk_update_context_spec.rb rename to spec/services/issues/bulk_update_service_spec.rb -- GitLab From e53dd7526f69545ca86fc6935ad8077592628772 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 Feb 2015 04:29:33 -0500 Subject: [PATCH 1118/1609] Allow mass-unassigning of issues Fixes #867 [ci skip] --- CHANGELOG | 1 + .../project_users_select.js.coffee | 2 +- .../issues/bulk_update_service_spec.rb | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index d5b05125110..3f9af5b9f9f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 7.9.0 (unreleased) - Fix ordering of imported but unchanged projects (Marco Wessel) - Mobile UI improvements: make aside content expandable - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) + - Fix mass-unassignment of issues (Robert Speicher) v 7.8.1 - Fix run of custom post receive hooks diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee index 7fb33926096..885f0d58a6a 100644 --- a/app/assets/javascripts/project_users_select.js.coffee +++ b/app/assets/javascripts/project_users_select.js.coffee @@ -15,7 +15,7 @@ class @ProjectUsersSelect name: 'Unassigned', avatar: null, username: 'none', - id: '' + id: -1 } data.results.unshift(nullUser) diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index eb867f78c5c..504213e667f 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -84,6 +84,25 @@ describe Issues::BulkUpdateService do expect(@project.issues.first.assignee).to eq(@new_assignee) } + it 'allows mass-unassigning' do + @project.issues.first.update_attribute(:assignee, @new_assignee) + expect(@project.issues.first.assignee).not_to be_nil + + @params[:update][:assignee_id] = -1 + + Issues::BulkUpdateService.new(@project, @user, @params).execute + expect(@project.issues.first.assignee).to be_nil + end + + it 'does not unassign when assignee_id is not present' do + @project.issues.first.update_attribute(:assignee, @new_assignee) + expect(@project.issues.first.assignee).not_to be_nil + + @params[:update][:assignee_id] = '' + + Issues::BulkUpdateService.new(@project, @user, @params).execute + expect(@project.issues.first.assignee).not_to be_nil + end end describe :update_milestone do -- GitLab From e27f5aef462e5cf32f23fbb3137b98011c0c0ddf Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Feb 2015 22:17:46 -0800 Subject: [PATCH 1119/1609] Fix sticky diff header --- app/assets/javascripts/diff.js.coffee | 3 ++- app/assets/javascripts/merge_request.js.coffee | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee index b0b312e7749..05f5af42571 100644 --- a/app/assets/javascripts/diff.js.coffee +++ b/app/assets/javascripts/diff.js.coffee @@ -1,6 +1,7 @@ class @Diff UNFOLD_COUNT = 20 constructor: -> + $(document).off('click', '.js-unfold') $(document).on('click', '.js-unfold', (event) => target = $(event.target) unfoldBottom = target.hasClass('js-unfold-bottom') @@ -36,7 +37,7 @@ class @Diff ) ) - $('.diff-header').stick_in_parent(offset_top: $('.navbar').height()) + $('.diff-header').stick_in_parent(recalc_every: 1, offset_top: $('.navbar').height()) lineNumbers: (line) -> return ([0, 0]) unless line.children().length diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 757592842eb..805bf0203cb 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -95,6 +95,7 @@ class @MergeRequest this.$('.merge-request-tabs .diffs-tab').addClass 'active' this.loadDiff() unless @diffs_loaded this.$('.diffs').show() + $(".diff-header").trigger("sticky_kit:recalc") when 'commits' this.$('.merge-request-tabs .commits-tab').addClass 'active' this.$('.commits').show() -- GitLab From d3c44b1a6c470990cd79d2e4feec3d1d9b450496 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 25 Feb 2015 10:59:50 +0100 Subject: [PATCH 1120/1609] Fix import status page project links for new Rails. --- app/views/import/base/create.js.haml | 2 +- app/views/import/bitbucket/status.html.haml | 2 +- app/views/import/github/status.html.haml | 2 +- app/views/import/gitlab/status.html.haml | 2 +- app/views/import/gitorious/status.html.haml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml index 8ebdf4f1a20..8d10722628f 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -20,6 +20,6 @@ job.attr("id", "project_#{@project.id}") target_field = job.find(".import-target") target_field.empty() - target_field.append('#{link_to @project.path_with_namespace, @project}') + target_field.append('#{link_to @project.path_with_namespace, [@project.namespace.becomes(Namespace), @project]}') $("table.import-jobs tbody").prepend(job) job.addClass("active").find(".import-actions").html(" started") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 90c97393b51..bcbbaadf3e0 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -20,7 +20,7 @@ %td = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, project + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span.cgreen diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 957022f382f..883090a3026 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -20,7 +20,7 @@ %td = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, project + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span.cgreen diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index db161681206..41ac073eae1 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -20,7 +20,7 @@ %td = link_to project.import_source, "https://gitlab.com/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, project + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span.cgreen diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index e06e068fdb4..ebe24747a05 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -20,7 +20,7 @@ %td = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank" %td - %strong= link_to project.path_with_namespace, project + %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' %span.cgreen -- GitLab From 449cf43a55a34553fae591db7d69d1505b4daa53 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 26 Feb 2015 09:03:34 -0800 Subject: [PATCH 1121/1609] Add z index for diff header so sticky header stays on top on diff comments. --- app/assets/stylesheets/sections/diff.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/sections/diff.scss b/app/assets/stylesheets/sections/diff.scss index f47ea329827..54311a68852 100644 --- a/app/assets/stylesheets/sections/diff.scss +++ b/app/assets/stylesheets/sections/diff.scss @@ -8,6 +8,7 @@ border-bottom: 1px solid #CCC; padding: 5px 5px 5px 10px; color: #555; + z-index: 10; > span { font-family: $monospace_font; -- GitLab From 4efe3cf5569045c3f115777a448c042ed3ba1d22 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 26 Feb 2015 18:25:59 +0100 Subject: [PATCH 1122/1609] More reasons why prefixing is good Inspired by http://www.dwheeler.com/essays/filenames-in-shell.html --- doc/development/shell_commands.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index 42f17e19536..821027f43fa 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -139,6 +139,11 @@ path = File.join(repo_path, user_input) File.read(path) ``` +If you have to use user input a relative path, prefix `./` to the path. + +Prefixing user-supplied paths also offers extra protection against paths +starting with `-` (see the discussion about using `--` above). + ## Guard against path traversal Path traversal is a security where the program (GitLab) tries to restrict user -- GitLab From 6de4e4a622a98d86a44e9adf2fca15ff30c478c7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 26 Feb 2015 09:34:20 -0800 Subject: [PATCH 1123/1609] Include route helper shortcut in controller --- app/controllers/application_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7940b5cb3f4..df1a588313e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,6 +2,7 @@ require 'gon' class ApplicationController < ActionController::Base include Gitlab::CurrentSettings + include GitlabRoutingHelper before_filter :authenticate_user_from_token! before_filter :authenticate_user! -- GitLab From 62a81494ba22a33e5c798d9ce169a64239166f34 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 26 Feb 2015 16:03:42 -0800 Subject: [PATCH 1124/1609] Update project_milestone_path update route. --- app/controllers/projects/milestones_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 97eaabb15c3..afdb560e73c 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -54,7 +54,8 @@ class Projects::MilestonesController < Projects::ApplicationController format.js format.html do if @milestone.valid? - redirect_to [@project, @milestone] + redirect_to namespace_project_milestone_path(@project.namespace, + @project, @milestone) else render :edit end -- GitLab From 6ac0a0217cfcfa9915d5380337b9e5dd25b699ea Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 26 Feb 2015 16:44:52 -0800 Subject: [PATCH 1125/1609] Fix syntax issue --- app/controllers/projects/merge_requests_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index f07923d6d9e..26d4c51773f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -79,7 +79,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController if @merge_request.valid? redirect_to( - merge_request_path(@merge_request) + merge_request_path(@merge_request), notice: 'Merge request was successfully created.' ) else -- GitLab From 804a2488cfd6704726c0dc7e6b0facc7e2ff7ce3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 27 Feb 2015 10:47:37 +0100 Subject: [PATCH 1126/1609] Fix and test User#contributed_projects_ids. --- app/models/user.rb | 1 + spec/models/user_spec.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 27ac93f4841..55768a351e3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -629,5 +629,6 @@ class User < ActiveRecord::Base reorder(project_id: :desc). select(:project_id). uniq + .map(&:project_id) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index c015a1d2687..29d0c24e87e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -505,4 +505,32 @@ describe User do expect(User.sort(nil).first).to eq(@user) end end + + describe "#contributed_projects_ids" do + + subject { create(:user) } + let!(:project1) { create(:project) } + let!(:project2) { create(:project, forked_from_project: project3) } + let!(:project3) { create(:project) } + let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } + let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) } + let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) } + + before do + project1.team << [subject, :master] + project2.team << [subject, :master] + end + + it "includes IDs for projects the user has pushed to" do + expect(subject.contributed_projects_ids).to include(project1.id) + end + + it "includes IDs for projects the user has had merge requests merged into" do + expect(subject.contributed_projects_ids).to include(project3.id) + end + + it "doesn't include IDs for unrelated projects" do + expect(subject.contributed_projects_ids).not_to include(project2.id) + end + end end -- GitLab From 7202db072f802a7f003684b300995251dd518376 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 27 Feb 2015 11:26:47 +0100 Subject: [PATCH 1127/1609] Redirect old note attachment path to new uploads path. --- config/routes.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index 35053bdb20f..e152b27a262 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -97,6 +97,10 @@ Gitlab::Application.routes.draw do constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /.+/ } end + get "files/note/:id/:filename", + to: redirect("uploads/note/attachment/%{id}/%{filename}"), + constraints: { filename: /.+/ } + # # Explore area # -- GitLab From 3e6b342a1ab6702d2f897ecc0f259d3876e65ade Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 27 Feb 2015 10:38:15 -0800 Subject: [PATCH 1128/1609] Remove CI sections related to release tool, update steps in monthly and patch release documentation. --- doc/release/howto_rc1.md | 25 +++---------------------- doc/release/monthly.md | 34 +++++++++++++++------------------- doc/release/patch.md | 2 +- 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/doc/release/howto_rc1.md b/doc/release/howto_rc1.md index c4156d25d5f..07c703142d4 100644 --- a/doc/release/howto_rc1.md +++ b/doc/release/howto_rc1.md @@ -27,7 +27,7 @@ Make sure the code quality indicators are green / good. - [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq) -### 4. Run release tool for CE and EE +### 4. Run release tool **Make sure EE `master` has latest changes from CE `master`** @@ -38,8 +38,8 @@ git clone git@dev.gitlab.org:gitlab/release-tools.git cd release-tools ``` -Release candidate creates stable branch from master. -So we need to sync master branch between all CE remotes. Also do same for EE. +Release candidate creates stable branch from master. +So we need to sync master branch between all CE, EE and CI remotes. ``` bundle exec rake sync @@ -53,22 +53,3 @@ bundle exec rake release["x.x.0.rc1"] Now developers can use master for merging new features. So you should use stable branch for future code changes related to release. - - -### 5. Release GitLab CI RC1 - -Add to your local `gitlab-ci/.git/config`: - -``` -[remote "public"] - url = none - pushurl = git@dev.gitlab.org:gitlab/gitlab-ci.git - pushurl = git@gitlab.com:gitlab-org/gitlab-ci.git - pushurl = git@github.com:gitlabhq/gitlab-ci.git -``` - -* Create a stable branch `x-y-stable` -* Bump VERSION to `x.y.0.rc1` -* `git tag -a v$(cat VERSION) -m "Version $(cat VERSION)"` -* `git push public x-y-stable v$(cat VERSION)` - diff --git a/doc/release/monthly.md b/doc/release/monthly.md index c9e6d3426bc..dd44c1eb860 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -15,7 +15,7 @@ This person should also make sure this document is kept up to date and issues ar ## Take vacations into account -The time is measured in weekdays to compensate for weekends. +The time is measured in weekdays to compensate for weekends. Do everything on time to prevent problems due to rush jobs or too little testing time. Make sure that you take into account any vacations of maintainers. If the release is falling behind immediately warn the team. @@ -38,29 +38,30 @@ Xth: (7 working days before the 22nd) Xth: (6 working days before the 22nd) - [ ] Merge CE master in to EE master via merge request (#LINK) -- [ ] Create CE, EE, CI RC1 versions (#LINK) - [ ] Determine QA person and notify this person +- [ ] Check the tasks in [how to rc1 guide](howto_rc1.md) and delegate tasks if necessary +- [ ] Create CE, EE, CI RC1 versions (#LINK) Xth: (5 working days before the 22nd) - [ ] Do QA and fix anything coming out of it (#LINK) - [ ] Close the omnibus-gitlab milestone +- [ ] Prepare the blog post (#LINK) Xth: (4 working days before the 22nd) -- [ ] Build rc1 package for GitLab.com (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#build-a-package) - [ ] Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) +- [ ] Create regression issues (CE, CI) (#LINK) +- [ ] Tweet about rc1 (#LINK) Xth: (3 working days before the 22nd) -- [ ] Create regression issues (CE, CI) (#LINK) -- [ ] Tweet about rc1 (#LINK) -- [ ] Prepare the blog post (#LINK) +- [ ] Merge CE stable branch into EE stable branch Xth: (2 working days before the 22nd) -- [ ] Merge CE stable branch into EE stable branch - [ ] Check that everyone is mentioned on the blog post (the reviewer should have done this one working day ago) +- [ ] Check that MVP is added to the mvp page (source/mvp/index.html in www-gitlab-com) Xth: (1 working day before the 22nd) @@ -93,13 +94,13 @@ There are three changelogs that need to be updated: CE, EE and CI. ## Prepare CHANGELOG for next release -Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version. +Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version, usually X.X.X.pre. ## QA Create issue on dev.gitlab.org `gitlab` repository, named "GitLab X.X QA" in order to keep track of the progress. -Use the omnibus packages of Enterprise Edition using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md). +Use the omnibus packages created for RC1 of Enterprise Edition using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md). **NOTE** Upgrader can only be tested when tags are pushed to all repositories. Do not forget to confirm it is working before releasing. Note that in the issue. @@ -112,8 +113,7 @@ create an issue about it in order to discuss the next steps after the release. ## Update GitLab.com with RC1 -Merge the RC1 EE code into GitLab.com. -Once the build is green, create a package. +Use the omnibus EE packages created for RC1. If there are big database migrations consider testing them with the production db on a VM. Try to deploy in the morning. It is important to do this as soon as possible, so we can catch any errors before we release the full version. @@ -127,7 +127,7 @@ Please do not raise issues directly in this issue but link to issues that might The decision to create a patch release or not is with the release manager who is assigned to this issue. The release manager will comment here about the plans for patch releases. -Assign the issue to the release manager and /cc all the core-team members active on the issue tracker. If there are any known bugs in the release add them immediately. +Assign the issue to the release manager and at mention all members of gitlab core team. If there are any known bugs in the release add them immediately. ## Tweet about RC1 @@ -143,8 +143,8 @@ Tweet about the RC release: 1. Also check the CI changelog 1. Add a proposed tweet text to the blog post WIP MR description. 1. Create a WIP MR for the blog post -1. Ask Dmitriy to add screenshots to the WIP MR. -1. Decide with team who will be the MVP user. +1. Ask Dmitriy (or a team member with OS X) to add screenshots to the WIP MR. +1. Decide with core team who will be the MVP user. 1. Create WIP MR for adding MVP to MVP page on website 1. Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible. 1. Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master) @@ -166,11 +166,7 @@ Bump version, create release tag and push to remotes: bundle exec rake release["x.x.0"] ``` -Also perform these steps for GitLab CI: - -1. bump version in the stable branch -1. create annotated tag -1. push the stable branch and the annotated tag to the public repositories +This will create correct version and tag and push to all CE, EE and CI remotes. Update [installation.md](/doc/install/installation.md) to the newest version in master. diff --git a/doc/release/patch.md b/doc/release/patch.md index d8bb4aef0e2..80afa19b6c5 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -51,6 +51,6 @@ CE=false be rake release['x.x.x'] 1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md) 1. Apply the patch to GitLab.com and the private GitLab development server -1. Create and publish a blog post +1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md) 1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post 1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only) -- GitLab From 238455aa97c21cf4126994627725ba0196decf9c Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 27 Feb 2015 12:32:05 -0800 Subject: [PATCH 1129/1609] Mention audit events ee feature in logs documentation. --- doc/logs/logs.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/logs/logs.md b/doc/logs/logs.md index 07302894dd4..ec0109a426f 100644 --- a/doc/logs/logs.md +++ b/doc/logs/logs.md @@ -1,6 +1,8 @@ ## Log system GitLab has advanced log system so everything is logging and you can analize your instance using various system log files. -These log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. +In addition to system log files, GitLab Enterprise Edition comes with Audit Events. Find more about them [in Audit Events documentation](http://doc.gitlab.com/ee/administration/audit_events.html) + +System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files. #### production.log This file lives in `/var/log/gitlab/gitlab-rails/production.log` for omnibus package or in `/home/git/gitlab/logs/production.log` for installations from the source. @@ -12,7 +14,7 @@ This task is more useful for GitLab contributors and developers. Use part of thi Started GET "/gitlabhq/yaml_db/tree/master" for 168.111.56.1 at 2015-02-12 19:34:53 +0200 Processing by Projects::TreeController#show as HTML Parameters: {"project_id"=>"gitlabhq/yaml_db", "id"=>"master"} - + ... [CUT OUT] amespaces"."created_at" DESC, "namespaces"."id" DESC LIMIT 1 [["id", 26]] -- GitLab From 9e4d3f328f00459447e087677a1684e54faa47c8 Mon Sep 17 00:00:00 2001 From: Mlanawo Mbechezi Date: Fri, 27 Feb 2015 22:49:40 +0100 Subject: [PATCH 1130/1609] fix typo --- app/views/admin/services/index.html.haml | 2 +- app/views/projects/services/index.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml index 1d3e192a325..0093fb97765 100644 --- a/app/views/admin/services/index.html.haml +++ b/app/views/admin/services/index.html.haml @@ -6,7 +6,7 @@ %tr %th %th Service - %th Desription + %th Description %th Last edit - @services.sort_by(&:title).each do |service| %tr diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml index d615d128653..0d3ccb6bb83 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/index.html.haml @@ -6,7 +6,7 @@ %tr %th %th Service - %th Desription + %th Description %th Last edit - @services.sort_by(&:title).each do |service| %tr -- GitLab From df2353716b076a445c454dcb2da1f09e06f7f3c5 Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Fri, 27 Feb 2015 16:22:53 -0800 Subject: [PATCH 1131/1609] Changed header to Create New Issue --- app/views/projects/issues/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 76075124f9e..4da42c83db8 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,5 +1,5 @@ %div.issue-form-holder - %h3.page-title= @issue.new_record? ? "New Issue" : "Edit Issue ##{@issue.iid}" + %h3.page-title= @issue.new_record? ? "Create New Issue" : "Edit Issue ##{@issue.iid}" %hr = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| -- GitLab From a41b9533fe569e1cd8471fbac8a34447560ecbb8 Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Fri, 27 Feb 2015 16:24:05 -0800 Subject: [PATCH 1132/1609] Changed to "Create Issue" --- app/views/projects/issues/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml index 4da42c83db8..7d7217eb2a8 100644 --- a/app/views/projects/issues/_form.html.haml +++ b/app/views/projects/issues/_form.html.haml @@ -1,5 +1,5 @@ %div.issue-form-holder - %h3.page-title= @issue.new_record? ? "Create New Issue" : "Edit Issue ##{@issue.iid}" + %h3.page-title= @issue.new_record? ? "Create Issue" : "Edit Issue ##{@issue.iid}" %hr = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f| -- GitLab From d2c85a68bb763d2beccf8ebc0087791f6714c6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A8s=20Koetsier?= Date: Sun, 28 Dec 2014 21:08:33 +0100 Subject: [PATCH 1133/1609] Allow a user to specify a channel and username for the slack-webhook --- CHANGELOG | 1 + .../projects/services_controller.rb | 2 +- app/models/project_services/slack_service.rb | 13 ++++++++--- .../project_services/slack_service_spec.rb | 22 +++++++++++++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6803855fcf7..58ae4811094 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -88,6 +88,7 @@ v 7.8.0 - Improve database performance for GitLab - Add Asana service (Jeremy Benoist) - Improve project web hooks with extra data + - Slack username and channel options v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 5c29a6550f5..ce0f98bdeff 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -50,7 +50,7 @@ class Projects::ServicesController < Projects::ApplicationController :room, :recipients, :project_url, :webhook, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch + :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel ) end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 297d8bbb5d4..c7cbff63fe5 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -14,7 +14,7 @@ # class SlackService < Service - prop_accessor :webhook + prop_accessor :webhook, :username, :channel validates :webhook, presence: true, if: :activated? def title @@ -31,7 +31,10 @@ class SlackService < Service def fields [ - { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' } + { type: 'text', name: 'webhook', + placeholder: 'https://hooks.slack.com/services/...' }, + { type: 'text', name: 'username', placeholder: 'username' }, + { type: 'text', name: 'channel', placeholder: '#channel' } ] end @@ -43,7 +46,11 @@ class SlackService < Service project_name: project_name )) - notifier = Slack::Notifier.new(webhook) + opt = {} + opt[:channel] = channel if channel + opt[:username] = username if username + + notifier = Slack::Notifier.new(webhook, opt) notifier.ping(message.pretext, attachments: message.attachments) end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 90b385423f1..8a75d8987ab 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -36,6 +36,8 @@ describe SlackService do let(:project) { create(:project) } let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } + let(:username) { 'slack_username' } + let(:channel) { 'slack_channel' } before do slack.stub( @@ -53,5 +55,25 @@ describe SlackService do expect(WebMock).to have_requested(:post, webhook_url).once end + + it 'should use the username as an option for slack when configured' do + slack.stub(username: username) + expect(Slack::Notifier).to receive(:new). + with(webhook_url, username: username). + and_return( + double(:slack_service).as_null_object + ) + slack.execute(sample_data) + end + + it 'should use the channel as an option when it is configured' do + slack.stub(channel: channel) + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: channel). + and_return( + double(:slack_service).as_null_object + ) + slack.execute(sample_data) + end end end -- GitLab From ff696856079c984332de167219f2768415e6730f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 27 Feb 2015 11:26:47 +0100 Subject: [PATCH 1134/1609] Add comment about note attachment redirect. --- config/routes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/routes.rb b/config/routes.rb index e152b27a262..63299176932 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -97,6 +97,7 @@ Gitlab::Application.routes.draw do constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /.+/ } end + # Redirect old note attachments path to new uploads path. get "files/note/:id/:filename", to: redirect("uploads/note/attachment/%{id}/%{filename}"), constraints: { filename: /.+/ } -- GitLab From 51abeaa1bc93862a4d15506a590704f9fc56cfd6 Mon Sep 17 00:00:00 2001 From: sue445 Date: Sun, 1 Mar 2015 02:07:53 +0900 Subject: [PATCH 1135/1609] Expose avatar_url in projects API * Impl Project#avatar_url * Refactor ApplicationHelper: Use Project#avatar_url * Update changelog --- CHANGELOG | 1 + app/helpers/application_helper.rb | 6 ++--- app/models/project.rb | 10 ++++++++ doc/api/projects.md | 9 ++++--- lib/api/entities.rb | 1 + spec/helpers/application_helper_spec.rb | 6 +++-- spec/models/project_spec.rb | 31 +++++++++++++++++++++++++ 7 files changed, 55 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d5b05125110..f534f50b7aa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ v 7.9.0 (unreleased) - Save web edit in new branch - Fix ordering of imported but unchanged projects (Marco Wessel) - Mobile UI improvements: make aside content expandable + - Expose avatar_url in projects API - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) v 7.8.1 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 365de3595cd..a81e41819b7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -58,10 +58,8 @@ module ApplicationHelper Project.find_with_namespace(project_id) end - if project.avatar.present? - image_tag project.avatar.url, options - elsif project.avatar_in_git - image_tag namespace_project_avatar_path(project.namespace, project), options + if project.avatar_url + image_tag project.avatar_url, options else # generated icon project_identicon(project, options) end diff --git a/app/models/project.rb b/app/models/project.rb index d33b25db201..7f2e0b4c17b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -37,6 +37,8 @@ class Project < ActiveRecord::Base include Gitlab::ShellAdapter include Gitlab::VisibilityLevel include Gitlab::ConfigHelper + include Rails.application.routes.url_helpers + extend Gitlab::ConfigHelper extend Enumerize @@ -408,6 +410,14 @@ class Project < ActiveRecord::Base @avatar_file end + def avatar_url + if avatar.present? + [gitlab_config.url, avatar.url].join + elsif avatar_in_git + [gitlab_config.url, namespace_project_avatar_path(namespace, self)].join + end + end + # For compatibility with old code def code path diff --git a/doc/api/projects.md b/doc/api/projects.md index a1a23051d7e..7fe244477db 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -68,7 +68,8 @@ Parameters: "path": "diaspora", "updated_at": "2013-09-30T13: 46: 02Z" }, - "archived": false + "archived": false, + "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png" }, { "id": 6, @@ -103,7 +104,8 @@ Parameters: "path": "brightbox", "updated_at": "2013-09-30T13:46:02Z" }, - "archived": false + "archived": false, + "avatar_url": null } ] ``` @@ -195,7 +197,8 @@ Parameters: "notification_level": 3 } }, - "archived": false + "archived": false, + "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png" } ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 7572104fc16..af76f3c439e 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -56,6 +56,7 @@ module API expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at expose :namespace expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } + expose :avatar_url end class ProjectMember < UserBasic diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 9d99b6e33cb..de491ce8a58 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -64,8 +64,9 @@ describe ApplicationHelper do project = create(:project) project.avatar = File.open(avatar_file_path) project.save! + avatar_url = "http://localhost/uploads/project/avatar/#{ project.id }/gitlab_logo.png" expect(project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to eq( - "\"Gitlab" + "\"Gitlab" ) end @@ -75,8 +76,9 @@ describe ApplicationHelper do allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) + avatar_url = 'http://localhost' + namespace_project_avatar_path(project.namespace, project) expect(project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match( - image_tag(namespace_project_avatar_path(project.namespace, project))) + image_tag(avatar_url)) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a9df6f137b7..879a63dd9f9 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -326,4 +326,35 @@ describe Project do expect(project.avatar_type).to eq(['only images allowed']) end end + + describe :avatar_url do + subject { project.avatar_url } + + let(:project) { create(:project) } + + context 'When avatar file is uploaded' do + before do + project.update_columns(avatar: 'uploads/avatar.png') + allow(project.avatar).to receive(:present?) { true } + end + + let(:avatar_path) do + "/uploads/project/avatar/#{project.id}/uploads/avatar.png" + end + + it { should eq "http://localhost#{avatar_path}" } + end + + context 'When avatar file in git' do + before do + allow(project).to receive(:avatar_in_git) { true } + end + + let(:avatar_path) do + "/#{project.namespace.name}/#{project.path}/avatar" + end + + it { should eq "http://localhost#{avatar_path}" } + end + end end -- GitLab From 7486bc0ae33adc141abbca2ea5dea833e56d5409 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Sun, 1 Mar 2015 09:40:04 +0100 Subject: [PATCH 1136/1609] Update Dockerfile for GitLab 7.8.1 --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3a0a55e18e3..3584a754c62 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update -q \ # If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.8.0-omnibus-1_amd64.deb \ + wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.8.1-omnibus-1_amd64.deb \ && dpkg -i $TMP_FILE \ && rm -f $TMP_FILE -- GitLab From f84b7eef3f969a65d0930c9d62b6968b2ae70f12 Mon Sep 17 00:00:00 2001 From: Aorimn Date: Wed, 4 Feb 2015 21:31:55 +0100 Subject: [PATCH 1137/1609] Add Irker service Irker is a gateway which sends IRC messages on git updates. This new service provides an interface to this gateway, integrated in Gitlab, for each updates. As per the guidelines, this commit adds the new feature in the CHANGELOG, tests and documentation. See http://www.catb.org/esr/irker/ --- CHANGELOG | 1 + .../projects/services_controller.rb | 3 +- app/models/project.rb | 1 + app/models/project_services/irker_service.rb | 152 ++++++++++++++++ app/models/service.rb | 3 +- app/workers/irker_worker.rb | 169 ++++++++++++++++++ doc/project_services/irker.md | 46 +++++ doc/project_services/project_services.md | 1 + features/project/service.feature | 6 + features/steps/project/services.rb | 17 ++ .../project_services/irker_service_spec.rb | 103 +++++++++++ 11 files changed, 500 insertions(+), 2 deletions(-) create mode 100644 app/models/project_services/irker_service.rb create mode 100644 app/workers/irker_worker.rb create mode 100644 doc/project_services/irker.md create mode 100644 spec/models/project_services/irker_service_spec.rb diff --git a/CHANGELOG b/CHANGELOG index dae32953cd9..ee862a4ca3c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ v 7.9.0 (unreleased) - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) - Fix mass-unassignment of issues (Robert Speicher) - Allow user confirmation to be skipped for new users via API + - Add a service to send updates to an Irker gateway (Romain Coltel) v 7.8.1 - Fix run of custom post receive hooks diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 5c29a6550f5..e7823020e60 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -50,7 +50,8 @@ class Projects::ServicesController < Projects::ApplicationController :room, :recipients, :project_url, :webhook, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch + :description, :issues_url, :new_issue_url, :restrict_to_branch, + :colorize_messages, :channels ) end end diff --git a/app/models/project.rb b/app/models/project.rb index 7f2e0b4c17b..907f331d8f1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -73,6 +73,7 @@ class Project < ActiveRecord::Base has_one :gitlab_ci_service, dependent: :destroy has_one :campfire_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy + has_one :irker_service, dependent: :destroy has_one :pivotaltracker_service, dependent: :destroy has_one :hipchat_service, dependent: :destroy has_one :flowdock_service, dependent: :destroy diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb new file mode 100644 index 00000000000..a0203a5bb10 --- /dev/null +++ b/app/models/project_services/irker_service.rb @@ -0,0 +1,152 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) + +require 'uri' + +class IrkerService < Service + prop_accessor :colorize_messages, :recipients, :channels + validates :recipients, presence: true, if: :activated? + validate :check_recipients_count, if: :activated? + + before_validation :get_channels + after_initialize :initialize_settings + + # Writer for RSpec tests + attr_writer :settings + + def initialize_settings + # See the documentation (doc/project_services/irker.md) for possible values + # here + @settings ||= { + server_ip: 'localhost', + server_port: 6659, + max_channels: 3, + default_irc_uri: nil + } + end + + def title + 'Irker (IRC gateway)' + end + + def description + 'Send IRC messages, on update, to a list of recipients through an Irker '\ + 'gateway.' + end + + def help + msg = 'Recipients have to be specified with a full URI: '\ + 'irc[s]://irc.network.net[:port]/#channel. Special cases: if you want '\ + 'the channel to be a nickname instead, append ",isnick" to the channel '\ + 'name; if the channel is protected by a secret password, append '\ + '"?key=secretpassword" to the URI.' + + unless @settings[:default_irc].nil? + msg += ' Note that a default IRC URI is provided by this service\'s '\ + "administrator: #{default_irc}. You can thus just give a channel name." + end + msg + end + + def to_param + 'irker' + end + + def execute(push_data) + IrkerWorker.perform_async(project_id, channels, + colorize_messages, push_data, @settings) + end + + def fields + [ + { type: 'textarea', name: 'recipients', + placeholder: 'Recipients/channels separated by whitespaces' }, + { type: 'checkbox', name: 'colorize_messages' }, + ] + end + + private + + def check_recipients_count + return true if recipients.nil? || recipients.empty? + + if recipients.split(/\s+/).count > max_chans + errors.add(:recipients, "are limited to #{max_chans}") + end + end + + def max_chans + @settings[:max_channels] + end + + def get_channels + return true unless :activated? + return true if recipients.nil? || recipients.empty? + + map_recipients + + errors.add(:recipients, 'are all invalid') if channels.empty? + true + end + + def map_recipients + self.channels = recipients.split(/\s+/).map do |recipient| + format_channel default_irc_uri, recipient + end + channels.reject! &:nil? + end + + def default_irc_uri + default_irc = @settings[:default_irc_uri] + if !(default_irc.nil? || default_irc[-1] == '/') + default_irc += '/' + end + default_irc + end + + def format_channel(default_irc, recipient) + cnt = 0 + url = nil + + # Try to parse the chan as a full URI + begin + uri = URI.parse(recipient) + raise URI::InvalidURIError if uri.scheme.nil? && cnt == 0 + rescue URI::InvalidURIError + unless default_irc.nil? + cnt += 1 + recipient = "#{default_irc}#{recipient}" + retry if cnt == 1 + end + else + url = consider_uri uri + end + url + end + + def consider_uri(uri) + # Authorize both irc://domain.com/#chan and irc://domain.com/chan + if uri.is_a?(URI) && uri.scheme[/^ircs?$/] && !uri.path.nil? + # Do not authorize irc://domain.com/ + if uri.fragment.nil? && uri.path.length > 1 + uri.to_s + else + # Authorize irc://domain.com/smthg#chan + # The irker daemon will deal with it by concatenating smthg and + # chan, thus sending messages on #smthgchan + uri.to_s + end + end + end +end diff --git a/app/models/service.rb b/app/models/service.rb index f87d875c10a..f4e97da3212 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -100,7 +100,8 @@ class Service < ActiveRecord::Base def self.available_services_names %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla asana - emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira redmine custom_issue_tracker) + emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira + redmine custom_issue_tracker irker) end def self.create_from_template(project_id, template) diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb new file mode 100644 index 00000000000..613bae351d8 --- /dev/null +++ b/app/workers/irker_worker.rb @@ -0,0 +1,169 @@ +require 'json' +require 'socket' + +class IrkerWorker + include Sidekiq::Worker + + def perform(project_id, chans, colors, push_data, settings) + project = Project.find(project_id) + + # Get config parameters + return false unless init_perform settings, chans, colors + + repo_name = push_data['repository']['name'] + committer = push_data['user_name'] + branch = push_data['ref'].gsub(%r'refs/[^/]*/', '') + + if @colors + repo_name = "\x0304#{repo_name}\x0f" + branch = "\x0305#{branch}\x0f" + end + + # Firsts messages are for branch creation/deletion + send_branch_updates push_data, project, repo_name, committer, branch + + # Next messages are for commits + send_commits push_data, project, repo_name, committer, branch + + close_connection + true + end + + private + + def init_perform(set, chans, colors) + @colors = colors + @channels = chans + start_connection set['server_ip'], set['server_port'] + end + + def start_connection(irker_server, irker_port) + begin + @socket = TCPSocket.new irker_server, irker_port + rescue Errno::ECONNREFUSED => e + logger.fatal "Can't connect to Irker daemon: #{e}" + return false + end + true + end + + def sendtoirker(privmsg) + to_send = { to: @channels, privmsg: privmsg } + @socket.puts JSON.dump(to_send) + end + + def close_connection + @socket.close + end + + def send_branch_updates(push_data, project, repo_name, committer, branch) + if push_data['before'] =~ /^000000/ + send_new_branch project, repo_name, committer, branch + elsif push_data['after'] =~ /^000000/ + send_del_branch repo_name, committer, branch + end + end + + def send_new_branch(project, repo_name, committer, branch) + repo_path = project.path_with_namespace + newbranch = "#{Gitlab.config.gitlab.url}/#{repo_path}/branches" + newbranch = "\x0302\x1f#{newbranch}\x0f" if @colors + + privmsg = "[#{repo_name}] #{committer} has created a new branch " + privmsg += "#{branch}: #{newbranch}" + sendtoirker privmsg + end + + def send_del_branch(repo_name, committer, branch) + privmsg = "[#{repo_name}] #{committer} has deleted the branch #{branch}" + sendtoirker privmsg + end + + def send_commits(push_data, project, repo_name, committer, branch) + return if push_data['total_commits_count'] == 0 + + # Next message is for number of commit pushed, if any + if push_data['before'] =~ /^000000/ + # Tweak on push_data["before"] in order to have a nice compare URL + push_data['before'] = before_on_new_branch push_data, project + end + + send_commits_count(push_data, project, repo_name, committer, branch) + + # One message per commit, limited by 3 messages (same limit as the + # github irc hook) + commits = push_data['commits'].first(3) + commits.each do |hook_attrs| + send_one_commit project, hook_attrs, repo_name, branch + end + end + + def before_on_new_branch(push_data, project) + commit = commit_from_id project, push_data['commits'][0]['id'] + parents = commit.parents + # Return old value if there's no new one + return push_data['before'] if parents.empty? + # Or return the first parent-commit + parents[0].id + end + + def send_commits_count(data, project, repo, committer, branch) + url = compare_url data, project.path_with_namespace + commits = colorize_commits data['total_commits_count'] + + new_commits = 'new commit' + new_commits += 's' if data['total_commits_count'] > 1 + + sendtoirker "[#{repo}] #{committer} pushed #{commits} #{new_commits} " \ + "to #{branch}: #{url}" + end + + def compare_url(data, repo_path) + sha1 = Commit::truncate_sha(data['before']) + sha2 = Commit::truncate_sha(data['after']) + compare_url = "#{Gitlab.config.gitlab.url}/#{repo_path}/compare" + compare_url += "/#{sha1}...#{sha2}" + colorize_url compare_url + end + + def send_one_commit(project, hook_attrs, repo_name, branch) + commit = commit_from_id project, hook_attrs['id'] + sha = colorize_sha Commit::truncate_sha(hook_attrs['id']) + author = hook_attrs['author']['name'] + files = colorize_nb_files(files_count commit) + title = commit.title + + sendtoirker "#{repo_name}/#{branch} #{sha} #{author} (#{files}): #{title}" + end + + def commit_from_id(project, id) + commit = Gitlab::Git::Commit.find(project.repository, id) + Commit.new(commit) + end + + def files_count(commit) + files = "#{commit.diffs.count} file" + files += 's' if commit.diffs.count > 1 + files + end + + def colorize_sha(sha) + sha = "\x0314#{sha}\x0f" if @colors + sha + end + + def colorize_nb_files(nb_files) + nb_files = "\x0312#{nb_files}\x0f" if @colors + nb_files + end + + def colorize_url(url) + url = "\x0302\x1f#{url}\x0f" if @colors + url + end + + def colorize_commits(commits) + commits = "\x02#{commits}\x0f" if @colors + commits + end +end diff --git a/doc/project_services/irker.md b/doc/project_services/irker.md new file mode 100644 index 00000000000..780a45bca20 --- /dev/null +++ b/doc/project_services/irker.md @@ -0,0 +1,46 @@ +# Irker IRC Gateway + +GitLab provides a way to push update messages to an Irker server. When +configured, pushes to a project will trigger the service to send data directly +to the Irker server. + +See the project homepage for further info: http://www.catb.org/esr/irker/ + +## Needed setup + +You will first need an Irker daemon. You can download the Irker code from its +gitorious repository on https://gitorious.org/irker: `git clone +git@gitorious.org:irker/irker.git`. Once you have downloaded the code, you can +run the python script named `irkerd`. This script is the gateway script, it acts +both as an IRC client, for sending messages to an IRC server obviously, and as a +TCP server, for receiving messages from the GitLab service. + +If the Irker server runs on the same machine, you are done. If not, you will +need to follow the firsts steps of the next section. + +## Optional setup + +In the `app/models/project_services/irker_service.rb` file, you can modify some +options in the `initialize_settings` method: +- **server_ip** (defaults to `localhost`): the server IP address where the +`irkerd` daemon runs; +- **server_port** (defaults to `6659`): the server port of the `irkerd` daemon; +- **max_channels** (defaults to `3`): the maximum number of recipients the +client is authorized to join, per project; +- **default_irc_uri** (no default) : if this option is set, it has to be in the +format `irc[s]://domain.name` and will be prepend to each and every channel +provided by the user which is not a full URI. + +If the Irker server and the GitLab application do not run on the same host, you +will **need** to setup at least the **server_ip** option. + +## Note on Irker recipients + +Irker accepts channel names of the form `chan` and `#chan`, both for the +`#chan` channel. If you want to send messages in query, you will need to add +`,isnick` avec the channel name, in this form: `Aorimn,isnick`. In this latter +case, `Aorimn` is treated as a nick and no more as a channel name. + +Irker can also join password-protected channels. Users need to append +`?key=thesecretpassword` to the chan name. + diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 93a57485cfd..86eda341d6c 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -13,6 +13,7 @@ __Project integrations with external services for continuous integration and mor - Gemnasium - GitLab CI - HipChat +- [Irker](irker.md) An IRC gateway to receive messages on repository updates. - Pivotal Tracker - Pushover - Slack diff --git a/features/project/service.feature b/features/project/service.feature index d0600aca010..fdff640ec85 100644 --- a/features/project/service.feature +++ b/features/project/service.feature @@ -61,6 +61,12 @@ Feature: Project Services And I fill email on push settings Then I should see email on push service settings saved + Scenario: Activate Irker (IRC Gateway) service + When I visit project "Shop" services page + And I click Irker service link + And I fill Irker settings + Then I should see Irker service settings saved + Scenario: Activate Atlassian Bamboo CI service When I visit project "Shop" services page And I click Atlassian Bamboo CI service link diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 3307117e69a..4b3d79324ab 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -17,6 +17,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps page.should have_content 'Atlassian Bamboo' page.should have_content 'JetBrains TeamCity' page.should have_content 'Asana' + page.should have_content 'Irker (IRC gateway)' end step 'I click gitlab-ci service link' do @@ -132,6 +133,22 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps find_field('Recipients').value.should == 'qa@company.name' end + step 'I click Irker service link' do + click_link 'Irker (IRC gateway)' + end + + step 'I fill Irker settings' do + check 'Active' + fill_in 'Recipients', with: 'irc://chat.freenode.net/#commits' + check 'Colorize messages' + click_button 'Save' + end + + step 'I should see Irker service settings saved' do + find_field('Recipients').value.should == 'irc://chat.freenode.net/#commits' + find_field('Colorize messages').value.should == '1' + end + step 'I click Slack service link' do click_link 'Slack' end diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb new file mode 100644 index 00000000000..bbd5245ad34 --- /dev/null +++ b/spec/models/project_services/irker_service_spec.rb @@ -0,0 +1,103 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +require 'spec_helper' +require 'socket' +require 'json' + +describe IrkerService do + describe 'Associations' do + it { should belong_to :project } + it { should have_one :service_hook } + end + + describe 'Validations' do + before do + subject.active = true + subject.properties['recipients'] = _recipients + end + + context 'active' do + let(:_recipients) { nil } + it { should validate_presence_of :recipients } + end + + context 'too many recipients' do + let(:_recipients) { 'a b c d' } + it 'should add an error if there is too many recipients' do + subject.send :check_recipients_count + subject.errors.should_not be_blank + end + end + + context '3 recipients' do + let(:_recipients) { 'a b c' } + it 'should not add an error if there is 3 recipients' do + subject.send :check_recipients_count + subject.errors.should be_blank + end + end + end + + describe 'Execute' do + let(:irker) { IrkerService.new } + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + let(:recipients) { '#commits' } + let(:colorize_messages) { '1' } + + before do + irker.stub( + active: true, + project: project, + project_id: project.id, + service_hook: true, + properties: { + 'recipients' => recipients, + 'colorize_messages' => colorize_messages + } + ) + irker.settings = { + server_ip: 'localhost', + server_port: 6659, + max_channels: 3, + default_irc_uri: 'irc://chat.freenode.net/' + } + irker.valid? + @irker_server = TCPServer.new 'localhost', 6659 + end + + after do + @irker_server.close + end + + it 'should send valid JSON messages to an Irker listener' do + irker.execute(sample_data) + + conn = @irker_server.accept + conn.readlines.each do |line| + msg = JSON.load(line.chomp("\n")) + msg.keys.should match_array(['to', 'privmsg']) + if msg['to'].is_a?(String) + msg['to'].should == 'irc://chat.freenode.net/#commits' + else + msg['to'].should match_array(['irc://chat.freenode.net/#commits']) + end + end + conn.close + end + end +end -- GitLab From 4bc5c66fe12a2dc5d8fe9ed5878da5dea2444442 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 1 Mar 2015 20:15:59 -0800 Subject: [PATCH 1138/1609] Fix broken `project_url` routing when protected branches are accessed with an empty repo --- app/helpers/gitlab_routing_helper.rb | 16 ++++++++++++++++ .../protected_branches_controller_spec.rb | 10 ++++++++++ 2 files changed, 26 insertions(+) create mode 100644 spec/controllers/projects/protected_branches_controller_spec.rb diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index f0eb50a0e17..ac37f909ce9 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -28,4 +28,20 @@ module GitlabRoutingHelper def merge_request_path(entity, *args) namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args) end + + def project_url(project, *args) + namespace_project_url(project.namespace, project, *args) + end + + def edit_project_url(project, *args) + edit_namespace_project_url(project.namespace, project, *args) + end + + def issue_url(entity, *args) + namespace_project_issue_url(entity.project.namespace, entity.project, entity, *args) + end + + def merge_request_url(entity, *args) + namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) + end end diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb new file mode 100644 index 00000000000..596d8d34b7c --- /dev/null +++ b/spec/controllers/projects/protected_branches_controller_spec.rb @@ -0,0 +1,10 @@ +require('spec_helper') + +describe Projects::ProtectedBranchesController do + describe "GET #index" do + let(:project) { create(:project_empty_repo, :public) } + it "redirect empty repo to projects page" do + get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param) + end + end +end -- GitLab From 8d0690c5c768415a5dae1155c236f7650ea894cf Mon Sep 17 00:00:00 2001 From: Nicolas Bouilleaud Date: Wed, 17 Dec 2014 15:26:43 +0100 Subject: [PATCH 1139/1609] Support names starting with a digit or _ for projects and users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is what’s actually allowed when creating a user or a project in gitlab. --- CHANGELOG | 1 + lib/gitlab/markdown.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index dae32953cd9..6bd93b8cd4b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 7.8.1 - Fix urls for the issues when relative url was enabled - Add Bitbucket omniauth provider. - Add Bitbucket importer. + - Support referencing issues to a project whose name starts with a digit v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index a1fd794aed2..d85c2ee4f2d 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -121,7 +121,7 @@ module Gitlab text end - NAME_STR = '[a-zA-Z][a-zA-Z0-9_\-\.]*' + NAME_STR = '[a-zA-Z0-9_][a-zA-Z0-9_\-\.]*' PROJ_STR = "(?#{NAME_STR}/#{NAME_STR})" REFERENCE_PATTERN = %r{ -- GitLab From dd37a10df44bd1771aa8b163fd857628d03842d9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 23 Feb 2015 23:14:57 +0100 Subject: [PATCH 1140/1609] Don't leak information about private project existence via Git-over-SSH/HTTP. --- lib/api/internal.rb | 39 +++++++++++++----------- lib/gitlab/backend/grack_auth.rb | 52 +++++++++++++++++--------------- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/lib/api/internal.rb b/lib/api/internal.rb index ba3fe619b92..753d0fcbd98 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -16,6 +16,17 @@ module API # post "/allowed" do status 200 + + actor = if params[:key_id] + Key.find_by(id: params[:key_id]) + elsif params[:user_id] + User.find_by(id: params[:user_id]) + end + + unless actor + return Gitlab::GitAccessStatus.new(false, 'No such user or key') + end + project_path = params[:project] # Check for *.wiki repositories. @@ -32,26 +43,20 @@ module API project = Project.find_with_namespace(project_path) - unless project - return Gitlab::GitAccessStatus.new(false, 'No such project') + if project + status = access.check( + actor, + params[:action], + project, + params[:changes] + ) end - actor = if params[:key_id] - Key.find_by(id: params[:key_id]) - elsif params[:user_id] - User.find_by(id: params[:user_id]) - end - - unless actor - return Gitlab::GitAccessStatus.new(false, 'No such user or key') + if project && status && status.allowed? + status + else + Gitlab::GitAccessStatus.new(false, 'No such project') end - - access.check( - actor, - params[:action], - project, - params[:changes] - ) end # diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index dc4b945f9d4..ee877e099b1 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -10,8 +10,9 @@ module Grack @request = Rack::Request.new(env) @auth = Request.new(env) - # Need this patch due to the rails mount + @gitlab_ci = false + # Need this patch due to the rails mount # Need this if under RELATIVE_URL_ROOT unless Gitlab.config.gitlab.relative_url_root.empty? # If website is mounted using relative_url_root need to remove it first @@ -22,8 +23,12 @@ module Grack @env['SCRIPT_NAME'] = "" - if project - auth! + auth! + + if project && authorized_request? + @app.call(env) + elsif @user.nil? && !@gitlab_ci + unauthorized else render_not_found end @@ -32,35 +37,30 @@ module Grack private def auth! - if @auth.provided? - return bad_request unless @auth.basic? - - # Authentication with username and password - login, password = @auth.credentials + return unless @auth.provided? - # Allow authentication for GitLab CI service - # if valid token passed - if gitlab_ci_request?(login, password) - return @app.call(env) - end + return bad_request unless @auth.basic? - @user = authenticate_user(login, password) + # Authentication with username and password + login, password = @auth.credentials - if @user - Gitlab::ShellEnv.set_env(@user) - @env['REMOTE_USER'] = @auth.username - end + # Allow authentication for GitLab CI service + # if valid token passed + if gitlab_ci_request?(login, password) + @gitlab_ci = true + return end - if authorized_request? - @app.call(env) - else - unauthorized + @user = authenticate_user(login, password) + + if @user + Gitlab::ShellEnv.set_env(@user) + @env['REMOTE_USER'] = @auth.username end end def gitlab_ci_request?(login, password) - if login == "gitlab-ci-token" && project.gitlab_ci? + if login == "gitlab-ci-token" && project && project.gitlab_ci? token = project.gitlab_ci_service.token if token.present? && token == password && git_cmd == 'git-upload-pack' @@ -107,6 +107,8 @@ module Grack end def authorized_request? + return true if @gitlab_ci + case git_cmd when *Gitlab::GitAccess::DOWNLOAD_COMMANDS if user @@ -141,7 +143,9 @@ module Grack end def project - @project ||= project_by_path(@request.path_info) + return @project if defined?(@project) + + @project = project_by_path(@request.path_info) end def project_by_path(path) -- GitLab From 643afcbe00b766f786e6c7bac6cbd55870159df1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Mar 2015 14:02:09 -0800 Subject: [PATCH 1141/1609] Reduce amount of sql queries on dashboard projects page --- app/controllers/dashboard_controller.rb | 2 +- app/views/dashboard/projects.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index eca7b39bcdf..4930029e165 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -47,7 +47,7 @@ class DashboardController < ApplicationController @projects = @projects.where(namespace_id: Group.find_by(name: params[:group])) if params[:group].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? - @projects = @projects.includes(:namespace) + @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]).per(30) diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml index 15db8592547..03d4b3d8bbb 100644 --- a/app/views/dashboard/projects.html.haml +++ b/app/views/dashboard/projects.html.haml @@ -16,7 +16,7 @@ %li.my-project-row %h4.project-title .pull-left - = project_icon("#{project.namespace.to_param}/#{project.to_param}", alt: '', class: 'avatar project-avatar s60') + = project_icon(project, alt: '', class: 'avatar project-avatar s60') .project-access-icon = visibility_level_icon(project.visibility_level) = link_to project_path(project), class: dom_class(project) do -- GitLab From b8c9257fb1dc70cd65a14cb7dec62455ea54e394 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 2 Mar 2015 15:05:23 -0800 Subject: [PATCH 1142/1609] Fix bug where editing a comment with "+1" or "-1" would cause a server error Closes #1151 --- CHANGELOG | 1 + app/controllers/projects/notes_controller.rb | 8 +++++++- app/helpers/notes_helper.rb | 6 +++--- app/views/projects/notes/_edit_form.html.haml | 1 + app/views/projects/notes/_form.html.haml | 2 +- features/project/commits/comments.feature | 6 ++++++ features/project/issues/issues.feature | 9 +++++++++ features/steps/shared/note.rb | 17 +++++++++++++++++ 8 files changed, 45 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6bd93b8cd4b..e26d4ab690e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) + - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) - Move labels/milestones tabs to sidebar - Upgrade Rails gem to version 4.1.9. - Improve error messages for file edit failures diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 2f1d631c14a..868629a0bc4 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -3,10 +3,10 @@ class Projects::NotesController < Projects::ApplicationController before_filter :authorize_read_note! before_filter :authorize_write_note!, only: [:create] before_filter :authorize_admin_note!, only: [:update, :destroy] + before_filter :find_current_user_notes, except: [:destroy, :delete_attachment] def index current_fetched_at = Time.now.to_i - @notes = NotesFinder.new.execute(project, current_user, params) notes_json = { notes: [], last_fetched_at: current_fetched_at } @@ -116,4 +116,10 @@ class Projects::NotesController < Projects::ApplicationController :attachment, :line_code, :commit_id ) end + + private + + def find_current_user_notes + @notes = NotesFinder.new.execute(project, current_user, params) + end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 92ecb2abe4d..ab44fa6ee43 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -4,9 +4,9 @@ module NotesHelper (@noteable.class.name == note.noteable_type && !note.for_diff_line?) end - def note_target_fields - hidden_field_tag(:target_type, @target_type) + - hidden_field_tag(:target_id, @target_id) + def note_target_fields(note) + hidden_field_tag(:target_type, note.noteable.class.name.underscore) + + hidden_field_tag(:target_id, note.noteable.id) end def link_to_commit_diff_line_note(note) diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index b51ca795418..acb3991d294 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,5 +1,6 @@ .note-edit-form = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f| + = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 4476337cb10..be96c302143 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,5 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f| - = note_target_fields + = note_target_fields(@note) = f.hidden_field :commit_id = f.hidden_field :line_code = f.hidden_field :noteable_id diff --git a/features/project/commits/comments.feature b/features/project/commits/comments.feature index afcf0fdbb07..c41075d7ad4 100644 --- a/features/project/commits/comments.feature +++ b/features/project/commits/comments.feature @@ -41,3 +41,9 @@ Feature: Project Commits Comments Given I leave a comment like "XML attached" And I delete a comment Then I should not see a comment saying "XML attached" + + @javascript + Scenario: I can edit a comment with +1 + Given I leave a comment like "XML attached" + And I edit the last comment with a +1 + Then I should see +1 in the description diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 28ea44530fe..283979204db 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -139,6 +139,15 @@ Feature: Project Issues And I leave a comment with task markdown Then I should not see task checkboxes in the comment + @javascript + Scenario: Issue notes should be editable with +1 + Given project "Shop" has "Tasks-open" open issue with task markdown + When I visit issue page "Tasks-open" + And I leave a comment with a header containing "Comment with a header" + Then The comment with the header should not have an ID + And I edit the last comment with a +1 + Then I should see +1 in the description + # Task status in issues list Scenario: Issues list should display task status diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 45773056953..583746d4475 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -135,4 +135,21 @@ module SharedNote 'li.note div.timeline-content input[type="checkbox"]' ) end + + step 'I edit the last comment with a +1' do + find(".note").hover + find('.js-note-edit').click + + within(".current-note-edit-form") do + fill_in 'note[note]', with: '+1 Awesome!' + click_button 'Save Comment' + sleep 0.05 + end + end + + step 'I should see +1 in the description' do + within(".note") do + page.should have_content("+1 Awesome!") + end + end end -- GitLab From 38c52b1be6ca05862db0f7e8c4931bebc873bf75 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Mon, 2 Mar 2015 16:11:39 -0700 Subject: [PATCH 1143/1609] Fix checkbox alignment in application settings Add the form-control CSS class to the feature checkboxes on the application settings page to fix the vertical alignment with their labels. Also add aria-describedby attributes to form controls that have a help text block. --- CHANGELOG | 1 + .../application_settings/_form.html.haml | 30 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dae32953cd9..7e5fd3940dc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 7.9.0 (unreleased) - Fix ordering of imported but unchanged projects (Marco Wessel) - Mobile UI improvements: make aside content expandable - Expose avatar_url in projects API + - Fix checkbox alignment on the application settings page. - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) - Fix mass-unassignment of issues (Robert Speicher) - Allow user confirmation to be skipped for new users via API diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index f528d69f431..ac64d26f9aa 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -8,39 +8,39 @@ %fieldset %legend Features .form-group - = f.label :signup_enabled, class: 'control-label' + = f.label :signup_enabled, class: 'control-label col-sm-2' .col-sm-10 - = f.check_box :signup_enabled, class: 'checkbox' + = f.check_box :signup_enabled, class: 'checkbox form-control' .form-group - = f.label :signin_enabled, class: 'control-label' + = f.label :signin_enabled, class: 'control-label col-sm-2' .col-sm-10 - = f.check_box :signin_enabled, class: 'checkbox' + = f.check_box :signin_enabled, class: 'checkbox form-control' .form-group - = f.label :gravatar_enabled, class: 'control-label' + = f.label :gravatar_enabled, class: 'control-label col-sm-2' .col-sm-10 - = f.check_box :gravatar_enabled, class: 'checkbox' + = f.check_box :gravatar_enabled, class: 'checkbox form-control' .form-group - = f.label :twitter_sharing_enabled, "Twitter enabled", class: 'control-label' + = f.label :twitter_sharing_enabled, "Twitter enabled", class: 'control-label col-sm-2' .col-sm-10 - = f.check_box :twitter_sharing_enabled, class: 'checkbox' - %span.help-block Show users button to share their newly created public or internal projects on twitter + = f.check_box :twitter_sharing_enabled, class: 'checkbox form-control', :'aria-describedby' => 'twitter_help_block' + %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter %fieldset %legend Misc .form-group - = f.label :default_projects_limit, class: 'control-label' + = f.label :default_projects_limit, class: 'control-label col-sm-2' .col-sm-10 = f.number_field :default_projects_limit, class: 'form-control' .form-group - = f.label :default_branch_protection, class: 'control-label' + = f.label :default_branch_protection, class: 'control-label col-sm-2' .col-sm-10 = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' .form-group - = f.label :home_page_url, class: 'control-label' + = f.label :home_page_url, class: 'control-label col-sm-2' .col-sm-10 - = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com' - %span.help-block We will redirect non-logged in users to this page + = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' + %span.help-block#home_help_block We will redirect non-logged in users to this page .form-group - = f.label :sign_in_text, class: 'control-label' + = f.label :sign_in_text, class: 'control-label col-sm-2' .col-sm-10 = f.text_area :sign_in_text, class: 'form-control', rows: 4 .help-block Markdown enabled -- GitLab From 5b2b9a1f1fcfa323ae56d0d0214ff61bb6088321 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Mar 2015 17:28:47 -0800 Subject: [PATCH 1144/1609] Add brakeman gem --- Gemfile | 1 + Gemfile.lock | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/Gemfile b/Gemfile index 47e6fe95f34..01c02b5c8db 100644 --- a/Gemfile +++ b/Gemfile @@ -199,6 +199,7 @@ gem "virtus" gem 'addressable' group :development do + gem 'brakeman', require: false gem "annotate", "~> 2.6.0.beta2" gem "letter_opener" gem 'quiet_assets', '~> 1.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index 37880c45a29..102d1a28875 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -63,6 +63,16 @@ GEM bootstrap-sass (3.3.3) autoprefixer-rails (>= 5.0.0.1) sass (>= 3.2.19) + brakeman (3.0.1) + erubis (~> 2.6) + fastercsv (~> 1.5) + haml (>= 3.0, < 5.0) + highline (~> 1.6.20) + multi_json (~> 1.2) + ruby2ruby (~> 2.1.1) + ruby_parser (~> 3.5.0) + sass (~> 3.0) + terminal-table (~> 1.4) browser (0.7.2) builder (3.2.2) byebug (3.2.0) @@ -154,6 +164,7 @@ GEM multipart-post (~> 1.2.0) faraday_middleware (0.9.0) faraday (>= 0.7.4, < 0.9) + fastercsv (1.5.5) ffaker (1.22.1) ffi (1.9.3) fog (1.21.0) @@ -258,6 +269,7 @@ GEM haml (>= 3.1, < 5.0) railties (>= 4.0.1) hashie (2.1.2) + highline (1.6.21) hike (1.2.3) hipchat (1.4.0) httparty @@ -496,6 +508,11 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) ruby-progressbar (1.7.1) + ruby2ruby (2.1.3) + ruby_parser (~> 3.1) + sexp_processor (~> 4.0) + ruby_parser (3.5.0) + sexp_processor (~> 4.1) rubyntlm (0.4.0) rubypants (0.2.0) rugged (0.21.4) @@ -521,6 +538,7 @@ GEM select2-rails (3.5.2) thor (~> 0.14) settingslogic (2.0.9) + sexp_processor (4.4.5) shoulda-matchers (2.7.0) activesupport (>= 3.0.0) sidekiq (3.3.0) @@ -572,6 +590,7 @@ GEM temple (0.6.7) term-ansicolor (1.2.2) tins (~> 0.8) + terminal-table (1.4.5) test_after_commit (0.2.2) therubyracer (0.12.0) libv8 (~> 3.16.14.0) @@ -651,6 +670,7 @@ DEPENDENCIES better_errors binding_of_caller bootstrap-sass (~> 3.0) + brakeman browser byebug cal-heatmap-rails (~> 0.0.1) -- GitLab From cc877c53abbb1a8799b35dddac35b963dd5ecfdd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Mar 2015 17:41:05 -0800 Subject: [PATCH 1145/1609] Add rake task for brakeman --- lib/tasks/brakeman.rake | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 lib/tasks/brakeman.rake diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake new file mode 100644 index 00000000000..0a1e76ea822 --- /dev/null +++ b/lib/tasks/brakeman.rake @@ -0,0 +1,9 @@ +desc 'Security check via brakeman' +task :brakeman do + if system("brakeman -w3 -z") + exit 0 + else + puts 'Security check failed' + exit 1 + end +end -- GitLab From 16e899ca8b44a87883464ada507f521d02548fe2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Mar 2015 18:11:50 -0800 Subject: [PATCH 1146/1609] Add brakeman rake task and improve code security --- .../projects/imports_controller.rb | 2 +- .../projects/team_members_controller.rb | 8 +--- app/controllers/projects/wikis_controller.rb | 2 +- app/controllers/uploads_controller.rb | 41 ++++++++++++++++--- lib/tasks/brakeman.rake | 2 +- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index e2f957a640c..79d9910ce87 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -26,7 +26,7 @@ class Projects::ImportsController < Projects::ApplicationController def show unless @project.import_in_progress? if @project.import_finished? - redirect_to(@project) and return + redirect_to(project_path(@project)) and return else redirect_to new_namespace_project_import_path(@project.namespace, @project) && return diff --git a/app/controllers/projects/team_members_controller.rb b/app/controllers/projects/team_members_controller.rb index 71b0ab7ee82..f8a248ed729 100644 --- a/app/controllers/projects/team_members_controller.rb +++ b/app/controllers/projects/team_members_controller.rb @@ -15,15 +15,9 @@ class Projects::TeamMembersController < Projects::ApplicationController def create users = User.where(id: params[:user_ids].split(',')) - @project.team << [users, params[:access_level]] - if params[:redirect_to] - redirect_to params[:redirect_to] - else - redirect_to namespace_project_team_index_path(@project.namespace, - @project) - end + redirect_to namespace_project_team_index_path(@project.namespace, @project) end def update diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 69824dca944..3392fbca91e 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -97,7 +97,7 @@ class Projects::WikisController < Projects::ApplicationController @project_wiki.wiki rescue ProjectWiki::CouldNotCreateWikiError => ex flash[:notice] = "Could not create Wiki Repository at this time. Please try again later." - redirect_to @project + redirect_to project_path(@project) return false end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index b096c3913e1..810ac9f34bd 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -3,22 +3,53 @@ class UploadsController < ApplicationController before_filter :authorize_access def show - model = params[:model].camelize.constantize.find(params[:id]) - uploader = model.send(params[:mounted_as]) + unless upload_model && upload_mount + return not_found! + end - return not_found! if model.respond_to?(:project) && !can?(current_user, :read_project, model.project) + model = upload_model.find(params[:id]) + uploader = model.send(upload_mount) - return redirect_to uploader.url unless uploader.file_storage? + if model.respond_to?(:project) && !can?(current_user, :read_project, model.project) + return not_found! + end - return not_found! unless uploader.file.exists? + unless uploader.file_storage? + return redirect_to uploader.url + end + + unless uploader.file.exists? + return not_found! + end disposition = uploader.image? ? 'inline' : 'attachment' send_file uploader.file.path, disposition: disposition end + private + def authorize_access unless params[:mounted_as] == 'avatar' authenticate_user! && reject_blocked! end end + + def upload_model + upload_models = { + user: User, + project: Project, + note: Note, + group: Group + } + + upload_models[params[:model].to_sym] + end + + def upload_mount + upload_mounts = %w(avatar attachment file) + + if upload_mounts.include?(params[:mounted_as]) + params[:mounted_as] + end + end end diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake index 0a1e76ea822..abcb5f0ae46 100644 --- a/lib/tasks/brakeman.rake +++ b/lib/tasks/brakeman.rake @@ -1,6 +1,6 @@ desc 'Security check via brakeman' task :brakeman do - if system("brakeman -w3 -z") + if system("brakeman --skip-files lib/backup/repository.rb -w3 -z") exit 0 else puts 'Security check failed' -- GitLab From be165b18d0f3713a888767550ef66917c5a389ab Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Mar 2015 18:22:37 -0800 Subject: [PATCH 1147/1609] Add brakeman and jasmine --- CHANGELOG | 1 + lib/tasks/test.rake | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 84bdea30979..6a28772097e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 7.9.0 (unreleased) - Fix mass-unassignment of issues (Robert Speicher) - Allow user confirmation to be skipped for new users via API - Add a service to send updates to an Irker gateway (Romain Coltel) + - Add brakeman (security scanner for Ruby on Rails) v 7.8.1 - Fix run of custom post receive hooks diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index 3ea9290a814..a39d9649876 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -9,5 +9,5 @@ unless Rails.env.production? require 'coveralls/rake/task' Coveralls::RakeTask.new desc "GITLAB | Run all tests on CI with simplecov" - task :test_ci => [:rubocop, :spinach, :spec, 'coveralls:push'] + task :test_ci => [:rubocop, :brakeman, 'jasmine:ci', :spinach, :spec, 'coveralls:push'] end -- GitLab From f850cff4174bfe99a6f2ef0da365bf002990ad92 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Mar 2015 18:34:29 -0800 Subject: [PATCH 1148/1609] Update ci setup documenation --- doc/development/ci_setup.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md index f417667754e..f9b48868182 100644 --- a/doc/development/ci_setup.md +++ b/doc/development/ci_setup.md @@ -37,7 +37,10 @@ bundle install --deployment --path vendor/bundle (Setup) cp config/gitlab.yml.example config/gitlab.yml (Setup) bundle exec rake db:create (Setup) bundle exec rake spinach (Thread #1) -bundle exec rake spec (Thread #2) +bundle exec rake spec (thread #2) +bundle exec rake rubocop (thread #3) +bundle exec rake brakeman (thread #4) +bundle exec rake jasmine:ci (thread #5) ``` Use rubygems mirror. -- GitLab From 8348e1a9b57b042e97f14d3b4f7682806902efa1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Mar 2015 18:45:28 -0800 Subject: [PATCH 1149/1609] Enable ParenthesesAsGroupedExpression rule --- .rubocop.yml | 2 +- lib/api/entities.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index a4b51008194..53ca2ca2191 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -877,7 +877,7 @@ Lint/ParenthesesAsGroupedExpression: Checks for method calls with a space before the opening parenthesis. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' - Enabled: false + Enabled: true Lint/RequireParentheses: Description: >- diff --git a/lib/api/entities.rb b/lib/api/entities.rb index af76f3c439e..489be210784 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -143,7 +143,7 @@ module API class ProjectEntity < Grape::Entity expose :id, :iid - expose (:project_id) { |entity| entity.project.id } + expose(:project_id) { |entity| entity.project.id } expose :title, :description expose :state, :created_at, :updated_at end -- GitLab From f438791721360e547f3661a553f491d258013eb8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 2 Mar 2015 23:06:59 -0800 Subject: [PATCH 1150/1609] Fix import check for case sensetive namespaces --- app/controllers/import/base_controller.rb | 2 +- app/models/namespace.rb | 5 +++++ spec/models/namespace_spec.rb | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 4df171dbcfe..7dc0cac8d4c 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -3,7 +3,7 @@ class Import::BaseController < ApplicationController private def get_or_create_namespace - existing_namespace = Namespace.find_by("path = ? OR name = ?", @target_namespace, @target_namespace) + existing_namespace = Namespace.find_by_path_or_name(@target_namespace) if existing_namespace if existing_namespace.owner == current_user diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 2c7ed376265..35280889a86 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -48,6 +48,11 @@ class Namespace < ActiveRecord::Base where('lower(path) = :value', value: path.downcase).first end + # Case insensetive search for namespace by path or name + def self.find_by_path_or_name(path) + find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) + end + def self.search(query) where("name LIKE :query OR path LIKE :query", query: "%#{query}%") end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 4e268f8d8fa..ed6845c82cc 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -75,4 +75,14 @@ describe Namespace do expect(namespace.rm_dir).to be_truthy end end + + describe :find_by_path_or_name do + before do + @namespace = create(:namespace, name: 'WoW', path: 'woW') + end + + it { expect(Namespace.find_by_path_or_name('wow')).to eq(@namespace) } + it { expect(Namespace.find_by_path_or_name('WOW')).to eq(@namespace) } + it { expect(Namespace.find_by_path_or_name('unknown')).to eq(nil) } + end end -- GitLab From 6f71f5bb1be3d3c8ea2364a7763c62421de28f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A8s=20Koetsier?= Date: Tue, 3 Mar 2015 10:28:44 +0100 Subject: [PATCH 1151/1609] Fixed changelog for MR 8501 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 6a28772097e..4e56fc8e19b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 7.9.0 (unreleased) - Allow user confirmation to be skipped for new users via API - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) + - Slack username and channel options v 7.8.1 - Fix run of custom post receive hooks @@ -92,7 +93,6 @@ v 7.8.0 - Improve database performance for GitLab - Add Asana service (Jeremy Benoist) - Improve project web hooks with extra data - - Slack username and channel options v 7.7.2 - Update GitLab Shell to version 2.4.2 that fixes a bug when developers can push to protected branch -- GitLab From 0e11be40c39df66859ae0f3dc265cd903820c153 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 16:05:39 +0100 Subject: [PATCH 1152/1609] Add tests for GrackAuth. --- spec/lib/gitlab/backend/grack_auth_spec.rb | 146 +++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 spec/lib/gitlab/backend/grack_auth_spec.rb diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb new file mode 100644 index 00000000000..768312f0028 --- /dev/null +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -0,0 +1,146 @@ +require "spec_helper" + +describe Grack::Auth do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:app) { lambda { |env| [200, {}, "Success!"] } } + let!(:auth) { Grack::Auth.new(app) } + let(:env) { + { + "rack.input" => "", + "REQUEST_METHOD" => "GET", + "QUERY_STRING" => "service=git-upload-pack" + } + } + let(:status) { auth.call(env).first } + + describe "#call" do + context "when the project doesn't exist" do + before do + env["PATH_INFO"] = "doesnt/exist.git" + end + + context "when no authentication is provided" do + it "responds with status 401" do + expect(status).to eq(401) + end + end + + context "when username and password are provided" do + context "when authentication fails" do + before do + env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope") + end + + it "responds with status 401" do + expect(status).to eq(401) + end + end + + context "when authentication succeeds" do + before do + env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) + end + + it "responds with status 404" do + expect(status).to eq(404) + end + end + end + end + + context "when the project exists" do + before do + env["PATH_INFO"] = project.path_with_namespace + ".git" + end + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + it "responds with status 200" do + expect(status).to eq(200) + end + end + + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context "when no authentication is provided" do + it "responds with status 401" do + expect(status).to eq(401) + end + end + + context "when username and password are provided" do + context "when authentication fails" do + before do + env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope") + end + + it "responds with status 401" do + expect(status).to eq(401) + end + end + + context "when authentication succeeds" do + before do + env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) + end + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + before do + user.block + project.team << [user, :master] + end + + it "responds with status 404" do + expect(status).to eq(404) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + expect(status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + expect(status).to eq(404) + end + end + end + end + + context "when a gitlab ci token is provided" do + let(:token) { "123" } + + before do + gitlab_ci_service = project.build_gitlab_ci_service + gitlab_ci_service.active = true + gitlab_ci_service.token = token + gitlab_ci_service.project_url = "http://google.com" + gitlab_ci_service.save + + env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token) + end + + it "responds with status 200" do + expect(status).to eq(200) + end + end + end + end + end +end -- GitLab From afe5d7d209a4088d71e35d6382e6523b89f94ebe Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 19 Feb 2015 05:02:57 +0000 Subject: [PATCH 1153/1609] Issue #595: Support Slack notifications upon issue and merge request events 1) Adds a DB migration for all services to toggle on push, issue, and merge events. 2) Upon an issue or merge request event, fire service hooks. 3) Slack service supports custom messages for each of these events. Other services not supported at the moment. 4) Label merge request hooks with their corresponding actions. --- CHANGELOG | 3 + .../projects/services_controller.rb | 3 +- app/models/project.rb | 5 +- app/models/project_services/asana_service.rb | 7 ++ .../project_services/assembla_service.rb | 11 +- app/models/project_services/bamboo_service.rb | 9 +- .../project_services/buildbox_service.rb | 7 ++ .../project_services/campfire_service.rb | 7 ++ app/models/project_services/ci_service.rb | 4 + .../emails_on_push_service.rb | 7 ++ .../project_services/flowdock_service.rb | 7 ++ .../project_services/gemnasium_service.rb | 7 ++ .../project_services/gitlab_ci_service.rb | 4 + .../gitlab_issue_tracker_service.rb | 4 + .../project_services/hipchat_service.rb | 7 ++ .../project_services/issue_tracker_service.rb | 7 ++ app/models/project_services/jira_service.rb | 4 + .../pivotaltracker_service.rb | 7 ++ .../project_services/pushover_service.rb | 7 ++ .../project_services/redmine_service.rb | 4 + app/models/project_services/slack_message.rb | 110 ------------------ .../slack_messages/slack_base_message.rb | 31 +++++ .../slack_messages/slack_issue_message.rb | 56 +++++++++ .../slack_messages/slack_merge_message.rb | 54 +++++++++ .../slack_messages/slack_push_message.rb | 110 ++++++++++++++++++ app/models/project_services/slack_service.rb | 38 +++++- .../project_services/teamcity_service.rb | 11 +- app/models/service.rb | 14 +++ app/services/git_push_service.rb | 2 +- app/services/issues/base_service.rb | 12 +- app/services/merge_requests/base_service.rb | 16 ++- app/views/projects/services/_form.html.haml | 32 +++++ .../20150219004514_add_events_to_services.rb | 8 ++ db/schema.rb | 8 +- lib/gitlab/push_data_builder.rb | 1 + .../project_services/assembla_service_spec.rb | 20 ++-- .../project_services/buildbox_service_spec.rb | 20 ++-- .../project_services/flowdock_service_spec.rb | 20 ++-- .../gemnasium_service_spec.rb | 20 ++-- .../gitlab_ci_service_spec.rb | 20 ++-- .../project_services/pushover_service_spec.rb | 20 ++-- .../slack_issue_message_spec.rb | 55 +++++++++ .../slack_merge_message_spec.rb | 50 ++++++++ .../slack_push_message_spec.rb} | 4 +- .../project_services/slack_service_spec.rb | 59 ++++++++-- spec/models/service_spec.rb | 4 + spec/services/git_push_service_spec.rb | 1 + 47 files changed, 722 insertions(+), 195 deletions(-) delete mode 100644 app/models/project_services/slack_message.rb create mode 100644 app/models/project_services/slack_messages/slack_base_message.rb create mode 100644 app/models/project_services/slack_messages/slack_issue_message.rb create mode 100644 app/models/project_services/slack_messages/slack_merge_message.rb create mode 100644 app/models/project_services/slack_messages/slack_push_message.rb create mode 100644 db/migrate/20150219004514_add_events_to_services.rb create mode 100644 spec/models/project_services/slack_messages/slack_issue_message_spec.rb create mode 100644 spec/models/project_services/slack_messages/slack_merge_message_spec.rb rename spec/models/project_services/{slack_message_spec.rb => slack_messages/slack_push_message_spec.rb} (94%) diff --git a/CHANGELOG b/CHANGELOG index 6a28772097e..8011817d0ae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. +v 7.9.0 (unreleased) + - Added issue and merge request events to Slack service (Stan Hu) + - Fix broken access control for note attachments (Hannes Rosenögger) v 7.9.0 (unreleased) - Move labels/milestones tabs to sidebar diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 82aad329c1a..087579de106 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -51,7 +51,8 @@ class Projects::ServicesController < Projects::ApplicationController :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :build_type, :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, - :colorize_messages, :channels + :colorize_messages, :channels, + :push_events, :issues_events, :merge_requests_events, :tag_push_events ) end end diff --git a/app/models/project.rb b/app/models/project.rb index 907f331d8f1..c45338bf4eb 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -479,8 +479,9 @@ class Project < ActiveRecord::Base end end - def execute_services(data) - services.select(&:active).each do |service| + def execute_services(data, hooks_scope = :push_hooks) + # Call only service hooks that are active for this scope + services.send(hooks_scope).each do |service| service.async_execute(data) end end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 66b72572b9c..2b530390aeb 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'asana' @@ -62,6 +66,9 @@ automatically inspected. Leave blank to include all branches.' end def execute(push) + object_kind = push[:object_kind] + return unless object_kind == "push" + Asana.configure do |client| client.api_key = api_key end diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index cf7598f35eb..01c647c1705 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class AssemblaService < Service @@ -38,8 +42,11 @@ class AssemblaService < Service ] end - def execute(push) + def execute(data) + object_kind = data[:object_kind] + return unless object_kind == "push" + url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}" - AssemblaService.post(url, body: { payload: push }.to_json, headers: { 'Content-Type' => 'application/json' }) + AssemblaService.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' }) end end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index df68803152f..6ff52af040d 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class BambooService < CiService @@ -118,7 +122,10 @@ class BambooService < CiService end end - def execute(_data) + def execute(data) + object_kind = data[:object_kind] + return unless object_kind == "push" + # Bamboo requires a GET and does not take any data. self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", verify: false) diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb index 058c890ae45..201bfc560a3 100644 --- a/app/models/project_services/buildbox_service.rb +++ b/app/models/project_services/buildbox_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require "addressable/uri" @@ -33,6 +37,9 @@ class BuildboxService < CiService end def execute(data) + object_kind = data[:object_kind] + return unless object_kind == "push" + service_hook.execute(data) end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 14b6b87a0b7..41ab6c56ad8 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class CampfireService < Service @@ -38,6 +42,9 @@ class CampfireService < Service end def execute(push_data) + object_kind = push_data[:object_kind] + return unless object_kind == "push" + room = gate.find_room_by_name(self.room) return true unless room diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 5a26c25b3c3..e58d6d7a233 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # # Base class for CI services diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 86693ad0c7e..28be15c3b35 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # class EmailsOnPushService < Service @@ -30,6 +34,9 @@ class EmailsOnPushService < Service end def execute(push_data) + object_kind = push_data[:object_kind] + return unless object_kind == "push" + EmailsOnPushWorker.perform_async(project_id, recipients, push_data) end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 13e2dfceb1a..9cc0e367882 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require "flowdock-git-hook" @@ -38,6 +42,9 @@ class FlowdockService < Service end def execute(push_data) + object_kind = push_data[:object_kind] + return unless object_kind == "push" + Flowdock::Git.post( push_data[:ref], push_data[:before], diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index a2c87ae88f1..130c9eaeb4b 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require "gemnasium/gitlab_service" @@ -39,6 +43,9 @@ class GemnasiumService < Service end def execute(push_data) + object_kind = push_data[:object_kind] + return unless object_kind == "push" + Gemnasium::GitlabService.execute( ref: push_data[:ref], before: push_data[:before], diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index f4b463e8199..a64b24b5ef1 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # class GitlabCiService < CiService diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 05c048e4e45..00f8d430fd5 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class GitlabIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 003e06a4c80..462478812a1 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # class HipchatService < Service @@ -41,6 +45,9 @@ class HipchatService < Service end def execute(push_data) + object_kind = push_data.fetch(:object_kind) + return unless object_kind == "push" + gate[room].send('GitLab', create_message(push_data)) end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index c991a34ecdb..0d9e5c13992 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class IssueTrackerService < Service @@ -66,6 +70,9 @@ class IssueTrackerService < Service end def execute(data) + object_kind = data[:object_kind] + return unless object_kind == "push" + message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again." result = false diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 4c056605ea8..20611eeb60c 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class JiraService < IssueTrackerService diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 287812c57a5..4bb2a978ed1 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class PivotaltrackerService < Service @@ -38,6 +42,9 @@ class PivotaltrackerService < Service end def execute(push) + object_kind = push[:object_kind] + return unless object_kind == "push" + url = 'https://www.pivotaltracker.com/services/v5/source_commits' push[:commits].each do |commit| message = { diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 3a3af59390a..4aa7e0afa77 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class PushoverService < Service @@ -77,6 +81,9 @@ class PushoverService < Service end def execute(push_data) + object_kind = push_data[:object_kind] + return unless object_kind == "push" + ref = push_data[:ref].gsub('refs/heads/', '') before = push_data[:before] after = push_data[:after] diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index e1dc10415e0..f96eae2daa1 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class RedmineService < IssueTrackerService diff --git a/app/models/project_services/slack_message.rb b/app/models/project_services/slack_message.rb deleted file mode 100644 index 6c6446db45f..00000000000 --- a/app/models/project_services/slack_message.rb +++ /dev/null @@ -1,110 +0,0 @@ -require 'slack-notifier' - -class SlackMessage - attr_reader :after - attr_reader :before - attr_reader :commits - attr_reader :project_name - attr_reader :project_url - attr_reader :ref - attr_reader :username - - def initialize(params) - @after = params.fetch(:after) - @before = params.fetch(:before) - @commits = params.fetch(:commits, []) - @project_name = params.fetch(:project_name) - @project_url = params.fetch(:project_url) - @ref = params.fetch(:ref).gsub('refs/heads/', '') - @username = params.fetch(:user_name) - end - - def pretext - format(message) - end - - def attachments - return [] if new_branch? || removed_branch? - - commit_message_attachments - end - - private - - def message - if new_branch? - new_branch_message - elsif removed_branch? - removed_branch_message - else - push_message - end - end - - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end - - def new_branch_message - "#{username} pushed new branch #{branch_link} to #{project_link}" - end - - def removed_branch_message - "#{username} removed branch #{ref} from #{project_link}" - end - - def push_message - "#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})" - end - - def commit_messages - commits.each_with_object('') do |commit, str| - str << compose_commit_message(commit) - end.chomp - end - - def commit_message_attachments - [{ text: format(commit_messages), color: attachment_color }] - end - - def compose_commit_message(commit) - author = commit.fetch(:author).fetch(:name) - id = commit.fetch(:id)[0..8] - message = commit.fetch(:message) - url = commit.fetch(:url) - - "[#{id}](#{url}): #{message} - #{author}\n" - end - - def new_branch? - before.include?('000000') - end - - def removed_branch? - after.include?('000000') - end - - def branch_url - "#{project_url}/commits/#{ref}" - end - - def compare_url - "#{project_url}/compare/#{before}...#{after}" - end - - def branch_link - "[#{ref}](#{branch_url})" - end - - def project_link - "[#{project_name}](#{project_url})" - end - - def compare_link - "[Compare changes](#{compare_url})" - end - - def attachment_color - '#345' - end -end diff --git a/app/models/project_services/slack_messages/slack_base_message.rb b/app/models/project_services/slack_messages/slack_base_message.rb new file mode 100644 index 00000000000..c2fc27884bf --- /dev/null +++ b/app/models/project_services/slack_messages/slack_base_message.rb @@ -0,0 +1,31 @@ +require 'slack-notifier' + +module SlackMessages + class SlackBaseMessage + def initialize(params) + raise NotImplementedError + end + + def pretext + format(message) + end + + def attachments + raise NotImplementedError + end + + private + + def message + raise NotImplementedError + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/slack_messages/slack_issue_message.rb b/app/models/project_services/slack_messages/slack_issue_message.rb new file mode 100644 index 00000000000..0c3a492aae7 --- /dev/null +++ b/app/models/project_services/slack_messages/slack_issue_message.rb @@ -0,0 +1,56 @@ +module SlackMessages + class SlackIssueMessage < SlackBaseMessage + attr_reader :username + attr_reader :title + attr_reader :project_name + attr_reader :project_url + attr_reader :issue_iid + attr_reader :issue_url + attr_reader :action + attr_reader :state + attr_reader :description + + def initialize(params) + @username = params[:user][:username] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @title = obj_attr[:title] + @issue_iid = obj_attr[:iid] + @issue_url = obj_attr[:url] + @action = obj_attr[:action] + @state = obj_attr[:state] + @description = obj_attr[:description] + end + + def attachments + return [] unless opened_issue? + + description_message + end + + private + + def message + "#{username} #{state} issue #{issue_link} in #{project_link}: #{title}" + end + + def opened_issue? + action == "open" + end + + def description_message + [{ text: format(description), color: attachment_color }] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def issue_link + "[##{issue_iid}](#{issue_url})" + end + end +end diff --git a/app/models/project_services/slack_messages/slack_merge_message.rb b/app/models/project_services/slack_messages/slack_merge_message.rb new file mode 100644 index 00000000000..bc49a963a9b --- /dev/null +++ b/app/models/project_services/slack_messages/slack_merge_message.rb @@ -0,0 +1,54 @@ +module SlackMessages + class SlackMergeMessage < SlackBaseMessage + attr_reader :username + attr_reader :project_name + attr_reader :project_url + attr_reader :merge_request_id + attr_reader :source_branch + attr_reader :target_branch + attr_reader :state + + def initialize(params) + @username = params[:user][:username] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @merge_request_id = obj_attr[:iid] + @source_branch = obj_attr[:source_branch] + @target_branch = obj_attr[:target_branch] + @state = obj_attr[:state] + end + + def pretext + format(message) + end + + def attachments + [] + end + + private + + def message + merge_request_message + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def merge_request_message + "#{username} #{state} merge request #{merge_request_link} in #{project_link}" + end + + def merge_request_link + "[##{merge_request_id}](#{merge_request_url})" + end + + def merge_request_url + "#{project_url}/merge_requests/#{merge_request_id}" + end + end +end diff --git a/app/models/project_services/slack_messages/slack_push_message.rb b/app/models/project_services/slack_messages/slack_push_message.rb new file mode 100644 index 00000000000..c7769bbeda1 --- /dev/null +++ b/app/models/project_services/slack_messages/slack_push_message.rb @@ -0,0 +1,110 @@ +require 'slack-notifier' + +module SlackMessages + class SlackPushMessage < SlackBaseMessage + attr_reader :after + attr_reader :before + attr_reader :commits + attr_reader :project_name + attr_reader :project_url + attr_reader :ref + attr_reader :username + + def initialize(params) + @after = params[:after] + @before = params[:before] + @commits = params.fetch(:commits, []) + @project_name = params[:project_name] + @project_url = params[:project_url] + @ref = params[:ref].gsub('refs/heads/', '') + @username = params[:user_name] + end + + def pretext + format(message) + end + + def attachments + return [] if new_branch? || removed_branch? + + commit_message_attachments + end + + private + + def message + if new_branch? + new_branch_message + elsif removed_branch? + removed_branch_message + else + push_message + end + end + + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def new_branch_message + "#{username} pushed new branch #{branch_link} to #{project_link}" + end + + def removed_branch_message + "#{username} removed branch #{ref} from #{project_link}" + end + + def push_message + "#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})" + end + + def commit_messages + commits.map { |commit| compose_commit_message(commit) }.join("\n") + end + + def commit_message_attachments + [{ text: format(commit_messages), color: attachment_color }] + end + + def compose_commit_message(commit) + author = commit[:author][:name] + id = Commit.truncate_sha(commit[:id]) + message = commit[:message] + url = commit[:url] + + "[#{id}](#{url}): #{message} - #{author}" + end + + def new_branch? + before.include?('000000') + end + + def removed_branch? + after.include?('000000') + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def compare_url + "#{project_url}/compare/#{before}...#{after}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def compare_link + "[Compare changes](#{compare_url})" + end + + def attachment_color + '#345' + end + end +end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index c7cbff63fe5..1318a1ed1b8 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -11,7 +11,14 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # +require "slack_messages/slack_issue_message" +require "slack_messages/slack_push_message" +require "slack_messages/slack_merge_message" class SlackService < Service prop_accessor :webhook, :username, :channel @@ -38,20 +45,37 @@ class SlackService < Service ] end - def execute(push_data) + def execute(data) return unless webhook.present? - message = SlackMessage.new(push_data.merge( + object_kind = data[:object_kind] + + data = data.merge( project_url: project_url, project_name: project_name - )) + ) + + # WebHook events often have an 'update' event that follows a 'open' or + # 'close' action. Ignore update events for now to prevent duplicate + # messages from arriving. + + message = case object_kind + when "push" + message = SlackMessages::SlackPushMessage.new(data) + when "issue" + message = SlackMessages::SlackIssueMessage.new(data) unless is_update?(data) + when "merge_request" + message = SlackMessages::SlackMergeMessage.new(data) unless is_update?(data) + end opt = {} opt[:channel] = channel if channel opt[:username] = username if username - notifier = Slack::Notifier.new(webhook, opt) - notifier.ping(message.pretext, attachments: message.attachments) + if message + notifier = Slack::Notifier.new(webhook, opt) + notifier.ping(message.pretext, attachments: message.attachments) + end end private @@ -63,4 +87,8 @@ class SlackService < Service def project_url project.web_url end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index b6932f1c77b..07facfb6d06 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class TeamcityService < CiService @@ -115,13 +119,16 @@ class TeamcityService < CiService end end - def execute(push) + def execute(data) + object_kind = data[:object_kind] + return unless object_kind == "push" + auth = { username: username, password: password, } - branch = push[:ref].gsub('refs/heads/', '') + branch = data[:ref].gsub('refs/heads/', '') self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue", body: ""\ diff --git a/app/models/service.rb b/app/models/service.rb index f4e97da3212..9d6866f26d0 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -11,6 +11,11 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean +# # To add new service you should build a class inherited from Service # and implement a set of methods @@ -19,6 +24,10 @@ class Service < ActiveRecord::Base serialize :properties, JSON default_value_for :active, false + default_value_for :push_events, true + default_value_for :issues_events, true + default_value_for :merge_requests_events, true + default_value_for :tag_push_events, true after_initialize :initialize_properties @@ -29,6 +38,11 @@ class Service < ActiveRecord::Base scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } + scope :push_hooks, -> { where(push_events: true, active: true) } + scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } + scope :issue_hooks, -> { where(issues_events: true, active: true) } + scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } + def activated? active end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index f21e6ac207d..13def127763 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -54,7 +54,7 @@ class GitPushService @push_data = post_receive_data(oldrev, newrev, ref) EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) - project.execute_services(@push_data.dup) + project.execute_services(@push_data.dup, :push_hooks) end end diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 755c0ef45a8..c3ca04a4343 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -1,13 +1,19 @@ module Issues class BaseService < ::IssuableBaseService - private - - def execute_hooks(issue, action = 'open') + def hook_data(issue, action) issue_data = issue.to_hook_data(current_user) issue_url = Gitlab::UrlBuilder.new(:issue).build(issue.id) issue_data[:object_attributes].merge!(url: issue_url, action: action) + issue_data + end + + private + + def execute_hooks(issue, action = 'open') + issue_data = hook_data(issue, action) issue.project.execute_hooks(issue_data, :issue_hooks) + issue.project.execute_services(issue_data, :issue_hooks) end end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index b4199d1c800..f6e1ae6f283 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -5,13 +5,19 @@ module MergeRequests Note.create_status_change_note(merge_request, merge_request.target_project, current_user, merge_request.state, nil) end + def hook_data(merge_request, action) + hook_data = merge_request.to_hook_data(current_user) + merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) + hook_data[:object_attributes][:url] = merge_request_url + hook_data[:object_attributes][:action] = action + hook_data + end + def execute_hooks(merge_request, action = 'open') if merge_request.project - hook_data = merge_request.to_hook_data(current_user) - merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - hook_data[:object_attributes][:url] = merge_request_url - hook_data[:object_attributes][:action] = action - merge_request.project.execute_hooks(hook_data, :merge_request_hooks) + merge_data = hook_data(merge_request, action) + merge_request.project.execute_hooks(merge_data, :merge_request_hooks) + merge_request.project.execute_services(merge_data, :merge_request_hooks) end end end diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 8db6d67e06b..0519c8150e9 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -27,6 +27,38 @@ .col-sm-10 = f.check_box :active + .form-group + = f.label :url, "Trigger", class: 'control-label' + .col-sm-10 + %div + = f.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = f.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + %div + = f.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = f.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + %div + = f.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = f.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This url will be triggered when an issue is created + %div + = f.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = f.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This url will be triggered when a merge request is created + - @service.fields.each do |field| - name = field[:name] - value = @service.send(name) unless field[:type] == 'password' diff --git a/db/migrate/20150219004514_add_events_to_services.rb b/db/migrate/20150219004514_add_events_to_services.rb new file mode 100644 index 00000000000..cf73a0174f4 --- /dev/null +++ b/db/migrate/20150219004514_add_events_to_services.rb @@ -0,0 +1,8 @@ +class AddEventsToServices < ActiveRecord::Migration + def change + add_column :services, :push_events, :boolean, :default => true + add_column :services, :issues_events, :boolean, :default => true + add_column :services, :merge_requests_events, :boolean, :default => true + add_column :services, :tag_push_events, :boolean, :default => true + end +end diff --git a/db/schema.rb b/db/schema.rb index 2659efe4df9..1a9b512e159 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -364,9 +364,13 @@ ActiveRecord::Schema.define(version: 20150223022001) do t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.boolean "active", default: false, null: false + t.boolean "active", default: false, null: false t.text "properties" - t.boolean "template", default: false + t.boolean "template", default: false + t.boolean "push_events", default: true + t.boolean "issues_events", default: true + t.boolean "merge_requests_events", default: true + t.boolean "tag_push_events", default: true end add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 9aa5c8967a7..9d8d3ea3d22 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -29,6 +29,7 @@ module Gitlab # Hash to be passed as post_receive_data data = { + object_kind: "push", before: oldrev, after: newrev, ref: ref, diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb index ee7f780c8f6..cd34e006ebe 100644 --- a/spec/models/project_services/assembla_service_spec.rb +++ b/spec/models/project_services/assembla_service_spec.rb @@ -2,14 +2,18 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require 'spec_helper' diff --git a/spec/models/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildbox_service_spec.rb index 050363e14c7..c246e1c9d41 100644 --- a/spec/models/project_services/buildbox_service_spec.rb +++ b/spec/models/project_services/buildbox_service_spec.rb @@ -2,14 +2,18 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require 'spec_helper' diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index b34e36bc940..2ec167a7330 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -2,14 +2,18 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require 'spec_helper' diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index fe5d62b2f53..5f665fadfff 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -2,14 +2,18 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require 'spec_helper' diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index 0cd255f08ea..fcb33b11732 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -2,14 +2,18 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require 'spec_helper' diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb index 188626a7a27..bb2e72c3ac1 100644 --- a/spec/models/project_services/pushover_service_spec.rb +++ b/spec/models/project_services/pushover_service_spec.rb @@ -2,14 +2,18 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require 'spec_helper' diff --git a/spec/models/project_services/slack_messages/slack_issue_message_spec.rb b/spec/models/project_services/slack_messages/slack_issue_message_spec.rb new file mode 100644 index 00000000000..49a8eea0a58 --- /dev/null +++ b/spec/models/project_services/slack_messages/slack_issue_message_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe SlackMessages::SlackIssueMessage do + subject { SlackMessages::SlackIssueMessage.new(args) } + + let(:args) { + { + user: { + username: 'username' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: 'Issue title', + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + action: 'open', + state: 'opened', + description: 'issue description' + } + } + } + + let(:color) { '#345' } + + context 'open' do + it 'returns a message regarding opening of issues' do + expect(subject.pretext).to eq( + 'username opened issue in : '\ + 'Issue title') + expect(subject.attachments).to eq([ + { + text: "issue description", + color: color, + } + ]) + end + end + + context 'close' do + before do + args[:object_attributes][:action] = 'close' + args[:object_attributes][:state] = 'closed' + end + it 'returns a message regarding closing of issues' do + expect(subject.pretext). to eq( + 'username closed issue in : '\ + 'Issue title') + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/slack_messages/slack_merge_message_spec.rb b/spec/models/project_services/slack_messages/slack_merge_message_spec.rb new file mode 100644 index 00000000000..ef76c3312ea --- /dev/null +++ b/spec/models/project_services/slack_messages/slack_merge_message_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe SlackMessages::SlackMergeMessage do + subject { SlackMessages::SlackMergeMessage.new(args) } + + let(:args) { + { + user: { + username: 'username' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + + object_attributes: { + title: 'Issue title', + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + state: 'opened', + description: 'issue description', + source_branch: 'source_branch', + target_branch: 'target_branch', + } + } + } + + let(:color) { '#345' } + + context 'open' do + it 'returns a message regarding opening of merge requests' do + expect(subject.pretext).to eq( + 'username opened merge request '\ + 'in ') + expect(subject.attachments).to be_empty + end + end + + context 'close' do + before do + args[:object_attributes][:state] = 'closed' + end + it 'returns a message regarding closing of merge requests' do + expect(subject.pretext).to eq( + 'username closed merge request '\ + 'in ') + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/slack_message_spec.rb b/spec/models/project_services/slack_messages/slack_push_message_spec.rb similarity index 94% rename from spec/models/project_services/slack_message_spec.rb rename to spec/models/project_services/slack_messages/slack_push_message_spec.rb index 7197a94e53f..f11614d6921 100644 --- a/spec/models/project_services/slack_message_spec.rb +++ b/spec/models/project_services/slack_messages/slack_push_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackMessage do - subject { SlackMessage.new(args) } +describe SlackMessages::SlackPushMessage do + subject { SlackMessages::SlackPushMessage.new(args) } let(:args) { { diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 8a75d8987ab..49c48d0b65c 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -2,14 +2,18 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require 'spec_helper' @@ -34,7 +38,7 @@ describe SlackService do let(:slack) { SlackService.new } let(:user) { create(:user) } let(:project) { create(:project) } - let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } let(:username) { 'slack_username' } let(:channel) { 'slack_channel' } @@ -48,10 +52,43 @@ describe SlackService do ) WebMock.stub_request(:post, webhook_url) + + opts = { + title: 'Awesome issue', + description: 'please fix' + } + + issue_service = Issues::CreateService.new(project, user, opts) + @issue = issue_service.execute + @issues_sample_data = issue_service.hook_data(@issue, 'open') + + opts = { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'stable', + target_branch: 'master' + } + merge_service = MergeRequests::CreateService.new(project, + user, opts) + @merge_request = merge_service.execute + @merge_sample_data = merge_service.hook_data(@merge_request, + 'open') end - it "should call Slack API" do - slack.execute(sample_data) + it "should call Slack API for pull requests" do + slack.execute(push_sample_data) + + WebMock.should have_requested(:post, webhook_url).once + end + + it "should call Slack API for issue events" do + slack.execute(@issues_sample_data) + + WebMock.should have_requested(:post, webhook_url).once + end + + it "should call Slack API for merge requests events" do + slack.execute(@merge_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 9a1248055b1..cc047a20dd2 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -11,6 +11,10 @@ # active :boolean default(FALSE), not null # properties :text # template :boolean default(FALSE) +# push_events :boolean +# issues_events :boolean +# merge_requests_events :boolean +# tag_push_events :boolean # require 'spec_helper' diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 9924935094e..e264072b573 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -49,6 +49,7 @@ describe GitPushService do subject { @push_data } + it { is_expected.to include(object_kind: 'push') } it { is_expected.to include(before: @oldrev) } it { is_expected.to include(after: @newrev) } it { is_expected.to include(ref: @ref) } -- GitLab From d9ff616fd803c8bd9d208c22d01770acc8d58a38 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 14:49:26 +0100 Subject: [PATCH 1154/1609] Code style, directory structure. --- app/models/project_services/asana_service.rb | 10 ++++---- .../project_services/campfire_service.rb | 6 ++--- .../emails_on_push_service.rb | 6 ++--- .../project_services/flowdock_service.rb | 10 ++++---- .../project_services/gemnasium_service.rb | 10 ++++---- .../project_services/hipchat_service.rb | 6 ++--- .../pivotaltracker_service.rb | 6 ++--- .../project_services/pushover_service.rb | 22 ++++++++--------- app/models/project_services/slack_service.rb | 24 ++++++++++--------- .../slack_base_message.rb | 4 ++-- .../slack_issue_message.rb | 4 ++-- .../slack_merge_message.rb | 4 ++-- .../slack_push_message.rb | 4 ++-- 13 files changed, 59 insertions(+), 57 deletions(-) rename app/models/project_services/{slack_messages => slack_service}/slack_base_message.rb (89%) rename app/models/project_services/{slack_messages => slack_service}/slack_issue_message.rb (94%) rename app/models/project_services/{slack_messages => slack_service}/slack_merge_message.rb (94%) rename app/models/project_services/{slack_messages => slack_service}/slack_push_message.rb (97%) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 2b530390aeb..a5686c48bc6 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -65,16 +65,16 @@ automatically inspected. Leave blank to include all branches.' ] end - def execute(push) - object_kind = push[:object_kind] + def execute(data) + object_kind = data[:object_kind] return unless object_kind == "push" Asana.configure do |client| client.api_key = api_key end - user = push[:user_name] - branch = push[:ref].gsub('refs/heads/', '') + user = data[:user_name] + branch = data[:ref].gsub('refs/heads/', '') branch_restriction = restrict_to_branch.to_s @@ -86,7 +86,7 @@ automatically inspected. Leave blank to include all branches.' project_name = project.name_with_namespace push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name - push[:commits].each do |commit| + data[:commits].each do |commit| check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg) end end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 41ab6c56ad8..7af6882329c 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -41,14 +41,14 @@ class CampfireService < Service ] end - def execute(push_data) - object_kind = push_data[:object_kind] + def execute(data) + object_kind = data[:object_kind] return unless object_kind == "push" room = gate.find_room_by_name(self.room) return true unless room - message = build_message(push_data) + message = build_message(data) room.speak(message) end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 28be15c3b35..1b7ce481c1f 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -33,11 +33,11 @@ class EmailsOnPushService < Service 'emails_on_push' end - def execute(push_data) - object_kind = push_data[:object_kind] + def execute(data) + object_kind = data[:object_kind] return unless object_kind == "push" - EmailsOnPushWorker.perform_async(project_id, recipients, push_data) + EmailsOnPushWorker.perform_async(project_id, recipients, data) end def fields diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 9cc0e367882..e4ea84cb617 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -41,14 +41,14 @@ class FlowdockService < Service ] end - def execute(push_data) - object_kind = push_data[:object_kind] + def execute(data) + object_kind = data[:object_kind] return unless object_kind == "push" Flowdock::Git.post( - push_data[:ref], - push_data[:before], - push_data[:after], + data[:ref], + data[:before], + data[:after], token: token, repo: project.repository.path_to_repo, repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}", diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 130c9eaeb4b..ada61b78047 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -42,14 +42,14 @@ class GemnasiumService < Service ] end - def execute(push_data) - object_kind = push_data[:object_kind] + def execute(data) + object_kind = data[:object_kind] return unless object_kind == "push" Gemnasium::GitlabService.execute( - ref: push_data[:ref], - before: push_data[:before], - after: push_data[:after], + ref: data[:ref], + before: data[:before], + after: data[:after], token: token, api_key: api_key, repo: project.repository.path_to_repo diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 462478812a1..965ecdc684b 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -44,11 +44,11 @@ class HipchatService < Service ] end - def execute(push_data) - object_kind = push_data.fetch(:object_kind) + def execute(data) + object_kind = data[:object_kind] return unless object_kind == "push" - gate[room].send('GitLab', create_message(push_data)) + gate[room].send('GitLab', create_message(data)) end private diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 4bb2a978ed1..dd9e7f35c17 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -41,12 +41,12 @@ class PivotaltrackerService < Service ] end - def execute(push) - object_kind = push[:object_kind] + def execute(data) + object_kind = data[:object_kind] return unless object_kind == "push" url = 'https://www.pivotaltracker.com/services/v5/source_commits' - push[:commits].each do |commit| + data[:commits].each do |commit| message = { 'source_commit' => { 'commit_id' => commit[:id], diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 4aa7e0afa77..a715e7b2cc5 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -80,24 +80,24 @@ class PushoverService < Service ] end - def execute(push_data) - object_kind = push_data[:object_kind] + def execute(data) + object_kind = data[:object_kind] return unless object_kind == "push" - ref = push_data[:ref].gsub('refs/heads/', '') - before = push_data[:before] - after = push_data[:after] + ref = data[:ref].gsub('refs/heads/', '') + before = data[:before] + after = data[:after] if before.include?('000000') - message = "#{push_data[:user_name]} pushed new branch \"#{ref}\"." + message = "#{data[:user_name]} pushed new branch \"#{ref}\"." elsif after.include?('000000') - message = "#{push_data[:user_name]} deleted branch \"#{ref}\"." + message = "#{data[:user_name]} deleted branch \"#{ref}\"." else - message = "#{push_data[:user_name]} push to branch \"#{ref}\"." + message = "#{data[:user_name]} push to branch \"#{ref}\"." end - if push_data[:total_commits_count] > 0 - message << "\nTotal commits count: #{push_data[:total_commits_count]}" + if data[:total_commits_count] > 0 + message << "\nTotal commits count: #{data[:total_commits_count]}" end pushover_data = { @@ -107,7 +107,7 @@ class PushoverService < Service priority: priority, title: "#{project.name_with_namespace}", message: message, - url: push_data[:repository][:homepage], + url: data[:repository][:homepage], url_title: "See project #{project.name_with_namespace}" } diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 1318a1ed1b8..8289e474031 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -16,9 +16,6 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # -require "slack_messages/slack_issue_message" -require "slack_messages/slack_push_message" -require "slack_messages/slack_merge_message" class SlackService < Service prop_accessor :webhook, :username, :channel @@ -59,14 +56,15 @@ class SlackService < Service # 'close' action. Ignore update events for now to prevent duplicate # messages from arriving. - message = case object_kind - when "push" - message = SlackMessages::SlackPushMessage.new(data) - when "issue" - message = SlackMessages::SlackIssueMessage.new(data) unless is_update?(data) - when "merge_request" - message = SlackMessages::SlackMergeMessage.new(data) unless is_update?(data) - end + message = \ + case object_kind + when "push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + end opt = {} opt[:channel] = channel if channel @@ -92,3 +90,7 @@ class SlackService < Service data[:object_attributes][:action] == 'update' end end + +require "slack_service/issue_message" +require "slack_service/push_message" +require "slack_service/merge_message" \ No newline at end of file diff --git a/app/models/project_services/slack_messages/slack_base_message.rb b/app/models/project_services/slack_service/slack_base_message.rb similarity index 89% rename from app/models/project_services/slack_messages/slack_base_message.rb rename to app/models/project_services/slack_service/slack_base_message.rb index c2fc27884bf..aa00d6061a1 100644 --- a/app/models/project_services/slack_messages/slack_base_message.rb +++ b/app/models/project_services/slack_service/slack_base_message.rb @@ -1,7 +1,7 @@ require 'slack-notifier' -module SlackMessages - class SlackBaseMessage +class SlackService + class BaseMessage def initialize(params) raise NotImplementedError end diff --git a/app/models/project_services/slack_messages/slack_issue_message.rb b/app/models/project_services/slack_service/slack_issue_message.rb similarity index 94% rename from app/models/project_services/slack_messages/slack_issue_message.rb rename to app/models/project_services/slack_service/slack_issue_message.rb index 0c3a492aae7..cb2e3f7421f 100644 --- a/app/models/project_services/slack_messages/slack_issue_message.rb +++ b/app/models/project_services/slack_service/slack_issue_message.rb @@ -1,5 +1,5 @@ -module SlackMessages - class SlackIssueMessage < SlackBaseMessage +module SlackService + class IssueMessage < BaseMessage attr_reader :username attr_reader :title attr_reader :project_name diff --git a/app/models/project_services/slack_messages/slack_merge_message.rb b/app/models/project_services/slack_service/slack_merge_message.rb similarity index 94% rename from app/models/project_services/slack_messages/slack_merge_message.rb rename to app/models/project_services/slack_service/slack_merge_message.rb index bc49a963a9b..309983e9f1c 100644 --- a/app/models/project_services/slack_messages/slack_merge_message.rb +++ b/app/models/project_services/slack_service/slack_merge_message.rb @@ -1,5 +1,5 @@ -module SlackMessages - class SlackMergeMessage < SlackBaseMessage +module SlackService + class MergeMessage < BaseMessage attr_reader :username attr_reader :project_name attr_reader :project_url diff --git a/app/models/project_services/slack_messages/slack_push_message.rb b/app/models/project_services/slack_service/slack_push_message.rb similarity index 97% rename from app/models/project_services/slack_messages/slack_push_message.rb rename to app/models/project_services/slack_service/slack_push_message.rb index c7769bbeda1..ab2d48f9fb2 100644 --- a/app/models/project_services/slack_messages/slack_push_message.rb +++ b/app/models/project_services/slack_service/slack_push_message.rb @@ -1,7 +1,7 @@ require 'slack-notifier' -module SlackMessages - class SlackPushMessage < SlackBaseMessage +module SlackService + class PushMessage < BaseMessage attr_reader :after attr_reader :before attr_reader :commits -- GitLab From 19f04cf989a75f425af09a8551957dae42b1ff31 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 14:49:36 +0100 Subject: [PATCH 1155/1609] Execute services for tag push. --- app/services/git_tag_push_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 46d8987f12d..5fd8f642fad 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -8,6 +8,7 @@ class GitTagPushService EventCreateService.new.push(project, user, @push_data) project.repository.expire_cache project.execute_hooks(@push_data.dup, :tag_push_hooks) + project.execute_services(@push_data.dup, :tag_push_hooks) if project.gitlab_ci? project.gitlab_ci_service.async_execute(@push_data) -- GitLab From d86c0cda24a76c9330b5fed59e857ce9e4150b9b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 20 Feb 2015 16:05:40 +0100 Subject: [PATCH 1156/1609] Fix specs. --- app/models/project_services/slack_service.rb | 2 +- .../{slack_base_message.rb => base_message.rb} | 0 .../{slack_issue_message.rb => issue_message.rb} | 2 +- .../{slack_merge_message.rb => merge_message.rb} | 2 +- .../{slack_push_message.rb => push_message.rb} | 4 +--- .../issue_message_spec.rb} | 4 ++-- .../merge_message_spec.rb} | 4 ++-- .../push_message_spec.rb} | 8 ++++---- 8 files changed, 12 insertions(+), 14 deletions(-) rename app/models/project_services/slack_service/{slack_base_message.rb => base_message.rb} (100%) rename app/models/project_services/slack_service/{slack_issue_message.rb => issue_message.rb} (98%) rename app/models/project_services/slack_service/{slack_merge_message.rb => merge_message.rb} (98%) rename app/models/project_services/slack_service/{slack_push_message.rb => push_message.rb} (98%) rename spec/models/project_services/{slack_messages/slack_issue_message_spec.rb => slack_service/issue_message_spec.rb} (92%) rename spec/models/project_services/{slack_messages/slack_merge_message_spec.rb => slack_service/merge_message_spec.rb} (92%) rename spec/models/project_services/{slack_messages/slack_push_message_spec.rb => slack_service/push_message_spec.rb} (87%) diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 8289e474031..279abad8081 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -93,4 +93,4 @@ end require "slack_service/issue_message" require "slack_service/push_message" -require "slack_service/merge_message" \ No newline at end of file +require "slack_service/merge_message" diff --git a/app/models/project_services/slack_service/slack_base_message.rb b/app/models/project_services/slack_service/base_message.rb similarity index 100% rename from app/models/project_services/slack_service/slack_base_message.rb rename to app/models/project_services/slack_service/base_message.rb diff --git a/app/models/project_services/slack_service/slack_issue_message.rb b/app/models/project_services/slack_service/issue_message.rb similarity index 98% rename from app/models/project_services/slack_service/slack_issue_message.rb rename to app/models/project_services/slack_service/issue_message.rb index cb2e3f7421f..e2fed0bb1b9 100644 --- a/app/models/project_services/slack_service/slack_issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -1,4 +1,4 @@ -module SlackService +class SlackService class IssueMessage < BaseMessage attr_reader :username attr_reader :title diff --git a/app/models/project_services/slack_service/slack_merge_message.rb b/app/models/project_services/slack_service/merge_message.rb similarity index 98% rename from app/models/project_services/slack_service/slack_merge_message.rb rename to app/models/project_services/slack_service/merge_message.rb index 309983e9f1c..4dcce1d15a0 100644 --- a/app/models/project_services/slack_service/slack_merge_message.rb +++ b/app/models/project_services/slack_service/merge_message.rb @@ -1,4 +1,4 @@ -module SlackService +class SlackService class MergeMessage < BaseMessage attr_reader :username attr_reader :project_name diff --git a/app/models/project_services/slack_service/slack_push_message.rb b/app/models/project_services/slack_service/push_message.rb similarity index 98% rename from app/models/project_services/slack_service/slack_push_message.rb rename to app/models/project_services/slack_service/push_message.rb index ab2d48f9fb2..2e566bc317b 100644 --- a/app/models/project_services/slack_service/slack_push_message.rb +++ b/app/models/project_services/slack_service/push_message.rb @@ -1,6 +1,4 @@ -require 'slack-notifier' - -module SlackService +class SlackService class PushMessage < BaseMessage attr_reader :after attr_reader :before diff --git a/spec/models/project_services/slack_messages/slack_issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb similarity index 92% rename from spec/models/project_services/slack_messages/slack_issue_message_spec.rb rename to spec/models/project_services/slack_service/issue_message_spec.rb index 49a8eea0a58..a23a7cc068e 100644 --- a/spec/models/project_services/slack_messages/slack_issue_message_spec.rb +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackMessages::SlackIssueMessage do - subject { SlackMessages::SlackIssueMessage.new(args) } +describe SlackService::IssueMessage do + subject { SlackService::IssueMessage.new(args) } let(:args) { { diff --git a/spec/models/project_services/slack_messages/slack_merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb similarity index 92% rename from spec/models/project_services/slack_messages/slack_merge_message_spec.rb rename to spec/models/project_services/slack_service/merge_message_spec.rb index ef76c3312ea..25d03cd8736 100644 --- a/spec/models/project_services/slack_messages/slack_merge_message_spec.rb +++ b/spec/models/project_services/slack_service/merge_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackMessages::SlackMergeMessage do - subject { SlackMessages::SlackMergeMessage.new(args) } +describe SlackService::MergeMessage do + subject { SlackService::MergeMessage.new(args) } let(:args) { { diff --git a/spec/models/project_services/slack_messages/slack_push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb similarity index 87% rename from spec/models/project_services/slack_messages/slack_push_message_spec.rb rename to spec/models/project_services/slack_service/push_message_spec.rb index f11614d6921..ef0e7a6ee30 100644 --- a/spec/models/project_services/slack_messages/slack_push_message_spec.rb +++ b/spec/models/project_services/slack_service/push_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackMessages::SlackPushMessage do - subject { SlackMessages::SlackPushMessage.new(args) } +describe SlackService::PushMessage do + subject { SlackService::PushMessage.new(args) } let(:args) { { @@ -31,8 +31,8 @@ describe SlackMessages::SlackPushMessage do ) expect(subject.attachments).to eq([ { - text: ": message1 - author1\n"\ - ": message2 - author2", + text: ": message1 - author1\n"\ + ": message2 - author2", color: color, } ]) -- GitLab From f13567edc4b9ce68179d12562a711540c8994206 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 14:29:21 +0100 Subject: [PATCH 1157/1609] Only execute GitlabCiService for push events. --- app/models/project_services/gitlab_ci_service.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index a64b24b5ef1..34a20f55792 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -22,8 +22,6 @@ class GitlabCiService < CiService validates :project_url, presence: true, if: :activated? validates :token, presence: true, if: :activated? - delegate :execute, to: :service_hook, prefix: nil - after_save :compose_service_hook, if: :activated? def compose_service_hook @@ -32,6 +30,13 @@ class GitlabCiService < CiService hook.save end + def execute(data) + object_kind = data[:object_kind] + return unless object_kind == "push" + + service_hook.execute(data) + end + def commit_status_path(sha) project_url + "/commits/#{sha}/status.json?token=#{token}" end -- GitLab From ca56d9ff9ff8b28172d5e3dae7e09b77e2e6b835 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 14:29:41 +0100 Subject: [PATCH 1158/1609] Don't execute GitlabCiService twice for pushed tags. --- app/services/git_tag_push_service.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 5fd8f642fad..725ef01ff23 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -10,10 +10,6 @@ class GitTagPushService project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) - if project.gitlab_ci? - project.gitlab_ci_service.async_execute(@push_data) - end - true end -- GitLab From bbcb12f2719d5d8747339ad1bcb3457217870dc2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 14:31:08 +0100 Subject: [PATCH 1159/1609] Execute tag_push services and hooks when tag is created through web UI. --- app/services/create_tag_service.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index a735d3f7f20..850077006ea 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -21,12 +21,12 @@ class CreateTagService < BaseService new_tag = repository.find_tag(tag_name) if new_tag - if project.gitlab_ci? - push_data = create_push_data(project, current_user, new_tag) - project.gitlab_ci_service.async_execute(push_data) - end - EventCreateService.new.push_ref(project, current_user, new_tag, 'add', 'refs/tags') + + push_data = create_push_data(project, current_user, new_tag) + project.execute_hooks(push_data.dup, :tag_push_hooks) + project.execute_services(push_data.dup, :tag_push_hooks) + success(new_tag) else error('Invalid reference name') -- GitLab From 85fa334eb6fd2069287a660e6ffa2295ea3a787f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 14:35:14 +0100 Subject: [PATCH 1160/1609] Execute GitlabCiService for both push and tag_push events. --- app/models/project_services/gitlab_ci_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 34a20f55792..bfc7a1fee32 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -32,7 +32,7 @@ class GitlabCiService < CiService def execute(data) object_kind = data[:object_kind] - return unless object_kind == "push" + return unless %w(push tag_push).include?(object_kind) service_hook.execute(data) end -- GitLab From d57e809cbd56aea8a49c6595663fc4b7250c5a34 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 28 Feb 2015 17:33:18 +0100 Subject: [PATCH 1161/1609] Set supported events per project service. --- app/models/project_services/asana_service.rb | 7 ++- .../project_services/assembla_service.rb | 7 ++- app/models/project_services/bamboo_service.rb | 7 ++- .../project_services/buildbox_service.rb | 7 ++- .../project_services/campfire_service.rb | 7 ++- app/models/project_services/ci_service.rb | 4 ++ .../emails_on_push_service.rb | 7 ++- .../project_services/flowdock_service.rb | 7 ++- .../project_services/gemnasium_service.rb | 7 ++- .../project_services/gitlab_ci_service.rb | 7 ++- .../project_services/hipchat_service.rb | 7 ++- .../project_services/issue_tracker_service.rb | 7 ++- .../pivotaltracker_service.rb | 7 ++- .../project_services/pushover_service.rb | 7 ++- app/models/project_services/slack_service.rb | 5 ++ .../project_services/teamcity_service.rb | 7 ++- app/models/service.rb | 6 ++ app/views/admin/services/_form.html.haml | 37 +++++++++++ app/views/projects/services/_form.html.haml | 63 ++++++++++--------- 19 files changed, 156 insertions(+), 57 deletions(-) diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index a5686c48bc6..8ad1ad6267a 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -65,9 +65,12 @@ automatically inspected. Leave blank to include all branches.' ] end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) Asana.configure do |client| client.api_key = api_key diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 01c647c1705..02aa7c972e7 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -42,9 +42,12 @@ class AssemblaService < Service ] end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}" AssemblaService.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' }) diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 6ff52af040d..6c6d74c615e 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -73,6 +73,10 @@ class BambooService < CiService ] end + def supported_events + %w(push) + end + def build_info(sha) url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}") @@ -123,8 +127,7 @@ class BambooService < CiService end def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) # Bamboo requires a GET and does not take any data. self.class.get("#{bamboo_url}/updateAndBuild.action?buildKey=#{build_key}", diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb index 201bfc560a3..96428c91711 100644 --- a/app/models/project_services/buildbox_service.rb +++ b/app/models/project_services/buildbox_service.rb @@ -36,9 +36,12 @@ class BuildboxService < CiService hook.save end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) service_hook.execute(data) end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 7af6882329c..2f86fbe7a03 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -41,9 +41,12 @@ class CampfireService < Service ] end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) room = gate.find_room_by_name(self.room) return true unless room diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index e58d6d7a233..646783c8733 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -25,6 +25,10 @@ class CiService < Service :ci end + def supported_events + %w(push) + end + # Return complete url to build page # # Ex. diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 1b7ce481c1f..21041e08a20 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -33,9 +33,12 @@ class EmailsOnPushService < Service 'emails_on_push' end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) EmailsOnPushWorker.perform_async(project_id, recipients, data) end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index e4ea84cb617..443dca72a8c 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -41,9 +41,12 @@ class FlowdockService < Service ] end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) Flowdock::Git.post( data[:ref], diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index ada61b78047..41eedc215d5 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -42,9 +42,12 @@ class GemnasiumService < Service ] end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) Gemnasium::GitlabService.execute( ref: data[:ref], diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index bfc7a1fee32..02bf305f8f2 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -30,9 +30,12 @@ class GitlabCiService < CiService hook.save end + def supported_events + %w(push tag_push) + end + def execute(data) - object_kind = data[:object_kind] - return unless %w(push tag_push).include?(object_kind) + return unless supported_events.include?(data[:object_kind]) service_hook.execute(data) end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 965ecdc684b..b85863d2f0d 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -44,9 +44,12 @@ class HipchatService < Service ] end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) gate[room].send('GitLab', create_message(data)) end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 0d9e5c13992..bfc65b5379f 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -69,9 +69,12 @@ class IssueTrackerService < Service end end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) message = "#{self.type} was unable to reach #{self.project_url}. Check the url and try again." result = false diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index dd9e7f35c17..a2fa9788f10 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -41,9 +41,12 @@ class PivotaltrackerService < Service ] end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) url = 'https://www.pivotaltracker.com/services/v5/source_commits' data[:commits].each do |commit| diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index a715e7b2cc5..586d9e94a95 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -80,9 +80,12 @@ class PushoverService < Service ] end + def supported_events + %w(push) + end + def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) ref = data[:ref].gsub('refs/heads/', '') before = data[:before] diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 279abad8081..64d6f4327b8 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -42,7 +42,12 @@ class SlackService < Service ] end + def supported_events + %w(push issue merge_request) + end + def execute(data) + return unless supported_events.include?(data[:object_kind]) return unless webhook.present? object_kind = data[:object_kind] diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 07facfb6d06..686e6225a2e 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -61,6 +61,10 @@ class TeamcityService < CiService 'teamcity' end + def supported_events + %w(push) + end + def fields [ { type: 'text', name: 'teamcity_url', @@ -120,8 +124,7 @@ class TeamcityService < CiService end def execute(data) - object_kind = data[:object_kind] - return unless object_kind == "push" + return unless supported_events.include?(data[:object_kind]) auth = { username: username, diff --git a/app/models/service.rb b/app/models/service.rb index 9d6866f26d0..98bd40ae95e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -80,6 +80,10 @@ class Service < ActiveRecord::Base [] end + def supported_events + %w(push tag_push issue merge_request) + end + def execute # implement inside child end @@ -105,6 +109,8 @@ class Service < ActiveRecord::Base end def async_execute(data) + return unless supported_events.include?(data[:object_kind]) + Sidekiq::Client.enqueue(ProjectServiceWorker, id, data) end diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 5df8849317b..62f4001ca66 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -14,6 +14,43 @@ = preserve do = markdown @service.help + .form-group + = f.label :url, "Trigger", class: 'control-label' + - if @service.supported_events.length > 1 + .col-sm-10 + - if @service.supported_events.include?("push") + %div + = f.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = f.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + - if @service.supported_events.include?("tag_push") + %div + = f.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = f.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + - if @service.supported_events.include?("issue") + %div + = f.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = f.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This url will be triggered when an issue is created + - if @service.supported_events.include?("merge_request") + %div + = f.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = f.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This url will be triggered when a merge request is created + - @service.fields.each do |field| - name = field[:name] - value = @service.send(name) unless field[:type] == 'password' diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 0519c8150e9..55ac85c32b9 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -29,35 +29,40 @@ .form-group = f.label :url, "Trigger", class: 'control-label' - .col-sm-10 - %div - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This url will be triggered by a push to the repository - %div - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This url will be triggered when a new tag is pushed to the repository - %div - = f.check_box :issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :issues_events, class: 'list-label' do - %strong Issues events - %p.light - This url will be triggered when an issue is created - %div - = f.check_box :merge_requests_events, class: 'pull-left' - .prepend-left-20 - = f.label :merge_requests_events, class: 'list-label' do - %strong Merge Request events - %p.light - This url will be triggered when a merge request is created + - if @service.supported_events.length > 1 + .col-sm-10 + - if @service.supported_events.include?("push") + %div + = f.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = f.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + - if @service.supported_events.include?("tag_push") + %div + = f.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = f.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + - if @service.supported_events.include?("issue") + %div + = f.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = f.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This url will be triggered when an issue is created + - if @service.supported_events.include?("merge_request") + %div + = f.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = f.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This url will be triggered when a merge request is created - @service.fields.each do |field| - name = field[:name] -- GitLab From 5c910b94cef084fc1fae398fdf72a220f800e7ad Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sat, 28 Feb 2015 17:41:01 +0100 Subject: [PATCH 1162/1609] Set correct object_kind on tag push data. --- app/services/create_tag_service.rb | 4 +++- app/services/git_tag_push_service.rb | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 850077006ea..8cd65724cb9 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -40,7 +40,9 @@ class CreateTagService < BaseService end def create_push_data(project, user, tag) - Gitlab::PushDataBuilder. + data = Gitlab::PushDataBuilder. build(project, user, Gitlab::Git::BLANK_SHA, tag.target, 'refs/tags/' + tag.name, []) + data[:object_kind] = "tag_push" + data end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 725ef01ff23..cd92f50b02a 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -16,7 +16,8 @@ class GitTagPushService private def create_push_data(oldrev, newrev, ref) - Gitlab::PushDataBuilder. - build(project, user, oldrev, newrev, ref, []) + data = Gitlab::PushDataBuilder.build(project, user, oldrev, newrev, ref, []) + data[:object_kind] = "tag_push" + data end end -- GitLab From 2c7baed3946dcc724e091698978419a18c7d6930 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 3 Mar 2015 11:20:01 +0100 Subject: [PATCH 1163/1609] Fix changelog. --- CHANGELOG | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8011817d0ae..50e18f1006e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,4 @@ Please view this file on the master branch, on stable branches it's out of date. -v 7.9.0 (unreleased) - - Added issue and merge request events to Slack service (Stan Hu) - - Fix broken access control for note attachments (Hannes Rosenögger) v 7.9.0 (unreleased) - Move labels/milestones tabs to sidebar @@ -19,6 +16,7 @@ v 7.9.0 (unreleased) - Allow user confirmation to be skipped for new users via API - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) + - Added issue and merge request events to Slack service (Stan Hu) v 7.8.1 - Fix run of custom post receive hooks -- GitLab From d513ca584aaed7ca2a1de2d2fbd2192422f13d81 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 16:48:22 +0100 Subject: [PATCH 1164/1609] Revert "Merge branch 'go-get-workaround-nginx' of https://github.com/mattes/gitlabhq into mattes-go-get-workaround-nginx" This reverts commit 51349ca3c83c56e072f87253d375316f7164b49a, reversing changes made to b180476bd69bdf99b1727b041116fa8447c0201f. --- app/views/layouts/_head.html.haml | 7 +++++++ lib/support/nginx/gitlab | 10 ---------- lib/support/nginx/gitlab-ssl | 10 ---------- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index d12145651af..bece8061fb9 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,5 +1,12 @@ %head %meta{charset: "utf-8"} + + -# Go repository retrieval support + -# Need to be the fist thing in the head + -# Since Go is using an XML parser to process HTML5 + -# https://github.com/gitlabhq/gitlabhq/pull/5958#issuecomment-45397555 + - if controller_name == 'projects' && action_name == 'show' + %meta{name: "go-import", content: "#{@project.web_url_without_protocol} git #{@project.web_url}.git"} %meta{content: "GitLab Community Edition", name: "description"} %title diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index fd5b2664786..62a4276536c 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -77,16 +77,6 @@ server { proxy_pass http://gitlab; } - ## If ``go get`` detected, return go-import meta tag. - ## This works for public and for private repositories. - ## See also http://golang.org/cmd/go/#hdr-Remote_import_paths - if ($http_user_agent ~* "Go") { - return 200 " - - - "; - } - ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index a9699bac611..2aefc944698 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -123,16 +123,6 @@ server { proxy_pass http://gitlab; } - ## If ``go get`` detected, return go-import meta tag. - ## This works for public and for private repositories. - ## See also http://golang.org/cmd/go/#hdr-Remote_import_paths - if ($http_user_agent ~* "Go") { - return 200 " - - - "; - } - ## If a file, which is not found in the root folder is requested, ## then the proxy passes the request to the upsteam (gitlab unicorn). location @gitlab { -- GitLab From 3702c4ad80614d71fc5ac3ea1af7c3789ec8146d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Feb 2015 17:02:11 +0100 Subject: [PATCH 1165/1609] Render go-import meta tag for private repos. --- app/controllers/projects_controller.rb | 11 +++++++++++ app/views/layouts/_head.html.haml | 7 ------- app/views/projects/go_import.html.haml | 5 +++++ spec/controllers/projects_controller_spec.rb | 16 ++++++++++++++++ 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 app/views/projects/go_import.html.haml diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 5486a97e51d..82b8a1cc13a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,4 +1,5 @@ class ProjectsController < ApplicationController + prepend_before_filter :render_go_import, only: [:show] skip_before_filter :authenticate_user!, only: [:show] before_filter :project, except: [:new, :create] before_filter :repository, except: [:new, :create] @@ -184,4 +185,14 @@ class ProjectsController < ApplicationController end end end + + def render_go_import + return unless params["go-get"] == "1" + + @namespace = params[:namespace_id] + @id = params[:project_id] || params[:id] + @id = @id.gsub(/\.git\Z/, "") + + render "go_import", layout: false + end end diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index bece8061fb9..d12145651af 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -1,12 +1,5 @@ %head %meta{charset: "utf-8"} - - -# Go repository retrieval support - -# Need to be the fist thing in the head - -# Since Go is using an XML parser to process HTML5 - -# https://github.com/gitlabhq/gitlabhq/pull/5958#issuecomment-45397555 - - if controller_name == 'projects' && action_name == 'show' - %meta{name: "go-import", content: "#{@project.web_url_without_protocol} git #{@project.web_url}.git"} %meta{content: "GitLab Community Edition", name: "description"} %title diff --git a/app/views/projects/go_import.html.haml b/app/views/projects/go_import.html.haml new file mode 100644 index 00000000000..87ac75a350f --- /dev/null +++ b/app/views/projects/go_import.html.haml @@ -0,0 +1,5 @@ +!!! 5 +%html + %head + - web_url = [Gitlab.config.gitlab.url, @namespace, @id].join('/') + %meta{name: "go-import", content: "#{web_url.split('://')[1]} git #{web_url}.git"} diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 89bb35de8fc..a1b82a32150 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -7,6 +7,22 @@ describe ProjectsController do let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + describe "GET show" do + + context "when requested by `go get`" do + render_views + + it "renders the go-import meta tag" do + get :show, "go-get" => "1", namespace_id: "bogus_namespace", id: "bogus_project" + + expect(response.body).to include("name='go-import'") + + content = "localhost/bogus_namespace/bogus_project git http://localhost/bogus_namespace/bogus_project.git" + expect(response.body).to include("content='#{content}'") + end + end + end + describe "POST #toggle_star" do it "toggles star if user is signed in" do sign_in(user) -- GitLab From fc6160816119504e1cea0954453cd557231341a1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 3 Mar 2015 13:09:45 +0100 Subject: [PATCH 1166/1609] Fix specs. --- spec/models/project_services/slack_service_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 49c48d0b65c..9024e53f0ff 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -99,8 +99,8 @@ describe SlackService do with(webhook_url, username: username). and_return( double(:slack_service).as_null_object - ) - slack.execute(sample_data) + ) + slack.execute(push_sample_data) end it 'should use the channel as an option when it is configured' do @@ -110,7 +110,7 @@ describe SlackService do and_return( double(:slack_service).as_null_object ) - slack.execute(sample_data) + slack.execute(push_sample_data) end end end -- GitLab From 3102454a566292d6435fcaae8d21c6c2d26c0341 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 3 Mar 2015 13:52:13 +0100 Subject: [PATCH 1167/1609] Don't show Unassigned in user select when searching. --- .../javascripts/project_users_select.js.coffee | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee index 885f0d58a6a..e22c7c11f1c 100644 --- a/app/assets/javascripts/project_users_select.js.coffee +++ b/app/assets/javascripts/project_users_select.js.coffee @@ -11,14 +11,15 @@ class @ProjectUsersSelect Api.projectUsers project_id, query.term, (users) -> data = { results: users } - nullUser = { - name: 'Unassigned', - avatar: null, - username: 'none', - id: -1 - } - - data.results.unshift(nullUser) + if query.term.length == 0 + nullUser = { + name: 'Unassigned', + avatar: null, + username: 'none', + id: -1 + } + + data.results.unshift(nullUser) query.callback(data) -- GitLab From 10212c01fd18aa9961e86bd961475068a7596f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= Date: Tue, 3 Mar 2015 15:31:05 +0100 Subject: [PATCH 1168/1609] Count commits in branches as well in the commit calendar --- app/models/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index bbf35f04bbc..5b52739df2b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -146,7 +146,7 @@ class Repository end def timestamps_by_user_log(user) - args = %W(git log --author=#{user.email} --since=#{(Date.today - 1.year).to_s} --pretty=format:%cd --date=short) + args = %W(git log --author=#{user.email} --since=#{(Date.today - 1.year).to_s} --branches --pretty=format:%cd --date=short) dates = Gitlab::Popen.popen(args, path_to_repo).first.split("\n") if dates.present? -- GitLab From fbc3cb69c327e52a002c7909c257c43c9a2f5ba5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 3 Mar 2015 16:19:37 +0100 Subject: [PATCH 1169/1609] Add dashboard milestones. --- .../dashboard/milestones_controller.rb | 34 ++++++++ .../groups/milestones_controller.rb | 10 +-- app/helpers/milestones_helper.rb | 2 + .../dashboard/milestones/_issue.html.haml | 10 +++ .../dashboard/milestones/_issues.html.haml | 6 ++ .../milestones/_merge_request.html.haml | 10 +++ .../milestones/_merge_requests.html.haml | 6 ++ .../dashboard/milestones/index.html.haml | 39 +++++++++ app/views/dashboard/milestones/show.html.haml | 82 +++++++++++++++++++ app/views/groups/milestones/index.html.haml | 7 +- app/views/layouts/nav/_dashboard.html.haml | 5 ++ config/routes.rb | 6 +- 12 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 app/controllers/dashboard/milestones_controller.rb create mode 100644 app/views/dashboard/milestones/_issue.html.haml create mode 100644 app/views/dashboard/milestones/_issues.html.haml create mode 100644 app/views/dashboard/milestones/_merge_request.html.haml create mode 100644 app/views/dashboard/milestones/_merge_requests.html.haml create mode 100644 app/views/dashboard/milestones/index.html.haml create mode 100644 app/views/dashboard/milestones/show.html.haml diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb new file mode 100644 index 00000000000..386e283f3a0 --- /dev/null +++ b/app/controllers/dashboard/milestones_controller.rb @@ -0,0 +1,34 @@ +class Dashboard::MilestonesController < ApplicationController + before_filter :load_projects + + def index + project_milestones = case params[:state] + when 'all'; state + when 'closed'; state('closed') + else state('active') + end + @dashboard_milestones = Milestones::GroupService.new(project_milestones).execute + @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(30) + end + + def show + project_milestones = Milestone.where(project_id: @projects).order("due_date ASC") + @dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title) + end + + private + + def load_projects + @projects = current_user.authorized_projects.sorted_by_activity.non_archived + end + + def title + params[:title] + end + + def state(state = nil) + conditions = { project_id: @projects } + conditions.reverse_merge!(state: state) if state + Milestone.where(conditions).order("title ASC") + end +end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 860d8e03922..6802e529b54 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -4,10 +4,10 @@ class Groups::MilestonesController < ApplicationController before_filter :authorize_group_milestone!, only: :update def index - project_milestones = case params[:status] - when 'all'; status - when 'closed'; status('closed') - else status('active') + project_milestones = case params[:state] + when 'all'; state + when 'closed'; state('closed') + else state('active') end @group_milestones = Milestones::GroupService.new(project_milestones).execute @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(30) @@ -44,7 +44,7 @@ class Groups::MilestonesController < ApplicationController params[:title] end - def status(state = nil) + def state(state = nil) conditions = { project_id: group.projects } conditions.reverse_merge!(state: state) if state Milestone.where(conditions).order("title ASC") diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 47fa147dccf..3383b1ae5be 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -4,6 +4,8 @@ module MilestonesHelper namespace_project_milestones_path(@project.namespace, @project, opts) elsif @group group_milestones_path(@group, opts) + else + dashboard_milestones_path(opts) end end end diff --git a/app/views/dashboard/milestones/_issue.html.haml b/app/views/dashboard/milestones/_issue.html.haml new file mode 100644 index 00000000000..f689b9698eb --- /dev/null +++ b/app/views/dashboard/milestones/_issue.html.haml @@ -0,0 +1,10 @@ +%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid } + %span.milestone-row + - project = issue.project + %strong #{project.name_with_namespace} · + = link_to [project.namespace.becomes(Namespace), project, issue] do + %span.cgray ##{issue.iid} + = link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title + .pull-right.assignee-icon + - if issue.assignee + = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_issues.html.haml b/app/views/dashboard/milestones/_issues.html.haml new file mode 100644 index 00000000000..9f350b772bd --- /dev/null +++ b/app/views/dashboard/milestones/_issues.html.haml @@ -0,0 +1,6 @@ +.panel.panel-default + .panel-heading= title + %ul{ class: "well-list issues-sortable-list" } + - if issues + - issues.each do |issue| + = render 'issue', issue: issue diff --git a/app/views/dashboard/milestones/_merge_request.html.haml b/app/views/dashboard/milestones/_merge_request.html.haml new file mode 100644 index 00000000000..8f5c4cce529 --- /dev/null +++ b/app/views/dashboard/milestones/_merge_request.html.haml @@ -0,0 +1,10 @@ +%li{ id: dom_id(merge_request, 'sortable'), class: 'mr-row', 'data-iid' => merge_request.iid } + %span.milestone-row + - project = merge_request.project + %strong #{project.name_with_namespace} · + = link_to [project.namespace.becomes(Namespace), project, merge_request] do + %span.cgray ##{merge_request.iid} + = link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title + .pull-right.assignee-icon + - if merge_request.assignee + = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" diff --git a/app/views/dashboard/milestones/_merge_requests.html.haml b/app/views/dashboard/milestones/_merge_requests.html.haml new file mode 100644 index 00000000000..50057e2c636 --- /dev/null +++ b/app/views/dashboard/milestones/_merge_requests.html.haml @@ -0,0 +1,6 @@ +.panel.panel-default + .panel-heading= title + %ul{ class: "well-list merge_requests-sortable-list" } + - if merge_requests + - merge_requests.each do |merge_request| + = render 'merge_request', merge_request: merge_request diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml new file mode 100644 index 00000000000..65fc5898518 --- /dev/null +++ b/app/views/dashboard/milestones/index.html.haml @@ -0,0 +1,39 @@ +%h3.page-title + Milestones + %span.pull-right #{@dashboard_milestones.count} milestones + +%p.light + List all milestones from all projects you have access to. + +%hr + += render 'shared/milestones_filter' +.milestones + .panel.panel-default + %ul.well-list + - if @dashboard_milestones.blank? + %li + .nothing-here-block No milestones to show + - else + - @dashboard_milestones.each do |milestone| + %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } + %h4 + = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) + %div + %div + = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = pluralize milestone.issue_count, 'Issue' +   + = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = pluralize milestone.merge_requests_count, 'Merge Request' +   + %span.light #{milestone.percent_complete}% complete + .progress.progress-info + .progress-bar{style: "width: #{milestone.percent_complete}%;"} + %div + %br + - milestone.milestones.each do |milestone| + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do + %span.label.label-default + = milestone.project.name_with_namespace + = paginate @dashboard_milestones, theme: "gitlab" diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml new file mode 100644 index 00000000000..a45a52001be --- /dev/null +++ b/app/views/dashboard/milestones/show.html.haml @@ -0,0 +1,82 @@ +%h4.page-title + .issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" } + - if @dashboard_milestone.closed? + Closed + - else + Open + Milestone #{@dashboard_milestone.title} + +%hr +- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active? + .alert.alert-success + %span All issues for this milestone are closed. You may close the milestone now. + +.description +%table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - @dashboard_milestone.milestones.each do |milestone| + %tr + %td + = link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) + %td + = milestone.issues.opened.count + %td + - if milestone.closed? + Closed + - else + Open + %td + = milestone.expires_at + +.context + %p.lead + Progress: + #{@dashboard_milestone.closed_items_count} closed + – + #{@dashboard_milestone.open_items_count} open + .progress.progress-info + .progress-bar{style: "width: #{@dashboard_milestone.percent_complete}%;"} + +%ul.nav.nav-tabs + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab' do + Issues + %span.badge= @dashboard_milestone.issue_count + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do + Merge Requests + %span.badge= @dashboard_milestone.merge_requests_count + %li + = link_to '#tab-participants', 'data-toggle' => 'tab' do + Participants + %span.badge= @dashboard_milestone.participants.count + +.tab-content + .tab-pane.active#tab-issues + .row + .col-md-6 + = render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues + .col-md-6 + = render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues + + .tab-pane#tab-merge-requests + .row + .col-md-6 + = render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests + .col-md-6 + = render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests + + .tab-pane#tab-participants + %ul.bordered-list + - @dashboard_milestone.participants.each do |user| + %li + = link_to user, title: user.name, class: "darken" do + = image_tag avatar_icon(user.email, 32), class: "avatar s32" + %strong= truncate(user.name, lenght: 40) + %br + %small.cgray= user.username diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 7f0b2832cac..fcbcb309aa7 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -40,7 +40,8 @@ .progress-bar{style: "width: #{milestone.percent_complete}%;"} %div %br - - milestone.projects.each do |project| - %span.label.label-default - = project.name + - milestone.milestones.each do |milestone| + = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do + %span.label.label-default + = milestone.project.name = paginate @group_milestones, theme: "gitlab" diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 48c7c999427..304744ba251 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -9,6 +9,11 @@ %i.fa.fa-cube %span Projects + = nav_link(controller: :milestones) do + = link_to dashboard_milestones_path, title: 'Milestones' do + %i.fa.fa-clock-o + %span + Milestones = nav_link(path: 'dashboard#issues') do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do %i.fa.fa-exclamation-circle diff --git a/config/routes.rb b/config/routes.rb index 63299176932..5348c86ea9d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -220,6 +220,10 @@ Gitlab::Application.routes.draw do get :issues get :merge_requests end + + scope module: :dashboard do + resources :milestones, only: [:index, :show] + end end # @@ -236,7 +240,7 @@ Gitlab::Application.routes.draw do scope module: :groups do resources :group_members, only: [:create, :update, :destroy] resource :avatar, only: [:destroy] - resources :milestones + resources :milestones, only: [:index, :show, :update] end end -- GitLab From 4ecfcb4a1e05189372a72547103f1c2e9a23ab3b Mon Sep 17 00:00:00 2001 From: Ewan Edwards Date: Tue, 3 Mar 2015 07:38:38 -0800 Subject: [PATCH 1170/1609] Moved the Gmail integration line into the list of available integrations. --- doc/integration/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/integration/README.md b/doc/integration/README.md index 559a94533de..8f14c9d8b92 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -9,11 +9,10 @@ See the documentation below for details on how to configure these services. - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. - [Slack](slack.md) Integrate with the Slack chat service - [OAuth2 provider](oauth_provider.md) OAuth2 application creation +- [Gmail](gitlab_buttons_in_gmail.md) Adds GitLab actions to messages Jenkins support is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jenkins.html). -GitLab can also integrate with [Gmail](gitlab_buttons_in_gmail.md). - ## Project services Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a Project Service. -- GitLab From 3ff71897384b905218960c8562b681645ee21621 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Tue, 3 Mar 2015 10:01:49 -0800 Subject: [PATCH 1171/1609] Clearly mark it as installation from source. --- doc/install/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 28597fd39d2..2b204c72476 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -1,4 +1,4 @@ -# Installation +# Installation from source ## Consider the Omnibus package installation -- GitLab From 2088cee935e47b569f0c79b10dcb2c506b666af3 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 3 Mar 2015 08:01:27 -0800 Subject: [PATCH 1172/1609] Fix URL builder to use GitlabRoutingHelper --- lib/gitlab/url_builder.rb | 18 +++++++----------- spec/lib/gitlab/url_builder_spec.rb | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 7ab3f090a89..ab7c8ad89f3 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -1,6 +1,7 @@ module Gitlab class UrlBuilder include Rails.application.routes.url_helpers + include GitlabRoutingHelper def initialize(type) @type = type @@ -9,27 +10,22 @@ module Gitlab def build(id) case @type when :issue - issue_url(id) + build_issue_url(id) when :merge_request - merge_request_url(id) + build_merge_request_url(id) end end private - def issue_url(id) + def build_issue_url(id) issue = Issue.find(id) - namespace_project_issue_url(namespace_id: issue.project.namespace, - id: issue.iid, - project_id: issue.project, - host: Gitlab.config.gitlab['url']) + issue_url(issue, host: Gitlab.config.gitlab['url']) end - def merge_request_url(id) + def build_merge_request_url(id) merge_request = MergeRequest.find(id) - project_merge_request_url(id: merge_request.id, - project_id: merge_request.project, - host: Gitlab.config.gitlab['url']) + merge_request_url(merge_request, host: Gitlab.config.gitlab['url']) end end end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 518239fab6d..94b2fd5508e 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::UrlBuilder do it 'returns the merge request url' do merge_request = create(:merge_request) url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) - expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.to_param}/merge_requests/#{merge_request.id}" + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" end end end -- GitLab From 443cf9723192b821b95960f91c3ce0350d77edab Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Tue, 3 Mar 2015 14:47:10 -0800 Subject: [PATCH 1173/1609] Changed to 'View Build Page' --- app/views/projects/merge_requests/show/_mr_ci.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml index ee7fd0ef157..982d04d3e3d 100644 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml @@ -3,21 +3,21 @@ %i.fa.fa-check %span CI build passed for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget.ci-failed{style: "display:none"} %i.fa.fa-times %span CI build failed for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - [:running, :pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} %i.fa.fa-clock-o %span CI build #{status} for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget %i.fa.fa-spinner -- GitLab From dc7c90f132321497d9a914734f902762d2022a3a Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Tue, 3 Mar 2015 14:50:02 -0800 Subject: [PATCH 1174/1609] Changed casing --- app/views/projects/merge_requests/show/_mr_ci.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml index 982d04d3e3d..85a7103f3bc 100644 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml @@ -3,21 +3,21 @@ %i.fa.fa-check %span CI build passed for #{@merge_request.last_commit_short_sha}. - = link_to "View Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget.ci-failed{style: "display:none"} %i.fa.fa-times %span CI build failed for #{@merge_request.last_commit_short_sha}. - = link_to "View Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - [:running, :pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} %i.fa.fa-clock-o %span CI build #{status} for #{@merge_request.last_commit_short_sha}. - = link_to "View Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget %i.fa.fa-spinner -- GitLab From b5ec0d6d450b32fa00d95d864fa797f64e9ca17e Mon Sep 17 00:00:00 2001 From: Sabba Petri Date: Tue, 3 Mar 2015 14:05:31 -0800 Subject: [PATCH 1175/1609] Spelling change Commit Statistics --- app/views/projects/graphs/commits.html.haml | 2 +- features/steps/project/graph.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index a189a487135..4a5d09b9503 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -1,7 +1,7 @@ = render 'head' %p.lead - Commits statistic for + Commit statistics for %strong #{@repository.root_ref} #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index bc07c3d413c..a2807c340f6 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -17,7 +17,7 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps end step 'page should have commits graphs' do - page.should have_content "Commits statistic for master" + page.should have_content "Commit statistics for master" page.should have_content "Commits per day of month" end end -- GitLab From f24e46b8f01624bffc37f2f800cb03bcafc43591 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 3 Mar 2015 21:59:43 -0800 Subject: [PATCH 1176/1609] Remove pull-review badge since we dont use it --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index b4f28a41be3..0563ceca409 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,6 @@ The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab. - [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) -- [![PullReview stats](https://www.pullreview.com/gitlab/gitlab-org/gitlab-ce/badges/master.svg?)](https://www.pullreview.com/gitlab.gitlab.com/gitlab-org/gitlab-ce/reviews/master) - ## Website On [about.gitlab.com](https://about.gitlab.com/) you can find more information about: -- GitLab From 53aec08d0295c8c33286f634c1f4ece81271c04d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 4 Mar 2015 10:13:16 +0100 Subject: [PATCH 1177/1609] Add changelog entry. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 6a28772097e..d088174ad71 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 7.9.0 (unreleased) - Allow user confirmation to be skipped for new users via API - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) + - Add grouped milestones from all projects to dashboard. v 7.8.1 - Fix run of custom post receive hooks -- GitLab From af522ede14cad4605bc7f0137ddf6950974eccce Mon Sep 17 00:00:00 2001 From: fabien Date: Wed, 4 Mar 2015 16:22:26 +0100 Subject: [PATCH 1178/1609] Update gemnasium-gitlab-service gem --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 102d1a28875..b8b8de08f13 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -190,7 +190,7 @@ GEM dotenv (>= 0.7) thor (>= 0.13.6) formatador (0.2.4) - gemnasium-gitlab-service (0.2.4) + gemnasium-gitlab-service (0.2.5) rugged (~> 0.21) gherkin-ruby (0.3.1) racc -- GitLab From f04847a3c70c96c008498179f58db6aceb0c5d09 Mon Sep 17 00:00:00 2001 From: Ewan Edwards Date: Mon, 16 Feb 2015 11:16:23 -0800 Subject: [PATCH 1179/1609] The "GitLab buttons in Gmail" document was not linked from anywhere else. It is now linked. --- doc/integration/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/integration/README.md b/doc/integration/README.md index e5f33d8deed..d66467c4b1f 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -11,6 +11,8 @@ See the documentation below for details on how to configure these services. GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html). +GitLab can also integrate with [Gmail](gitlab_buttons_in_gmail.md). + ## Project services Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a Project Service. -- GitLab From fe6fb32fe6cc9836f92e169f3e1f5bb3dae8ffc8 Mon Sep 17 00:00:00 2001 From: Ewan Edwards Date: Tue, 3 Mar 2015 07:38:38 -0800 Subject: [PATCH 1180/1609] Moved the Gmail integration line into the list of available integrations. --- doc/integration/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/integration/README.md b/doc/integration/README.md index d66467c4b1f..286bd34a0bd 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -8,11 +8,11 @@ See the documentation below for details on how to configure these services. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. - [Slack](slack.md) Integrate with the Slack chat service +- [OAuth2 provider](oauth_provider.md) OAuth2 application creation +- [Gmail](gitlab_buttons_in_gmail.md) Adds GitLab actions to messages GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html). -GitLab can also integrate with [Gmail](gitlab_buttons_in_gmail.md). - ## Project services Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a Project Service. -- GitLab From 890f14786a49cb715d8856c1a6917003649796c5 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 3 Mar 2015 15:35:07 -0800 Subject: [PATCH 1181/1609] Add link to smtp documentation. --- config/initializers/smtp_settings.rb.sample | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample index e00923e7e0c..f0fe2fdfa43 100644 --- a/config/initializers/smtp_settings.rb.sample +++ b/config/initializers/smtp_settings.rb.sample @@ -3,6 +3,9 @@ # 2. Edit settings inside this file # 3. Restart GitLab instance # +# For full list of options and their values see http://api.rubyonrails.org/classes/ActionMailer/Base.html +# + if Rails.env.production? Gitlab::Application.config.action_mailer.delivery_method = :smtp @@ -14,6 +17,6 @@ if Rails.env.production? domain: "gitlab.company.com", authentication: :login, enable_starttls_auto: true, - openssl_verify_mode: 'none' + openssl_verify_mode: 'peer' # See ActionMailer documentation for other possible options } end -- GitLab From 3d9a766d9f465ef97b259be860891ce35bd04d0b Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 4 Mar 2015 20:24:34 +0200 Subject: [PATCH 1182/1609] Web Hook sends email of pusher --- CHANGELOG | 1 + doc/web_hooks/web_hooks.md | 1 + lib/gitlab/push_data_builder.rb | 2 ++ 3 files changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 3b3baf56706..3d322aadb02 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ v 7.9.0 (unreleased) - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options + - Web hook sends pusher email as well as commiter v 7.8.1 - Fix run of custom post receive hooks diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 29ef5b59bac..3cccd84b063 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -21,6 +21,7 @@ Triggered when you push to the repository except when pushing tags. "ref": "refs/heads/master", "user_id": 4, "user_name": "John Smith", + "user_email": "john@example.com", "project_id": 15, "repository": { "name": "Diaspora", diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 9aa5c8967a7..6a72efa7221 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -9,6 +9,7 @@ module Gitlab # ref: String, # user_id: String, # user_name: String, + # user_email: String # project_id: String, # repository: { # name: String, @@ -35,6 +36,7 @@ module Gitlab checkout_sha: checkout_sha(project.repository, newrev, ref), user_id: user.id, user_name: user.name, + user_email: user.email, project_id: project.id, repository: { name: project.name, -- GitLab From 61acaff5a1e4db254c92854e639aa0cb8c2d3cff Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 4 Mar 2015 10:49:48 -0800 Subject: [PATCH 1183/1609] Bump gitlab_git to rc15 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 01c02b5c8db..d711efe6215 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '7.0.0.rc14' +gem "gitlab_git", '7.0.0.rc15' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' diff --git a/Gemfile.lock b/Gemfile.lock index 102d1a28875..1413e967416 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -212,7 +212,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.0.1.1) emoji (~> 1.0.1) - gitlab_git (7.0.0.rc14) + gitlab_git (7.0.0.rc15) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -701,7 +701,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.0.1.1) - gitlab_git (= 7.0.0.rc14) + gitlab_git (= 7.0.0.rc15) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.0) gollum-lib (~> 4.0.0) -- GitLab From c842c452838423f82c3e9aecdfecc236b2675002 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Wed, 4 Mar 2015 11:24:14 -0800 Subject: [PATCH 1184/1609] Advise on how to help others. --- CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3d4d8ea9b6..73a8f9eb49f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,13 @@ Please treat our volunteers with courtesy and respect, it will go a long way tow Issues and merge requests should be in English and contain appropriate language for audiences of all ages. +## Helping others + +Please help other GitLab users when you can. +The channnels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/). +Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the irc channel. +You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day. + ## Issue tracker To get support for your particular problem please use the channels as detailed in the [getting help section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#getting-help). Professional [support subscriptions](http://about.gitlab.com/subscription/) and [consulting services](http://about.gitlab.com/consultancy/) are available from [GitLab.com](http://about.gitlab.com/). -- GitLab From 52211ea72a345c32f7bd4389c83d70dfa796f99c Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Wed, 4 Mar 2015 12:00:53 -0800 Subject: [PATCH 1185/1609] Added Service Templates to CHANGELOG and fixed typo. --- CHANGELOG | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 37aee53bc0a..924959e5e66 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -48,7 +48,8 @@ v 7.8.0 - Add notes for label changes in issue and merge requests - Show tags in commit view (Hannes Rosenögger) - Only count a user's vote once on a merge request or issue (Michael Clarke) - - Increate font size when browse source files and diffs + - Increase font size when browse source files and diffs + - Service Templates now let you set default values for all services - Create new file in empty repository using GitLab UI - Ability to clone project using oauth2 token - Upgrade Sidekiq gem to version 3.3.0 -- GitLab From 6fd06006856bd7f06c5bf15db2ef064cba043a58 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 4 Mar 2015 19:56:41 +0200 Subject: [PATCH 1186/1609] Bugfix #1096 --- app/assets/javascripts/blob/edit_blob.js.coffee | 2 +- app/assets/javascripts/blob/new_blob.js.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 6914ca759f6..2e91a06daa8 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -15,7 +15,7 @@ class @EditBlob $(".js-commit-button").click -> $("#file-content").val editor.getValue() $(".file-editor form").submit() - return + return false editModePanes = $(".js-edit-mode-pane") editModeLinks = $(".js-edit-mode a") diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee index a6e27116b40..ab8f98715e8 100644 --- a/app/assets/javascripts/blob/new_blob.js.coffee +++ b/app/assets/javascripts/blob/new_blob.js.coffee @@ -15,7 +15,7 @@ class @NewBlob $(".js-commit-button").click -> $("#file-content").val editor.getValue() $(".file-editor form").submit() - return + return false editor: -> return @editor -- GitLab From 46f94173248ed143a78fb9c3e2951501cd553d27 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 4 Mar 2015 09:51:30 -0800 Subject: [PATCH 1187/1609] Update the Changelog. --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 062aa7718c3..a610f790589 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,12 @@ v 7.9.0 (unreleased) - Slack username and channel options - Add grouped milestones from all projects to dashboard. - Web hook sends pusher email as well as commiter +v 7.8.2 + - Fix service migration issue when upgrading from versions prior to 7.3 + - Fix setting of the default use project limit via admin UI + - Fix showing of already imported projects for GitLab and Gitorious importers + - Fix response of push to repository to return "Not found" if user doesn't have access + - Fix check if user is allowed to view the file attachment v 7.8.1 - Fix run of custom post receive hooks -- GitLab From 02f17ce1b3233e5a35d68493b9f3cc03fb8d7c9f Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 4 Mar 2015 10:16:13 -0800 Subject: [PATCH 1188/1609] Update changelog. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index a610f790589..95d176677f3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 7.8.2 - Fix showing of already imported projects for GitLab and Gitorious importers - Fix response of push to repository to return "Not found" if user doesn't have access - Fix check if user is allowed to view the file attachment + - Fix import check for case sensetive namespaces v 7.8.1 - Fix run of custom post receive hooks -- GitLab From 00778c073130789e9f65e02e6fa6e8b987506c9e Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 4 Mar 2015 13:45:27 -0800 Subject: [PATCH 1189/1609] Move items to the correct version in the changelog. --- CHANGELOG | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 95d176677f3..5270a81dfe0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,7 +20,11 @@ v 7.9.0 (unreleased) - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options - Add grouped milestones from all projects to dashboard. - - Web hook sends pusher email as well as commiter + - Web hook sends pusher email as well as commiter + - Add Bitbucket omniauth provider. + - Add Bitbucket importer. + - Support referencing issues to a project whose name starts with a digit + v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 - Fix setting of the default use project limit via admin UI @@ -35,9 +39,6 @@ v 7.8.1 - Fix the warning for LDAP users about need to set password - Fix avatars which were not shown for non logged in users - Fix urls for the issues when relative url was enabled - - Add Bitbucket omniauth provider. - - Add Bitbucket importer. - - Support referencing issues to a project whose name starts with a digit v 7.8.0 - Fix access control and protection against XSS for note attachments and other uploads. -- GitLab From 66c61f023b5c0f492cda561d31848da159c1bd00 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 4 Mar 2015 14:14:00 -0800 Subject: [PATCH 1190/1609] Re-annotate models --- app/models/application_setting.rb | 22 ++--- app/models/identity.rb | 2 + app/models/project_services/asana_service.rb | 18 ++-- .../project_services/assembla_service.rb | 18 ++-- app/models/project_services/bamboo_service.rb | 18 ++-- .../project_services/buildbox_service.rb | 19 ++-- .../project_services/campfire_service.rb | 18 ++-- app/models/project_services/ci_service.rb | 26 +++--- .../custom_issue_tracker_service.rb | 22 +++-- .../emails_on_push_service.rb | 26 +++--- .../project_services/flowdock_service.rb | 26 +++--- .../project_services/gemnasium_service.rb | 26 +++--- .../project_services/gitlab_ci_service.rb | 26 +++--- .../gitlab_issue_tracker_service.rb | 18 ++-- .../project_services/hipchat_service.rb | 26 +++--- app/models/project_services/irker_service.rb | 23 +++-- .../project_services/issue_tracker_service.rb | 18 ++-- app/models/project_services/jira_service.rb | 18 ++-- .../pivotaltracker_service.rb | 18 ++-- .../project_services/pushover_service.rb | 18 ++-- .../project_services/redmine_service.rb | 18 ++-- app/models/project_services/slack_service.rb | 18 ++-- .../project_services/teamcity_service.rb | 18 ++-- app/models/service.rb | 26 +++--- app/models/user.rb | 92 ++++++++++--------- spec/models/application_setting_spec.rb | 21 +++-- spec/models/asana_service_spec.rb | 21 +++-- .../project_services/assembla_service_spec.rb | 11 ++- .../project_services/buildbox_service_spec.rb | 11 ++- .../project_services/flowdock_service_spec.rb | 11 ++- .../gemnasium_service_spec.rb | 11 ++- .../gitlab_ci_service_spec.rb | 11 ++- .../gitlab_issue_tracker_service_spec.rb | 23 +++-- .../project_services/irker_service_spec.rb | 21 +++-- .../project_services/jira_service_spec.rb | 21 +++-- .../project_services/pushover_service_spec.rb | 11 ++- .../project_services/slack_service_spec.rb | 11 ++- spec/models/service_spec.rb | 26 +++--- spec/models/user_spec.rb | 88 +++++++++--------- 39 files changed, 462 insertions(+), 414 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index f1d918e5457..588668b3d1e 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -2,17 +2,17 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# default_branch_protection :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# twitter_sharing_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) # class ApplicationSetting < ActiveRecord::Base diff --git a/app/models/identity.rb b/app/models/identity.rb index b2c3792d1ce..440fcd0d052 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -6,6 +6,8 @@ # extern_uid :string(255) # provider :string(255) # user_id :integer +# created_at :datetime +# updated_at :datetime # class Identity < ActiveRecord::Base diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 8ad1ad6267a..8dce33e670b 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 02aa7c972e7..6dc2500e770 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 6c6d74c615e..50b7cb795d7 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb index 96428c91711..1270484ff6d 100644 --- a/app/models/project_services/buildbox_service.rb +++ b/app/models/project_services/buildbox_service.rb @@ -2,20 +2,21 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # + require "addressable/uri" class BuildboxService < CiService diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 2f86fbe7a03..21e1ca603bb 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 646783c8733..c6f6b4952c9 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -2,19 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # # Base class for CI services diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index b29d1c86881..8d25f627870 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -2,15 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class CustomIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 21041e08a20..d894d2913d7 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -2,19 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class EmailsOnPushService < Service diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 443dca72a8c..99e361dd6ed 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -2,19 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require "flowdock-git-hook" diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 41eedc215d5..4e75bdfc953 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -2,19 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require "gemnasium/gitlab_service" diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 02bf305f8f2..d81623625c9 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -2,19 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class GitlabCiService < CiService diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 00f8d430fd5..90be1e42b26 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index b85863d2f0d..4fb80a98d24 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -2,19 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # class HipchatService < Service diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index a0203a5bb10..deb210c61e4 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -2,15 +2,20 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# require 'uri' diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index bfc65b5379f..7fe1326890b 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 20611eeb60c..4a76f23c693 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index a2fa9788f10..13cbb9bdbcf 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 586d9e94a95..a67abb34831 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index f96eae2daa1..7d7d7d7660b 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 64d6f4327b8..c529a784017 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 686e6225a2e..cd9388de873 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -2,15 +2,15 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) # push_events :boolean default(TRUE) # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) diff --git a/app/models/service.rb b/app/models/service.rb index 98bd40ae95e..8f6a9d57d3b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -2,19 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # # To add new service you should build a class inherited from Service diff --git a/app/models/user.rb b/app/models/user.rb index 55768a351e3..51dd6332fdb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,51 +2,53 @@ # # Table name: users # -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# bio :string(255) -# failed_attempts :integer default(0) -# locked_at :datetime -# username :string(255) -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# hide_no_password :boolean default(FALSE) -# website_url :string(255) default(""), not null -# last_credential_check_at :datetime -# github_access_token :string(255) -# notification_email :string(255) -# password_automatically_set :boolean default(FALSE) -# bitbucket_access_token :string(255) +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# name :string(255) +# admin :boolean default(FALSE), not null +# projects_limit :integer default(10) +# skype :string(255) default(""), not null +# linkedin :string(255) default(""), not null +# twitter :string(255) default(""), not null +# authentication_token :string(255) +# theme_id :integer default(1), not null +# bio :string(255) +# failed_attempts :integer default(0) +# locked_at :datetime +# username :string(255) +# can_create_group :boolean default(TRUE), not null +# can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer +# last_credential_check_at :datetime +# avatar :string(255) +# confirmation_token :string(255) +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string(255) +# hide_no_ssh_key :boolean default(FALSE) +# website_url :string(255) default(""), not null +# github_access_token :string(255) +# gitlab_access_token :string(255) +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# bitbucket_access_token :string(255) +# bitbucket_access_token_secret :string(255) # require 'carrierwave/orm/activerecord' diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index cb43fdb7fc7..d1027f64d13 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -2,16 +2,17 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# default_branch_protection :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/asana_service_spec.rb b/spec/models/asana_service_spec.rb index 83e39f87f33..13c8d54a2af 100644 --- a/spec/models/asana_service_spec.rb +++ b/spec/models/asana_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb index cd34e006ebe..91730da1eec 100644 --- a/spec/models/project_services/assembla_service_spec.rb +++ b/spec/models/project_services/assembla_service_spec.rb @@ -5,15 +5,16 @@ # id :integer not null, primary key # type :string(255) # title :string(255) -# project_id :integer not null +# project_id :integer # created_at :datetime # updated_at :datetime # active :boolean default(FALSE), not null # properties :text -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildbox_service_spec.rb index c246e1c9d41..39d7df54cf0 100644 --- a/spec/models/project_services/buildbox_service_spec.rb +++ b/spec/models/project_services/buildbox_service_spec.rb @@ -5,15 +5,16 @@ # id :integer not null, primary key # type :string(255) # title :string(255) -# project_id :integer not null +# project_id :integer # created_at :datetime # updated_at :datetime # active :boolean default(FALSE), not null # properties :text -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index 2ec167a7330..73f68301a34 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -5,15 +5,16 @@ # id :integer not null, primary key # type :string(255) # title :string(255) -# project_id :integer not null +# project_id :integer # created_at :datetime # updated_at :datetime # active :boolean default(FALSE), not null # properties :text -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index 5f665fadfff..d44064bbe6a 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -5,15 +5,16 @@ # id :integer not null, primary key # type :string(255) # title :string(255) -# project_id :integer not null +# project_id :integer # created_at :datetime # updated_at :datetime # active :boolean default(FALSE), not null # properties :text -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index fcb33b11732..8bfb19e524b 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -5,15 +5,16 @@ # id :integer not null, primary key # type :string(255) # title :string(255) -# project_id :integer not null +# project_id :integer # created_at :datetime # updated_at :datetime # active :boolean default(FALSE), not null # properties :text -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb index c474f4a2d95..959044dc727 100644 --- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -2,16 +2,21 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # + require 'spec_helper' describe GitlabIssueTrackerService do diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb index bbd5245ad34..d55399bc360 100644 --- a/spec/models/project_services/irker_service_spec.rb +++ b/spec/models/project_services/irker_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 6ef4d036c3f..355911e6377 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -2,14 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer not null -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb index bb2e72c3ac1..5a18fd09bfc 100644 --- a/spec/models/project_services/pushover_service_spec.rb +++ b/spec/models/project_services/pushover_service_spec.rb @@ -5,15 +5,16 @@ # id :integer not null, primary key # type :string(255) # title :string(255) -# project_id :integer not null +# project_id :integer # created_at :datetime # updated_at :datetime # active :boolean default(FALSE), not null # properties :text -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 9024e53f0ff..4e8a96ec730 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -5,15 +5,16 @@ # id :integer not null, primary key # type :string(255) # title :string(255) -# project_id :integer not null +# project_id :integer # created_at :datetime # updated_at :datetime # active :boolean default(FALSE), not null # properties :text -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index cc047a20dd2..735652aea78 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -2,19 +2,19 @@ # # Table name: services # -# id :integer not null, primary key -# type :string(255) -# title :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null -# properties :text -# template :boolean default(FALSE) -# push_events :boolean -# issues_events :boolean -# merge_requests_events :boolean -# tag_push_events :boolean +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) # require 'spec_helper' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 29d0c24e87e..10e90cae143 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,47 +2,53 @@ # # Table name: users # -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# bio :string(255) -# failed_attempts :integer default(0) -# locked_at :datetime -# username :string(255) -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string(255) default(""), not null -# last_credential_check_at :datetime -# github_access_token :string(255) +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# name :string(255) +# admin :boolean default(FALSE), not null +# projects_limit :integer default(10) +# skype :string(255) default(""), not null +# linkedin :string(255) default(""), not null +# twitter :string(255) default(""), not null +# authentication_token :string(255) +# theme_id :integer default(1), not null +# bio :string(255) +# failed_attempts :integer default(0) +# locked_at :datetime +# username :string(255) +# can_create_group :boolean default(TRUE), not null +# can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer +# last_credential_check_at :datetime +# avatar :string(255) +# confirmation_token :string(255) +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string(255) +# hide_no_ssh_key :boolean default(FALSE) +# website_url :string(255) default(""), not null +# github_access_token :string(255) +# gitlab_access_token :string(255) +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# bitbucket_access_token :string(255) +# bitbucket_access_token_secret :string(255) # require 'spec_helper' -- GitLab From 516bcabbf42d60db2ac989dce4c7187b2a1e5de9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 3 Mar 2015 13:07:14 +0100 Subject: [PATCH 1191/1609] Increase timeout for Git-over-HTTP requests. --- CHANGELOG | 1 + Gemfile | 3 +++ Gemfile.lock | 8 ++++++++ config/initializers/timeout.rb | 8 ++++++++ config/unicorn.rb.example | 20 ++++---------------- lib/gitlab/middleware/timeout.rb | 13 +++++++++++++ public/503.html | 13 +++++++++++++ 7 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 config/initializers/timeout.rb create mode 100644 lib/gitlab/middleware/timeout.rb create mode 100644 public/503.html diff --git a/CHANGELOG b/CHANGELOG index 6a28772097e..7985a811f11 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 7.9.0 (unreleased) - Allow user confirmation to be skipped for new users via API - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) + - Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time. v 7.8.1 - Fix run of custom post receive hooks diff --git a/Gemfile b/Gemfile index 01c02b5c8db..f2517c6faa4 100644 --- a/Gemfile +++ b/Gemfile @@ -177,6 +177,9 @@ gem 'ace-rails-ap' # Keyboard shortcuts gem 'mousetrap-rails' +# Shutting down requests that take too long +gem "slowpoke" + gem "sass-rails", '~> 4.0.2' gem "coffee-rails" gem "uglifier" diff --git a/Gemfile.lock b/Gemfile.lock index 102d1a28875..7fde0011d38 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -149,6 +149,7 @@ GEM enumerize (0.7.0) activesupport (>= 3.2) equalizer (0.0.8) + errbase (0.0.2) erubis (2.7.0) escape_utils (0.2.4) eventmachine (1.0.4) @@ -428,6 +429,7 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) + rack-timeout (0.2.0) rails (4.1.9) actionmailer (= 4.1.9) actionpack (= 4.1.9) @@ -481,6 +483,8 @@ GEM rest-client (1.6.7) mime-types (>= 1.16) rinku (1.7.3) + robustly (0.0.3) + errbase rouge (1.7.4) rspec (2.99.0) rspec-core (~> 2.99.0) @@ -563,6 +567,9 @@ GEM temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) slop (3.6.0) + slowpoke (0.0.5) + rack-timeout (>= 0.1.0) + robustly spinach (0.8.7) colorize (= 0.5.8) gherkin-ruby (>= 0.3.1) @@ -772,6 +779,7 @@ DEPENDENCIES six slack-notifier (~> 1.0.0) slim + slowpoke spinach-rails spring (= 1.3.1) spring-commands-rspec (= 1.0.4) diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb new file mode 100644 index 00000000000..bc88595cf26 --- /dev/null +++ b/config/initializers/timeout.rb @@ -0,0 +1,8 @@ +# Slowpoke extends Rack::Timeout to gracefully kill Unicorn workers so they can clean up state. +Slowpoke.timeout = 60 + +# The `Rack::Timeout` middleware kills requests after 60 seconds (as set above). +# We're replacing it with our `Gitlab::Middleware::Timeout` that does the same, +# except ignoring Git-over-HTTP requests, letting those take as long as they need. + +Rails.application.config.middleware.swap(Rack::Timeout, Gitlab::Middleware::Timeout) diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index d8b4f5c7c32..29253b71f49 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -35,22 +35,10 @@ working_directory "/home/git/gitlab" # available in 0.94.0+ listen "/home/git/gitlab/tmp/sockets/gitlab.socket", :backlog => 1024 listen "127.0.0.1:8080", :tcp_nopush => true -# nuke workers after 30 seconds instead of 60 seconds (the default) -# -# NOTICE: git push over http depends on this value. -# If you want be able to push huge amount of data to git repository over http -# you will have to increase this value too. -# -# Example of output if you try to push 1GB repo to GitLab over http. -# -> git push http://gitlab.... master -# -# error: RPC failed; result=18, HTTP code = 200 -# fatal: The remote end hung up unexpectedly -# fatal: The remote end hung up unexpectedly -# -# For more information see http://stackoverflow.com/a/21682112/752049 -# -timeout 60 +# Kill workers after 1 hour. +# A shorter timeout of 60 seconds is enforced by rack-timeout for web requests. +# Git-over-HTTP only has the below timeout since large pulls/pushes can take a long time. +timeout 60 * 60 # feel free to point this anywhere accessible on the filesystem pid "/home/git/gitlab/tmp/pids/unicorn.pid" diff --git a/lib/gitlab/middleware/timeout.rb b/lib/gitlab/middleware/timeout.rb new file mode 100644 index 00000000000..015600392b9 --- /dev/null +++ b/lib/gitlab/middleware/timeout.rb @@ -0,0 +1,13 @@ +module Gitlab + module Middleware + class Timeout < Rack::Timeout + GRACK_REGEX = /[-\/\w\.]+\.git\//.freeze + + def call(env) + return @app.call(env) if env['PATH_INFO'] =~ GRACK_REGEX + + super + end + end + end +end diff --git a/public/503.html b/public/503.html new file mode 100644 index 00000000000..efdae0f512d --- /dev/null +++ b/public/503.html @@ -0,0 +1,13 @@ + + + + Page took too long to load (503) + + + +

    503

    +

    Page took too long to load.

    +
    +

    Please contact your GitLab administrator if this problem persists.

    + + -- GitLab From 6ddd45948f35dd6a3e04fbaf95d9f6935b4cbbc1 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 4 Mar 2015 22:31:28 +0000 Subject: [PATCH 1192/1609] Make Irker service use the supported events check --- app/models/project_services/irker_service.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index a0203a5bb10..14ea5360aa5 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -63,9 +63,15 @@ class IrkerService < Service 'irker' end - def execute(push_data) + def supported_events + %w(push) + end + + def execute(data) + return unless supported_events.include?(data[:object_kind]) + IrkerWorker.perform_async(project_id, channels, - colorize_messages, push_data, @settings) + colorize_messages, data, @settings) end def fields -- GitLab From 94229f24c7507c64786cd511ccf994e4fea83765 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 4 Mar 2015 15:10:31 -0800 Subject: [PATCH 1193/1609] Update spring to 1.3.3 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index d711efe6215..38e1c031d2a 100644 --- a/Gemfile +++ b/Gemfile @@ -250,7 +250,7 @@ group :development, :test do gem 'jasmine', '2.0.2' - gem "spring", '1.3.1' + gem "spring", '~> 1.3.1' gem "spring-commands-rspec", '1.0.4' gem "spring-commands-spinach", '1.0.0' end diff --git a/Gemfile.lock b/Gemfile.lock index 1413e967416..88ae8ac01be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -570,7 +570,7 @@ GEM capybara (>= 2.0.0) railties (>= 3) spinach (>= 0.4) - spring (1.3.1) + spring (1.3.3) spring-commands-rspec (1.0.4) spring (>= 0.9.1) spring-commands-spinach (1.0.0) @@ -773,7 +773,7 @@ DEPENDENCIES slack-notifier (~> 1.0.0) slim spinach-rails - spring (= 1.3.1) + spring (~> 1.3.1) spring-commands-rspec (= 1.0.4) spring-commands-spinach (= 1.0.0) stamp -- GitLab From 65105ff3bbe66363d4c922913dbc8c9514f1485c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 4 Mar 2015 17:22:55 -0800 Subject: [PATCH 1194/1609] Improve projects list * Add search filtering for group projects * Show all user projects on dashboard * Refactor projects list into one view * Hide big list of projects with 'Show all' button --- app/assets/javascripts/dashboard.js.coffee | 17 +--------- app/assets/javascripts/dispatcher.js.coffee | 6 +++- .../javascripts/projects_list.js.coffee | 24 ++++++++++++++ app/assets/javascripts/user.js.coffee | 1 + app/controllers/dashboard_controller.rb | 8 ++--- app/views/dashboard/_projects.html.haml | 19 ++---------- app/views/groups/_projects.html.haml | 31 ++++++------------- .../{dashboard => shared}/_project.html.haml | 14 ++++++--- app/views/shared/_projects_list.html.haml | 17 ++++++++++ app/views/users/_projects.html.haml | 20 ++++-------- 10 files changed, 77 insertions(+), 80 deletions(-) create mode 100644 app/assets/javascripts/projects_list.js.coffee rename app/views/{dashboard => shared}/_project.html.haml (56%) create mode 100644 app/views/shared/_projects_list.html.haml diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index 6ef5a539b8f..3bdb9469d06 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -1,22 +1,7 @@ class @Dashboard constructor: -> @initSidebarTab() - - $(".dash-filter").keyup -> - terms = $(this).val() - uiBox = $(this).parents('.panel').first() - if terms == "" || terms == undefined - uiBox.find(".dash-list li").show() - else - uiBox.find(".dash-list li").each (index) -> - name = $(this).find(".filter-title").text() - - if name.toLowerCase().search(terms.toLowerCase()) == -1 - $(this).hide() - else - $(this).show() - - + new ProjectsList() initSidebarTab: -> key = "dashboard_sidebar_filter" diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 591a3749a93..bf94fa3aaa0 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -62,9 +62,13 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'projects:commits:show' shortcut_handler = new ShortcutsNavigation() - when 'groups:show', 'projects:show' + when 'projects:show' new Activities() shortcut_handler = new ShortcutsNavigation() + when 'groups:show' + new Activities() + shortcut_handler = new ShortcutsNavigation() + new ProjectsList() when 'groups:members' new GroupMembers() new UsersSelect() diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee new file mode 100644 index 00000000000..c0e36d1ccc5 --- /dev/null +++ b/app/assets/javascripts/projects_list.js.coffee @@ -0,0 +1,24 @@ +class @ProjectsList + constructor: -> + $(".projects-list .js-expand").on 'click', (e) -> + e.preventDefault() + list = $(this).closest('.projects-list') + list.find("li").show() + list.find("li.bottom").hide() + + $(".projects-list-filter").keyup -> + terms = $(this).val() + uiBox = $(this).closest('.panel') + if terms == "" || terms == undefined + uiBox.find(".projects-list li").show() + else + uiBox.find(".projects-list li").each (index) -> + name = $(this).find(".filter-title").text() + + if name.toLowerCase().search(terms.toLowerCase()) == -1 + $(this).hide() + else + $(this).show() + uiBox.find(".projects-list li.bottom").hide() + + diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee index 8a2e2421c2e..d0d81f96921 100644 --- a/app/assets/javascripts/user.js.coffee +++ b/app/assets/javascripts/user.js.coffee @@ -1,3 +1,4 @@ class @User constructor: -> $('.profile-groups-avatars').tooltip("placement": "top") + new ProjectsList() diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 4930029e165..8f06a673584 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -5,15 +5,11 @@ class DashboardController < ApplicationController before_filter :event_filter, only: :show def show - # Fetch only 30 projects. - # If user needs more - point to Dashboard#projects page - @projects_limit = 30 - + @projects_limit = 20 @groups = current_user.authorized_groups.order_name_asc @has_authorized_projects = @projects.count > 0 @projects_count = @projects.count - @projects = @projects.includes(:namespace).limit(@projects_limit) - + @projects = @projects.includes(:namespace) @last_push = current_user.recent_push @publicish_project_count = Project.publicish(current_user).count diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index 0596738342f..3634b2bfd7b 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -1,25 +1,10 @@ .panel.panel-default .panel-heading.clearfix .input-group - = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'dash-filter form-control' + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if current_user.can_create_project? .input-group-addon.dash-new-project = link_to new_project_path do %strong New project - %ul.well-list.dash-list - - projects.each do |project| - %li.project-row - = render "project", project: project - - - if projects.blank? - %li - .nothing-here-block There are no projects here. - - if @projects_count > @projects_limit - %li.bottom - %span.light - #{@projects_limit} of #{pluralize(@projects_count, 'project')} displayed. - .pull-right - = link_to projects_dashboard_path do - Show all - %i.fa.fa-angle-right + = render 'shared/projects_list', projects: @projects, projects_limit: 20 diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index b505760fa8f..0dfd398f54d 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,23 +1,10 @@ .panel.panel-default - .panel-heading - Projects (#{projects.count}) - - if can? current_user, :create_projects, @group - .panel-head-actions - = link_to new_project_path(namespace_id: @group.id), class: "btn btn-new" do - %i.fa.fa-plus - New project - %ul.well-list - - if projects.blank? - .nothing-here-block This group has no projects yet - - projects.each do |project| - %li.project-row - = link_to project_path(project), class: dom_class(project) do - .dash-project-avatar - = project_icon(project, alt: '', class: 'avatar s40') - .dash-project-access-icon - = visibility_level_icon(project.visibility_level) - %span.str-truncated - %span.project-name - = project.name - %span.arrow - %i.fa.fa-angle-right + .panel-heading.clearfix + .input-group + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + - if current_user.can_create_project? + .input-group-addon.dash-new-project + = link_to new_project_path(namespace_id: @group.id) do + %strong New project + + = render 'shared/projects_list', projects: @projects, projects_limit: 20 diff --git a/app/views/dashboard/_project.html.haml b/app/views/shared/_project.html.haml similarity index 56% rename from app/views/dashboard/_project.html.haml rename to app/views/shared/_project.html.haml index fa9179cb249..d9ae0459862 100644 --- a/app/views/dashboard/_project.html.haml +++ b/app/views/shared/_project.html.haml @@ -1,6 +1,7 @@ = link_to project_path(project), class: dom_class(project) do - .dash-project-avatar - = project_icon(project, alt: '', class: 'avatar project-avatar s40') + - if avatar + .dash-project-avatar + = project_icon(project, alt: '', class: 'avatar project-avatar s40') .dash-project-access-icon = visibility_level_icon(project.visibility_level) %span.str-truncated @@ -10,5 +11,10 @@ \/ %span.project-name.filter-title = project.name - %span.arrow - %i.fa.fa-angle-right + - if stars + %span.pull-right.light + %i.fa.fa-star + = project.star_count + - else + %span.arrow + %i.fa.fa-angle-right diff --git a/app/views/shared/_projects_list.html.haml b/app/views/shared/_projects_list.html.haml new file mode 100644 index 00000000000..4c58092af44 --- /dev/null +++ b/app/views/shared/_projects_list.html.haml @@ -0,0 +1,17 @@ +- projects_limit = 20 unless local_assigns[:projects_limit] +- avatar = true unless local_assigns[:avatar] == false +- stars = false unless local_assigns[:stars] == true +%ul.well-list.projects-list + - projects.each_with_index do |project, i| + %li{class: (i >= projects_limit) ? 'project-row hide' : 'project-row'} + = render "shared/project", project: project, avatar: avatar, stars: stars + - if projects.blank? + %li + .nothing-here-block There are no projects here. + - if projects.count > projects_limit + %li.bottom + %span.light + #{projects_limit} of #{pluralize(projects.count, 'project')} displayed. + %span + = link_to '#', class: 'js-expand' do + Show all diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml index c925a48f550..6c7779be30e 100644 --- a/app/views/users/_projects.html.haml +++ b/app/views/users/_projects.html.haml @@ -1,21 +1,13 @@ - if @contributed_projects.present? .panel.panel-default .panel-heading Projects contributed to - %ul.well-list - - @contributed_projects.sort_by(&:star_count).reverse.each do |project| - %li - = link_to_project project - %span.pull-right.light - %i.fa.fa-star - = project.star_count + = render 'shared/projects_list', + projects: @contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 5, stars: true, avatar: false - if @projects.present? .panel.panel-default .panel-heading Personal projects - %ul.well-list - - @projects.sort_by(&:star_count).reverse.each do |project| - %li - = link_to_project project - %span.pull-right.light - %i.fa.fa-star - = project.star_count + = render 'shared/projects_list', + projects: @projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: false -- GitLab From 7b9b9f704eb1a14d6f943cc4d37d493a6cb8fde3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 4 Mar 2015 18:26:49 -0800 Subject: [PATCH 1195/1609] Fix project create link on group page --- app/views/groups/_projects.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 0dfd398f54d..6f53e125c47 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -2,7 +2,7 @@ .panel-heading.clearfix .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - - if current_user.can_create_project? + - if can? current_user, :create_projects, @group .input-group-addon.dash-new-project = link_to new_project_path(namespace_id: @group.id) do %strong New project -- GitLab From 730a49afc29d291287d5eae511c6e27e1a04ab72 Mon Sep 17 00:00:00 2001 From: Stefan Tatschner Date: Thu, 5 Mar 2015 11:06:06 +0100 Subject: [PATCH 1196/1609] Update rugments, fixes #8900 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index fa33a714a0e..19c71bd08c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -520,7 +520,7 @@ GEM rubyntlm (0.4.0) rubypants (0.2.0) rugged (0.21.4) - rugments (1.0.0.beta3) + rugments (1.0.0.beta4) safe_yaml (0.9.7) sanitize (2.1.0) nokogiri (>= 1.4.4) -- GitLab From f12ec5f4e80bee7690a2ec72cd65e72e62b21a18 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 5 Mar 2015 01:54:28 -0800 Subject: [PATCH 1197/1609] Add merge and issue event notification for HipChat --- CHANGELOG | 1 + .../project_services/hipchat_service.rb | 87 ++++++++++++++- .../project_services/hipchat_service_spec.rb | 102 ++++++++++++++++++ 3 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 spec/models/project_services/hipchat_service_spec.rb diff --git a/CHANGELOG b/CHANGELOG index f6853f6e114..68680bb7be5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) + - Added issue and merge request events to HipChat and Slack service (Stan Hu) - Fix merge request URL passed to Webhooks. (Stan Hu) - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) - Move labels/milestones tabs to sidebar diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 4fb80a98d24..9d094eaf146 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -45,7 +45,7 @@ class HipchatService < Service end def supported_events - %w(push) + %w(push issue merge_request) end def execute(data) @@ -62,7 +62,21 @@ class HipchatService < Service @gate ||= HipChat::Client.new(token, options) end - def create_message(push) + def create_message(data) + object_kind = data[:object_kind] + + message = \ + case object_kind + when "push" + create_push_message(data) + when "issue" + create_issue_message(data) unless is_update?(data) + when "merge_request" + create_merge_request_message(data) unless is_update?(data) + end + end + + def create_push_message(push) ref = push[:ref].gsub("refs/heads/", "") before = push[:before] after = push[:after] @@ -71,9 +85,9 @@ class HipchatService < Service message << "#{push[:user_name]} " if before.include?('000000') message << "pushed new branch
    #{ref}"\ - " to "\ - "#{project.name_with_namespace.gsub!(/\s/, "")}\n" + "#{project_url}/commits/#{URI.escape(ref)}\">#{ref}"\ + " to "\ + "#{project_url}\n" elsif after.include?('000000') message << "removed branch #{ref} from #{project.name_with_namespace.gsub!(/\s/,'')} \n" else @@ -93,4 +107,67 @@ class HipchatService < Service message end + + def create_issue_message(data) + username = data[:user][:username] + + obj_attr = data[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + title = obj_attr[:title] + state = obj_attr[:state] + issue_iid = obj_attr[:iid] + issue_url = obj_attr[:url] + description = obj_attr[:description] + + issue_link = "##{issue_iid}" + message = "#{username} #{state} issue #{issue_link} in #{project_link}: #{title}" + + if description + description = description.truncate(200, separator: ' ', omission: '...') + message << "
    #{description}
    " + end + + message + end + + def create_merge_request_message(data) + username = data[:user][:username] + + obj_attr = data[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + merge_request_id = obj_attr[:iid] + source_branch = obj_attr[:source_branch] + target_branch = obj_attr[:target_branch] + state = obj_attr[:state] + description = obj_attr[:description] + title = obj_attr[:title] + + merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" + merge_request_link = "##{merge_request_id}" + message = "#{username} #{state} merge request #{merge_request_link} in " \ + "#{project_link}: #{title}" + + if description + description = description.truncate(200, separator: ' ', omission: '...') + message << "
    #{description}
    " + end + + message + end + + def project_name + project.name_with_namespace.gsub(/\s/, '') + end + + def project_url + project.web_url + end + + def project_link + "#{project_name}" + end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb new file mode 100644 index 00000000000..804c3d5ddd9 --- /dev/null +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -0,0 +1,102 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# + +require 'spec_helper' + +describe HipchatService do + describe "Associations" do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe "Execute" do + let(:hipchat) { HipchatService.new } + let(:user) { create(:user, username: 'username') } + let(:project) { create(:project, name: 'project') } + let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' } + let(:project_name) { project.name_with_namespace.gsub(/\s/, '') } + + before(:each) do + hipchat.stub( + project_id: project.id, + project: project, + room: 123456, + server: 'https://hipchat.example.com', + token: 'verySecret' + ) + WebMock.stub_request(:post, api_url) + end + + context 'push events' do + let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) } + + it "should call Hipchat API for push events" do + hipchat.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + end + + context 'issue events' do + let(:issue) { create(:issue, title: 'Awesome issue', description: 'please fix') } + let(:issue_service) { Issues::CreateService.new(project, user) } + let(:issues_sample_data) { issue_service.hook_data(issue, 'open') } + + it "should call Hipchat API for issue events" do + hipchat.execute(issues_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create an issue message" do + message = hipchat.send(:create_issue_message, issues_sample_data) + + obj_attr = issues_sample_data[:object_attributes] + expect(message).to eq("#{user.username} opened issue " \ + "##{obj_attr["iid"]} in " \ + "#{project_name}: " \ + "Awesome issue" \ + "
    please fix
    ") + end + end + + context 'merge request events' do + let(:merge_request) { create(:merge_request, description: 'please fix', title: 'Awesome merge request', target_project: project, source_project: project) } + let(:merge_service) { MergeRequests::CreateService.new(project, user) } + let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') } + + it "should call Hipchat API for merge requests events" do + hipchat.execute(merge_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create a merge request message" do + message = hipchat.send(:create_merge_request_message, + merge_sample_data) + + obj_attr = merge_sample_data[:object_attributes] + expect(message).to eq("#{user.username} opened merge request " \ + "##{obj_attr["iid"]} in " \ + "#{project_name}: " \ + "Awesome merge request" \ + "
    please fix
    ") + end + end + end +end -- GitLab From 38862308f003d39b660920c36e142b2ece0a4f70 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Mar 2015 09:43:41 -0800 Subject: [PATCH 1198/1609] Cache project row on dashboard, group and user page --- app/views/shared/_project.html.haml | 41 +++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/app/views/shared/_project.html.haml b/app/views/shared/_project.html.haml index d9ae0459862..8746970c239 100644 --- a/app/views/shared/_project.html.haml +++ b/app/views/shared/_project.html.haml @@ -1,20 +1,21 @@ -= link_to project_path(project), class: dom_class(project) do - - if avatar - .dash-project-avatar - = project_icon(project, alt: '', class: 'avatar project-avatar s40') - .dash-project-access-icon - = visibility_level_icon(project.visibility_level) - %span.str-truncated - %span.namespace-name - - if project.namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name - - if stars - %span.pull-right.light - %i.fa.fa-star - = project.star_count - - else - %span.arrow - %i.fa.fa-angle-right += cache [project, controller.controller_name, controller.action_name] do + = link_to project_path(project), class: dom_class(project) do + - if avatar + .dash-project-avatar + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + .dash-project-access-icon + = visibility_level_icon(project.visibility_level) + %span.str-truncated + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name + - if stars + %span.pull-right.light + %i.fa.fa-star + = project.star_count + - else + %span.arrow + %i.fa.fa-angle-right -- GitLab From c17e11ca27882c0a35e23eba72689f2fdb89680a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 5 Mar 2015 11:35:36 -0800 Subject: [PATCH 1199/1609] Bump gitlab_git to fix 500 with annotated tags w/o message --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index c2f419832af..462c932584d 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '7.0.0.rc15' +gem "gitlab_git", '7.0.1' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' diff --git a/Gemfile.lock b/Gemfile.lock index 19c71bd08c8..9be5983430b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -213,7 +213,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.0.1.1) emoji (~> 1.0.1) - gitlab_git (7.0.0.rc15) + gitlab_git (7.0.1) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -708,7 +708,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.0.1.1) - gitlab_git (= 7.0.0.rc15) + gitlab_git (= 7.0.1) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.0) gollum-lib (~> 4.0.0) -- GitLab From 8b53d9efe648f10e0572c2d8017489d0d3bb4755 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Mar 2015 01:41:29 -0800 Subject: [PATCH 1200/1609] Fix bug with active tab remembering (saving cookie with different path) --- app/assets/javascripts/project_show.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/project_show.js.coffee b/app/assets/javascripts/project_show.js.coffee index d0eaaad92b8..6828ae471e5 100644 --- a/app/assets/javascripts/project_show.js.coffee +++ b/app/assets/javascripts/project_show.js.coffee @@ -6,7 +6,7 @@ class @ProjectShow new Flash('Star toggle failed. Try again later.', 'alert') $("a[data-toggle='tab']").on "shown.bs.tab", (e) -> - $.cookie "default_view", $(e.target).attr("href"), { expires: 30 } + $.cookie "default_view", $(e.target).attr("href"), { expires: 30, path: '/' } defaultView = $.cookie("default_view") if defaultView -- GitLab From b14d21e1749b682bbdfebad80cf404c2b640b551 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Mar 2015 02:18:53 -0800 Subject: [PATCH 1201/1609] Use bootstrap buttons with custom colors instead of own css --- app/assets/stylesheets/generic/buttons.scss | 121 ++------------------ app/assets/stylesheets/gl_bootstrap.scss | 7 ++ 2 files changed, 14 insertions(+), 114 deletions(-) diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index 3b360275065..d106e3b201e 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -1,115 +1,5 @@ .btn { - display: inline-block; - margin-bottom: 0; - font-weight: normal; - text-align: center; - vertical-align: middle; - cursor: pointer; - background-image: none; - border: $btn-border; - white-space: nowrap; - padding: 6px 12px; - font-size: 13px; - line-height: 18px; - border-radius: 4px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - color: #444444; - background-color: #fff; - text-shadow: none; - - &.hover, - &:hover { - color: #444444; - text-decoration: none; - background-color: #ebebeb; - border-color: #adadad; - } - - &.focus, - &:focus { - color: #444444; - text-decoration: none; - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; - } - - &.active, - &:active { - outline: 0; - background-image: none; - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - } - - &.disabled, - &[disabled] { - cursor: not-allowed; - pointer-events: none; - opacity: 0.65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - box-shadow: none; - } - - &.btn-primary { - color: #ffffff; - background-color: $bg_primary; - border-color: $border_primary; - - &.hover, - &:hover, - &.disabled, - &[disabled] { - color: #ffffff; - } - } - - &.btn-success { - color: #ffffff; - background-color: $bg_success; - border-color: $border_success; - - - &.hover, - &:hover, - &.disabled, - &[disabled] { - color: #ffffff; - } - } - - &.btn-danger { - color: #ffffff; - background-color: $bg_danger; - border-color: $border_danger; - - - &.hover, - &:hover, - &.disabled, - &[disabled] { - color: #ffffff; - } - } - - &.btn-warning { - color: #ffffff; - background-color: $bg_warning; - border-color: $border_warning; - - - &.hover, - &:hover, - &.disabled, - &[disabled] { - color: #ffffff; - } - } + @extend .btn-default; &.btn-new { @extend .btn-success; @@ -174,9 +64,12 @@ } } - &.btn-lg { - font-size: 15px; - line-height: 1.4; + &.btn-save { + @extend .btn-primary; + } + + &.btn-new, &.btn-create { + @extend .btn-success; } } diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/gl_bootstrap.scss index 6efa56544a5..34ddf6f8717 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/gl_bootstrap.scss @@ -8,6 +8,12 @@ $nav-pills-active-link-hover-bg: $bg_primary; $pagination-active-bg: $bg_primary; $list-group-active-bg: $bg_primary; +$brand-primary: $bg_primary; +$brand-success: $bg_success; +$brand-info: #029ACF; +$brand-warning: $bg_warning; +$brand-danger: $bg_danger; + // Core variables and mixins @import "bootstrap/variables"; @import "bootstrap/mixins"; @@ -23,6 +29,7 @@ $list-group-active-bg: $bg_primary; @import "bootstrap/grid"; @import "bootstrap/tables"; @import "bootstrap/forms"; +@import "bootstrap/buttons"; // Components @import "bootstrap/component-animations"; -- GitLab From 757dca2b78c8b218295c855d6b7529bad05ae24b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 13:26:33 +0100 Subject: [PATCH 1202/1609] Escape wildcards when searching LDAP by username. --- CHANGELOG | 1 + lib/gitlab/ldap/authentication.rb | 2 +- lib/gitlab/ldap/person.rb | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 37aee53bc0a..59846b778e1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 7.9.0 (unreleased) - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options - Add grouped milestones from all projects to dashboard. + - Escape wildcards when searching LDAP by username. v 7.8.1 - Fix run of custom post receive hooks diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb index 8af2c74e959..649cf3194b8 100644 --- a/lib/gitlab/ldap/authentication.rb +++ b/lib/gitlab/ldap/authentication.rb @@ -50,7 +50,7 @@ module Gitlab end def user_filter(login) - filter = Net::LDAP::Filter.eq(config.uid, login) + filter = Net::LDAP::Filter.equals(config.uid, login) # Apply LDAP user filter if present if config.user_filter.present? diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 3e0b3e6cbf8..3c426179375 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -9,10 +9,12 @@ module Gitlab attr_accessor :entry, :provider def self.find_by_uid(uid, adapter) + uid = Net::LDAP::Filter.escape(uid) adapter.user(adapter.config.uid, uid) end def self.find_by_dn(dn, adapter) + dn = Net::LDAP::Filter.escape(dn) adapter.user('dn', dn) end -- GitLab From dc558eb24e7432570e1fdbd73c9d67551602c8dc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 13:49:27 +0100 Subject: [PATCH 1203/1609] Fix width of text in milestone lists. --- app/assets/stylesheets/sections/milestone.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/sections/milestone.scss b/app/assets/stylesheets/sections/milestone.scss index d20391e38fd..29ad4e24f0c 100644 --- a/app/assets/stylesheets/sections/milestone.scss +++ b/app/assets/stylesheets/sections/milestone.scss @@ -1,3 +1,3 @@ .issues-sortable-list .str-truncated { - max-width: 70%; + max-width: 90%; } -- GitLab From be94e135524943236f85a63de08fa8ccc445a0af Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 14:03:41 +0100 Subject: [PATCH 1204/1609] Add assignee icon to milestone merge requests. --- app/assets/javascripts/milestone.js.coffee | 7 +++++++ app/controllers/projects/merge_requests_controller.rb | 6 ++++++ app/views/projects/milestones/_merge_request.html.haml | 3 +++ 3 files changed, 16 insertions(+) diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index c42f31933d3..d644d50b669 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -49,6 +49,13 @@ class @Milestone data: data success: (data) -> if data.saved == true + if data.assignee_avatar_url + img_tag = $('') + img_tag.attr('src', data.assignee_avatar_url) + img_tag.addClass('avatar s16') + $(li).find('.assignee-icon').html(img_tag) + else + $(li).find('.assignee-icon').html('') $(li).effect 'highlight' else new Flash("Issue update failed", 'alert') diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 26d4c51773f..848cf367493 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -100,6 +100,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request.target_project, @merge_request], notice: 'Merge request was successfully updated.') end + format.json do + render json: { + saved: @merge_request.valid?, + assignee_avatar_url: @merge_request.assignee.try(:avatar_url) + } + end end else render "edit" diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml index 46f2df1b183..42fbd0cd2ca 100644 --- a/app/views/projects/milestones/_merge_request.html.haml +++ b/app/views/projects/milestones/_merge_request.html.haml @@ -3,3 +3,6 @@ = link_to [@project.namespace.becomes(Namespace), @project, merge_request] do %span.cgray ##{merge_request.iid} = link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title + .pull-right.assignee-icon + - if merge_request.assignee + = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16" -- GitLab From b673d87227867f6d142ee6e615c750a600661c6b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 15:01:13 +0100 Subject: [PATCH 1205/1609] Send notifications and leave system comments when bulk updating issues. --- CHANGELOG | 1 + app/controllers/projects/issues_controller.rb | 11 +++++- app/services/issues/bulk_update_service.rb | 35 ++++++------------- app/views/projects/issues/index.html.haml | 4 +-- .../issues/bulk_update_service_spec.rb | 28 ++++++--------- 5 files changed, 33 insertions(+), 46 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b927b60140e..698ca0881fc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 7.9.0 (unreleased) - Add Bitbucket omniauth provider. - Add Bitbucket importer. - Support referencing issues to a project whose name starts with a digit + - Send notifications and leave system comments when bulk updating issues. v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 6a2af08a199..1f1a9b4d43a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -93,7 +93,7 @@ class Projects::IssuesController < Projects::ApplicationController end def bulk_update - result = Issues::BulkUpdateService.new(project, current_user, params).execute + result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute redirect_to :back, notice: "#{result[:count]} issues updated" end @@ -141,4 +141,13 @@ class Projects::IssuesController < Projects::ApplicationController :milestone_id, :state_event, :task_num, label_ids: [] ) end + + def bulk_update_params + params.require(:update).permit( + :issues_ids, + :assignee_id, + :milestone_id, + :state_event + ) + end end diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index f72a346af6f..c7cd20b6b60 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -1,38 +1,23 @@ module Issues class BulkUpdateService < BaseService def execute - update_data = params[:update] + issues_ids = params.delete(:issues_ids).split(",") + issue_params = params - issues_ids = update_data[:issues_ids].split(",") - milestone_id = update_data[:milestone_id] - assignee_id = update_data[:assignee_id] - status = update_data[:status] - - new_state = nil - - if status.present? - if status == 'closed' - new_state = :close - else - new_state = :reopen - end - end - - opts = {} - opts[:milestone_id] = milestone_id if milestone_id.present? - opts[:assignee_id] = assignee_id if assignee_id.present? + issue_params.delete(:state_event) unless issue_params[:state_event].present? + issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present? + issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present? issues = Issue.where(id: issues_ids) - issues = issues.select { |issue| can?(current_user, :modify_issue, issue) } - issues.each do |issue| - issue.update_attributes(opts) - issue.send new_state if new_state + next unless can?(current_user, :modify_issue, issue) + + Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue) end { - count: issues.count, - success: !issues.count.zero? + count: issues.count, + success: !issues.count.zero? } end end diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7defc8787a9..cbbcb1d06c0 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -25,11 +25,11 @@ .clearfix .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do - = select_tag('update[status]', options_for_select([['Open', 'open'], ['Closed', 'closed']]), prompt: "Status") + = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status") = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") = hidden_field_tag 'update[issues_ids]', [] - = hidden_field_tag :status, params[:status] + = hidden_field_tag :state_event, params[:state_event] = button_tag "Update issues", class: "btn update_selected_issues btn-save" .issues-holder diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 504213e667f..a97c55011c9 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -21,10 +21,8 @@ describe Issues::BulkUpdateService do create(:issue, project: @project) end @params = { - update: { - status: 'closed', - issues_ids: @issues.map(&:id) - } + state_event: 'close', + issues_ids: @issues.map(&:id) } end @@ -46,10 +44,8 @@ describe Issues::BulkUpdateService do create(:closed_issue, project: @project) end @params = { - update: { - status: 'reopen', - issues_ids: @issues.map(&:id) - } + state_event: 'reopen', + issues_ids: @issues.map(&:id) } end @@ -69,10 +65,8 @@ describe Issues::BulkUpdateService do before do @new_assignee = create :user @params = { - update: { - issues_ids: [issue.id], - assignee_id: @new_assignee.id - } + issues_ids: [issue.id], + assignee_id: @new_assignee.id } end @@ -88,7 +82,7 @@ describe Issues::BulkUpdateService do @project.issues.first.update_attribute(:assignee, @new_assignee) expect(@project.issues.first.assignee).not_to be_nil - @params[:update][:assignee_id] = -1 + @params[:assignee_id] = -1 Issues::BulkUpdateService.new(@project, @user, @params).execute expect(@project.issues.first.assignee).to be_nil @@ -98,7 +92,7 @@ describe Issues::BulkUpdateService do @project.issues.first.update_attribute(:assignee, @new_assignee) expect(@project.issues.first.assignee).not_to be_nil - @params[:update][:assignee_id] = '' + @params[:assignee_id] = '' Issues::BulkUpdateService.new(@project, @user, @params).execute expect(@project.issues.first.assignee).not_to be_nil @@ -110,10 +104,8 @@ describe Issues::BulkUpdateService do before do @milestone = create :milestone @params = { - update: { - issues_ids: [issue.id], - milestone_id: @milestone.id - } + issues_ids: [issue.id], + milestone_id: @milestone.id } end -- GitLab From 7e204cf389346d23e71bc4c2fa9e14cf82a7ed2e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 5 Mar 2015 10:38:23 -0800 Subject: [PATCH 1206/1609] Added comment notification events to HipChat and Slack services. Supports four different event types all bundled under the "note" event type: - comments on a commit - comments on an issue - comments on a merge request - comments on a code snippet --- CHANGELOG | 3 +- .../projects/services_controller.rb | 3 +- app/helpers/gitlab_routing_helper.rb | 4 + app/models/concerns/mentionable.rb | 1 - app/models/note.rb | 8 ++ app/models/project_services/asana_service.rb | 2 +- .../project_services/assembla_service.rb | 1 + app/models/project_services/bamboo_service.rb | 1 + .../project_services/buildbox_service.rb | 1 + .../project_services/campfire_service.rb | 1 + .../gitlab_issue_tracker_service.rb | 1 + .../project_services/hipchat_service.rb | 74 +++++++++- .../project_services/issue_tracker_service.rb | 1 + app/models/project_services/jira_service.rb | 1 + .../pivotaltracker_service.rb | 1 + .../project_services/pushover_service.rb | 1 + .../project_services/redmine_service.rb | 1 + app/models/project_services/slack_service.rb | 6 +- .../slack_service/note_message.rb | 82 +++++++++++ .../project_services/teamcity_service.rb | 1 + app/models/service.rb | 2 + app/models/snippet.rb | 4 + app/services/notes/create_service.rb | 12 ++ app/views/projects/services/_form.html.haml | 8 ++ ...50225065047_add_note_events_to_services.rb | 5 + db/schema.rb | 3 +- lib/gitlab/note_data_builder.rb | 77 +++++++++++ lib/gitlab/url_builder.rb | 29 ++++ spec/factories/merge_requests.rb | 2 +- spec/factories/notes.rb | 6 + spec/lib/gitlab/note_data_builder_spec.rb | 73 ++++++++++ spec/lib/gitlab/url_builder_spec.rb | 58 ++++++++ .../project_services/hipchat_service_spec.rb | 86 ++++++++++++ .../slack_service/note_message_spec.rb | 129 ++++++++++++++++++ .../project_services/slack_service_spec.rb | 67 ++++++++- 35 files changed, 736 insertions(+), 19 deletions(-) create mode 100644 app/models/project_services/slack_service/note_message.rb create mode 100644 db/migrate/20150225065047_add_note_events_to_services.rb create mode 100644 lib/gitlab/note_data_builder.rb create mode 100644 spec/lib/gitlab/note_data_builder_spec.rb create mode 100644 spec/models/project_services/slack_service/note_message_spec.rb diff --git a/CHANGELOG b/CHANGELOG index b927b60140e..611c6c77d54 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) - - Added issue and merge request events to HipChat and Slack service (Stan Hu) + - Added comment notification events to HipChat and Slack services (Stan Hu) + - Added issue and merge request events to HipChat and Slack services (Stan Hu) - Fix merge request URL passed to Webhooks. (Stan Hu) - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) - Move labels/milestones tabs to sidebar diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 087579de106..382d63d053b 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -52,7 +52,8 @@ class Projects::ServicesController < Projects::ApplicationController :build_key, :server, :teamcity_url, :build_type, :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :colorize_messages, :channels, - :push_events, :issues_events, :merge_requests_events, :tag_push_events + :push_events, :issues_events, :merge_requests_events, :tag_push_events, + :note_events ) end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index ac37f909ce9..8518a47a3a0 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -44,4 +44,8 @@ module GitlabRoutingHelper def merge_request_url(entity, *args) namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) end + + def snippet_url(entity, *args) + namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) + end end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 50be458bf24..74900d4675d 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -99,5 +99,4 @@ module Mentionable preexisting = references(p, original) create_cross_references!(p, a, preexisting) end - end diff --git a/app/models/note.rb b/app/models/note.rb index e6c258ffbe9..b19d7b0f256 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -308,6 +308,10 @@ class Note < ActiveRecord::Base end end + def hook_attrs + attributes + end + def set_diff # First lets find notes with same diff # before iterating over all mr diffs @@ -466,6 +470,10 @@ class Note < ActiveRecord::Base for_merge_request? && for_diff_line? end + def for_project_snippet? + noteable_type == "Snippet" + end + # override to return commits, which are not active record def noteable if for_commit? diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 8dce33e670b..6a622207385 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -15,8 +15,8 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # - require 'asana' class AsanaService < Service diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 6dc2500e770..fb7e0c0fb0d 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class AssemblaService < Service diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 50b7cb795d7..0100f1e4a10 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class BambooService < CiService diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb index 1270484ff6d..270863c1576 100644 --- a/app/models/project_services/buildbox_service.rb +++ b/app/models/project_services/buildbox_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # require "addressable/uri" diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 21e1ca603bb..1c63444fbf9 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class CampfireService < Service diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 90be1e42b26..84346350a6c 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class GitlabIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 9d094eaf146..d24351a7b13 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -45,7 +45,7 @@ class HipchatService < Service end def supported_events - %w(push issue merge_request) + %w(push issue merge_request note) end def execute(data) @@ -73,6 +73,8 @@ class HipchatService < Service create_issue_message(data) unless is_update?(data) when "merge_request" create_merge_request_message(data) unless is_update?(data) + when "note" + create_note_message(data) end end @@ -108,6 +110,14 @@ class HipchatService < Service message end + def format_body(body) + if body + body = body.truncate(200, separator: ' ', omission: '...') + end + + "
    #{body}
    " + end + def create_issue_message(data) username = data[:user][:username] @@ -123,8 +133,8 @@ class HipchatService < Service message = "#{username} #{state} issue #{issue_link} in #{project_link}: #{title}" if description - description = description.truncate(200, separator: ' ', omission: '...') - message << "
    #{description}
    " + description = format_body(description) + message << description end message @@ -148,8 +158,62 @@ class HipchatService < Service "#{project_link}: #{title}" if description - description = description.truncate(200, separator: ' ', omission: '...') - message << "
    #{description}
    " + description = format_body(description) + message << description + end + + message + end + + def format_title(title) + "" + title.lines.first.chomp + "" + end + + def create_note_message(data) + data = HashWithIndifferentAccess.new(data) + username = data[:user][:username] + + repo_attr = HashWithIndifferentAccess.new(data[:repository]) + + obj_attr = HashWithIndifferentAccess.new(data[:object_attributes]) + note = obj_attr[:note] + note_url = obj_attr[:url] + noteable_type = obj_attr[:noteable_type] + + case noteable_type + when "Commit" + commit_attr = HashWithIndifferentAccess.new(data[:commit]) + subject_desc = commit_attr[:id] + subject_desc = Commit.truncate_sha(subject_desc) + subject_type = "commit" + title = format_title(commit_attr[:message]) + when "Issue" + subj_attr = HashWithIndifferentAccess.new(data[:issue]) + subject_id = subj_attr[:iid] + subject_desc = "##{subject_id}" + subject_type = "issue" + title = format_title(subj_attr[:title]) + when "MergeRequest" + subj_attr = HashWithIndifferentAccess.new(data[:merge_request]) + subject_id = subj_attr[:iid] + subject_desc = "##{subject_id}" + subject_type = "merge request" + title = format_title(subj_attr[:title]) + when "Snippet" + subj_attr = HashWithIndifferentAccess.new(data[:snippet]) + subject_id = subj_attr[:id] + subject_desc = "##{subject_id}" + subject_type = "snippet" + title = format_title(subj_attr[:title]) + end + + subject_html = "#{subject_type} #{subject_desc}" + message = "#{username} commented on #{subject_html} in #{project_link}: " + message << title + + if note + note = format_body(note) + message << note end message diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 7fe1326890b..16876335b67 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class IssueTrackerService < Service diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 4a76f23c693..fcd9dc2f336 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class JiraService < IssueTrackerService diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 13cbb9bdbcf..ade9ee97873 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class PivotaltrackerService < Service diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index a67abb34831..0ce324434db 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class PushoverService < Service diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 7d7d7d7660b..dd9ba97ee1f 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class RedmineService < IssueTrackerService diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index c529a784017..a58840116f4 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class SlackService < Service @@ -43,7 +44,7 @@ class SlackService < Service end def supported_events - %w(push issue merge_request) + %w(push issue merge_request note) end def execute(data) @@ -69,6 +70,8 @@ class SlackService < Service IssueMessage.new(data) unless is_update?(data) when "merge_request" MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) end opt = {} @@ -99,3 +102,4 @@ end require "slack_service/issue_message" require "slack_service/push_message" require "slack_service/merge_message" +require "slack_service/note_message" diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb new file mode 100644 index 00000000000..f93dc358f61 --- /dev/null +++ b/app/models/project_services/slack_service/note_message.rb @@ -0,0 +1,82 @@ +class SlackService + class NoteMessage < BaseMessage + attr_reader :message + attr_reader :username + attr_reader :project_name + attr_reader :project_link + attr_reader :note + attr_reader :note_url + attr_reader :title + + def initialize(params) + params = HashWithIndifferentAccess.new(params) + @username = params[:user][:username] + @project_name = params[:project_name] + @project_url = params[:project_url] + + obj_attr = params[:object_attributes] + obj_attr = HashWithIndifferentAccess.new(obj_attr) + @note = obj_attr[:note] + @note_url = obj_attr[:url] + noteable_type = obj_attr[:noteable_type] + + case noteable_type + when "Commit" + create_commit_note(HashWithIndifferentAccess.new(params[:commit])) + when "Issue" + create_issue_note(HashWithIndifferentAccess.new(params[:issue])) + when "MergeRequest" + create_merge_note(HashWithIndifferentAccess.new(params[:merge_request])) + when "Snippet" + create_snippet_note(HashWithIndifferentAccess.new(params[:snippet])) + end + end + + def attachments + description_message + end + + private + + def format_title(title) + title.lines.first.chomp + end + + def create_commit_note(commit) + commit_sha = commit[:id] + commit_sha = Commit.truncate_sha(commit_sha) + commit_link = "[commit #{commit_sha}](#{@note_url})" + title = format_title(commit[:message]) + @message = "#{@username} commented on #{commit_link} in #{project_link}: *#{title}*" + end + + def create_issue_note(issue) + issue_iid = issue[:iid] + note_link = "[issue ##{issue_iid}](#{@note_url})" + title = format_title(issue[:title]) + @message = "#{@username} commented on #{note_link} in #{project_link}: *#{title}*" + end + + def create_merge_note(merge_request) + merge_request_id = merge_request[:iid] + merge_request_link = "[merge request ##{merge_request_id}](#{@note_url})" + title = format_title(merge_request[:title]) + @message = "#{@username} commented on #{merge_request_link} in #{project_link}: *#{title}*" + end + + def create_snippet_note(snippet) + snippet_id = snippet[:id] + snippet_link = "[snippet ##{snippet_id}](#{@note_url})" + title = format_title(snippet[:title]) + @message = "#{@username} commented on #{snippet_link} in #{project_link}: *#{title}*" + end + + def description_message + [{ text: format(@note), color: attachment_color }] + end + + def project_link + "[#{@project_name}](#{@project_url})" + end + end +end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index cd9388de873..038c200adc7 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -15,6 +15,7 @@ # issues_events :boolean default(TRUE) # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null # class TeamcityService < CiService diff --git a/app/models/service.rb b/app/models/service.rb index 8f6a9d57d3b..33734e97c55 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -28,6 +28,7 @@ class Service < ActiveRecord::Base default_value_for :issues_events, true default_value_for :merge_requests_events, true default_value_for :tag_push_events, true + default_value_for :note_events, true after_initialize :initialize_properties @@ -42,6 +43,7 @@ class Service < ActiveRecord::Base scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) } scope :issue_hooks, -> { where(issues_events: true, active: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } + scope :note_hooks, -> { where(note_events: true, active: true) } def activated? active diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 82c1ab94446..3fb2ec1d66c 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -59,6 +59,10 @@ class Snippet < ActiveRecord::Base content end + def hook_attrs + attributes + end + def size 0 end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index f64006a4edc..e969061f229 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -17,10 +17,22 @@ module Notes note.references.each do |mentioned| Note.create_cross_reference_note(mentioned, note.noteable, note.author, note.project) end + + execute_hooks(note) end end note end + + def hook_data(note) + Gitlab::NoteDataBuilder.build(note, current_user) + end + + def execute_hooks(note) + note_data = hook_data(note) + # TODO: Support Webhooks + note.project.execute_services(note_data, :note_hooks) + end end end diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 55ac85c32b9..defcdbe268e 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -47,6 +47,14 @@ %strong Tag push events %p.light This url will be triggered when a new tag is pushed to the repository + - if @service.supported_events.include?("note") + %div + = f.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = f.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment - if @service.supported_events.include?("issue") %div = f.check_box :issues_events, class: 'pull-left' diff --git a/db/migrate/20150225065047_add_note_events_to_services.rb b/db/migrate/20150225065047_add_note_events_to_services.rb new file mode 100644 index 00000000000..d54ba9e482f --- /dev/null +++ b/db/migrate/20150225065047_add_note_events_to_services.rb @@ -0,0 +1,5 @@ +class AddNoteEventsToServices < ActiveRecord::Migration + def change + add_column :services, :note_events, :boolean, default: true, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 1a9b512e159..a686bb4b3cd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150223022001) do +ActiveRecord::Schema.define(version: 20150225065047) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -371,6 +371,7 @@ ActiveRecord::Schema.define(version: 20150223022001) do t.boolean "issues_events", default: true t.boolean "merge_requests_events", default: true t.boolean "tag_push_events", default: true + t.boolean "note_events", default: true, null: false end add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb new file mode 100644 index 00000000000..644dec45dca --- /dev/null +++ b/lib/gitlab/note_data_builder.rb @@ -0,0 +1,77 @@ +module Gitlab + class NoteDataBuilder + class << self + # Produce a hash of post-receive data + # + # For all notes: + # + # data = { + # object_kind: "note", + # user: { + # name: String, + # username: String, + # avatar_url: String + # } + # project_id: Integer, + # repository: { + # name: String, + # url: String, + # description: String, + # homepage: String, + # } + # object_attributes: { + # + # } + # : { + # } + # note-specific data is a hash with one of the following keys and contains + # the hook data for that type. + # - commit + # - issue + # - merge_request + # - snippet + # + def build(note, user) + project = note.project + data = build_base_data(project, user, note) + + if note.for_commit? + data[:commit] = build_data_for_commit(project, user, note) + elsif note.for_issue? + data[:issue] = note.noteable.hook_attrs + elsif note.for_merge_request? + data[:merge_request] = note.noteable.hook_attrs + elsif note.for_project_snippet? + data[:snippet] = note.noteable.hook_attrs + end + + data + end + + def build_base_data(project, user, note) + base_data = { + object_kind: "note", + user: user.hook_attrs, + project_id: project.id, + repository: { + name: project.name, + url: project.url_to_repo, + description: project.description, + homepage: project.web_url, + }, + object_attributes: note.hook_attrs + } + + base_data[:object_attributes][:url] = + Gitlab::UrlBuilder.new(:note).build(note.id) + base_data + end + + def build_data_for_commit(project, user, note) + # commit_id is the SHA hash + commit = project.repository.commit(note.commit_id) + commit.hook_attrs(project) + end + end + end +end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index ab7c8ad89f3..6830d15875a 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -13,6 +13,9 @@ module Gitlab build_issue_url(id) when :merge_request build_merge_request_url(id) + when :note + build_note_url(id) + end end @@ -27,5 +30,31 @@ module Gitlab merge_request = MergeRequest.find(id) merge_request_url(merge_request, host: Gitlab.config.gitlab['url']) end + + def build_note_url(id) + note = Note.find(id) + if note.for_commit? + namespace_project_commit_url(namespace_id: note.project.namespace, + id: note.commit_id, + project_id: note.project, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_issue? + issue = Issue.find(note.noteable_id) + issue_url(issue, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_merge_request? + merge_request = MergeRequest.find(note.noteable_id) + merge_request_url(merge_request, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + elsif note.for_project_snippet? + snippet = Snippet.find(note.noteable_id) + snippet_url(snippet, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") + end + end end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 6ce1d7446fe..77cd37c22d9 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -40,7 +40,7 @@ FactoryGirl.define do source_branch "master" target_branch "feature" - merge_status :can_be_merged + merge_status "can_be_merged" trait :with_diffs do end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 83d0cc62dbf..f1c33461b55 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -30,6 +30,7 @@ FactoryGirl.define do factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note] factory :note_on_merge_request, traits: [:on_merge_request] factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] + factory :note_on_project_snippet, traits: [:on_project_snippet] trait :on_commit do project factory: :project @@ -52,6 +53,11 @@ FactoryGirl.define do noteable_type "Issue" end + trait :on_project_snippet do + noteable_id 1 + noteable_type "Snippet" + end + trait :with_attachment do attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb new file mode 100644 index 00000000000..448cd0c6880 --- /dev/null +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe 'Gitlab::NoteDataBuilder' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:data) { Gitlab::NoteDataBuilder.build(note, user) } + let(:note_url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors + + before(:each) do + expect(data).to have_key(:object_attributes) + expect(data[:object_attributes]).to have_key(:url) + expect(data[:object_attributes][:url]).to eq(note_url) + expect(data[:object_kind]).to eq('note') + expect(data[:user]).to eq(user.hook_attrs) + end + + describe 'When asking for a note on commit' do + let(:note) { create(:note_on_commit) } + + it 'returns the note and commit-specific data' do + expect(data).to have_key(:commit) + end + end + + describe 'When asking for a note on commit diff' do + let(:note) { create(:note_on_commit_diff) } + + it 'returns the note and commit-specific data' do + expect(data).to have_key(:commit) + end + end + + describe 'When asking for a note on issue' do + let(:issue) { create(:issue, created_at: fixed_time, updated_at: fixed_time) } + let(:note) { create(:note_on_issue, noteable_id: issue.id) } + + it 'returns the note and issue-specific data' do + expect(data).to have_key(:issue) + expect(data[:issue]).to eq(issue.hook_attrs) + end + end + + describe 'When asking for a note on merge request' do + let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } + let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } + + it 'returns the note and merge request data' do + expect(data).to have_key(:merge_request) + expect(data[:merge_request]).to eq(merge_request.hook_attrs) + end + end + + describe 'When asking for a note on merge request diff' do + let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } + let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } + + it 'returns the note and merge request diff data' do + expect(data).to have_key(:merge_request) + expect(data[:merge_request]).to eq(merge_request.hook_attrs) + end + end + + describe 'When asking for a note on project snippet' do + let!(:snippet) { create(:project_snippet, created_at: fixed_time, updated_at: fixed_time) } + let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } + + it 'returns the note and project snippet data' do + expect(data).to have_key(:snippet) + expect(data[:snippet]).to eq(snippet.hook_attrs) + end + end +end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 94b2fd5508e..5153ed15af3 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -16,4 +16,62 @@ describe Gitlab::UrlBuilder do expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}" end end + + describe 'When asking for a note on commit' do + let(:note) { create(:note_on_commit) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end + + describe 'When asking for a note on commit diff' do + let(:note) { create(:note_on_commit_diff) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}" + end + end + + describe 'When asking for a note on issue' do + let(:issue) { create(:issue) } + let(:note) { create(:note_on_issue, noteable_id: issue.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}" + end + end + + describe 'When asking for a note on merge request' do + let(:merge_request) { create(:merge_request) } + let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + describe 'When asking for a note on merge request diff' do + let(:merge_request) { create(:merge_request) } + let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}" + end + end + + describe 'When asking for a note on project snippet' do + let(:snippet) { create(:project_snippet) } + let(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } + let(:url) { Gitlab::UrlBuilder.new(:note).build(note.id) } + + it 'returns the note url' do + expect(url).to eq "#{Settings.gitlab['url']}/#{snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}" + end + end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 804c3d5ddd9..95ce4f8e4aa 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -98,5 +98,91 @@ describe HipchatService do "
    please fix
    ") end end + + context "Note events" do + let(:user) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:snippet) { create(:project_snippet, project: project) } + let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } + let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") } + let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")} + let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") } + + it "should call Hipchat API for commit comment events" do + data = Gitlab::NoteDataBuilder.build(commit_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + commit_id = Commit.truncate_sha(data[:commit][:id]) + title = hipchat.send(:format_title, data[:commit][:message]) + + expect(message).to eq("#{user.username} commented on " \ + "commit #{commit_id} in " \ + "#{project_name}: " \ + "#{title}" \ + "
    a comment on a commit
    ") + end + + it "should call Hipchat API for merge request comment events" do + data = Gitlab::NoteDataBuilder.build(merge_request_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + merge_id = data[:merge_request]['iid'] + title = data[:merge_request]['title'] + + expect(message).to eq("#{user.username} commented on " \ + "merge request ##{merge_id} in " \ + "#{project_name}: " \ + "#{title}" \ + "
    merge request note
    ") + end + + it "should call Hipchat API for issue comment events" do + data = Gitlab::NoteDataBuilder.build(issue_note, user) + hipchat.execute(data) + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + issue_id = data[:issue]['iid'] + title = data[:issue]['title'] + + expect(message).to eq("#{user.username} commented on " \ + "issue ##{issue_id} in " \ + "#{project_name}: " \ + "#{title}" \ + "
    issue note
    ") + end + + it "should call Hipchat API for snippet comment events" do + data = Gitlab::NoteDataBuilder.build(snippet_note, user) + hipchat.execute(data) + + expect(WebMock).to have_requested(:post, api_url).once + + message = hipchat.send(:create_message, data) + + obj_attr = data[:object_attributes] + snippet_id = data[:snippet]['id'] + title = data[:snippet]['title'] + + expect(message).to eq("#{user.username} commented on " \ + "snippet ##{snippet_id} in " \ + "#{project_name}: " \ + "#{title}" \ + "
    snippet note
    ") + end + end end end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb new file mode 100644 index 00000000000..f2516c10008 --- /dev/null +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe SlackService::NoteMessage do + let(:color) { '#345' } + + before do + @args = { + user: { + name: 'Test User', + username: 'username', + avatar_url: 'http://fakeavatar' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + repository: { + name: 'project_name', + url: 'somewhere.com', + }, + object_attributes: { + id: 10, + note: 'comment on a commit', + url: 'url', + noteable_type: 'Commit' + } + } + end + + context 'commit notes' do + before do + @args[:object_attributes][:note] = 'comment on a commit' + @args[:object_attributes][:noteable_type] = 'Commit' + @args[:commit] = { + id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23', + message: "Added a commit message\ndetails\n123\n" + } + end + + it 'returns a message regarding notes on commits' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("username commented on " \ + " in : " \ + "*Added a commit message*") + expected_attachments = [ + { + text: "comment on a commit", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'merge request notes' do + before do + @args[:object_attributes][:note] = 'comment on a merge request' + @args[:object_attributes][:noteable_type] = 'MergeRequest' + @args[:merge_request] = { + id: 1, + iid: 30, + title: "merge request title\ndetails\n" + } + end + it 'returns a message regarding notes on a merge request' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("username commented on " \ + " in : " \ + "*merge request title*") + expected_attachments = [ + { + text: "comment on a merge request", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'issue notes' do + before do + @args[:object_attributes][:note] = 'comment on an issue' + @args[:object_attributes][:noteable_type] = 'Issue' + @args[:issue] = { + id: 1, + iid: 20, + title: "issue title\ndetails\n" + } + end + + it 'returns a message regarding notes on an issue' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq( + "username commented on " \ + " in : " \ + "*issue title*") + expected_attachments = [ + { + text: "comment on an issue", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'project snippet notes' do + before do + @args[:object_attributes][:note] = 'comment on a snippet' + @args[:object_attributes][:noteable_type] = 'Snippet' + @args[:snippet] = { + id: 5, + title: "snippet title\ndetails\n" + } + end + + it 'returns a message regarding notes on a project snippet' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("username commented on " \ + " in : " \ + "*snippet title*") + expected_attachments = [ + { + text: "comment on a snippet", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end +end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 4e8a96ec730..c36506644b3 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -76,16 +76,16 @@ describe SlackService do 'open') end - it "should call Slack API for pull requests" do + it "should call Slack API for push events" do slack.execute(push_sample_data) - WebMock.should have_requested(:post, webhook_url).once + expect(WebMock).to have_requested(:post, webhook_url).once end it "should call Slack API for issue events" do slack.execute(@issues_sample_data) - WebMock.should have_requested(:post, webhook_url).once + expect(WebMock).to have_requested(:post, webhook_url).once end it "should call Slack API for merge requests events" do @@ -97,10 +97,10 @@ describe SlackService do it 'should use the username as an option for slack when configured' do slack.stub(username: username) expect(Slack::Notifier).to receive(:new). - with(webhook_url, username: username). - and_return( - double(:slack_service).as_null_object - ) + with(webhook_url, username: username). + and_return( + double(:slack_service).as_null_object + ) slack.execute(push_sample_data) end @@ -114,4 +114,57 @@ describe SlackService do slack.execute(push_sample_data) end end + + describe "Note events" do + let(:slack) { SlackService.new } + let(:user) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + let(:issue) { create(:issue, project: project) } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:snippet) { create(:project_snippet, project: project) } + let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } + let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") } + let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")} + let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") } + let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' } + + before do + slack.stub( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + end + + it "should call Slack API for commit comment events" do + data = Gitlab::NoteDataBuilder.build(commit_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for merge request comment events" do + data = Gitlab::NoteDataBuilder.build(merge_request_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for issue comment events" do + data = Gitlab::NoteDataBuilder.build(issue_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + + it "should call Slack API for snippet comment events" do + data = Gitlab::NoteDataBuilder.build(snippet_note, user) + slack.execute(data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end + end end -- GitLab From ad14ed5e49a956511ae8f4d3bf377457fdb1075d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 6 Mar 2015 08:31:49 -0800 Subject: [PATCH 1207/1609] Add tag_push event notification to HipChat and Slack services. Normalize output to use: - User name instead of username - Include first line of title in message description - Link to "Issue #X" instead of "#X" --- CHANGELOG | 6 +-- .../project_services/hipchat_service.rb | 38 +++++++++------- app/models/project_services/slack_service.rb | 4 +- .../slack_service/issue_message.rb | 8 ++-- .../slack_service/merge_message.rb | 14 ++++-- .../slack_service/note_message.rb | 12 ++--- .../slack_service/push_message.rb | 19 +++++--- .../project_services/hipchat_service_spec.rb | 45 +++++++++++++++---- .../slack_service/issue_message_spec.rb | 11 ++--- .../slack_service/merge_message_spec.rb | 13 +++--- .../slack_service/note_message_spec.rb | 8 ++-- .../slack_service/push_message_spec.rb | 20 +++++++++ 12 files changed, 134 insertions(+), 64 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 611c6c77d54..c0d38a9abf0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,9 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) - - Added comment notification events to HipChat and Slack services (Stan Hu) - - Added issue and merge request events to HipChat and Slack services (Stan Hu) + - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) + - Add comment notification events to HipChat and Slack services (Stan Hu) + - Add issue and merge request events to HipChat and Slack services (Stan Hu) - Fix merge request URL passed to Webhooks. (Stan Hu) - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) - Move labels/milestones tabs to sidebar @@ -35,7 +36,6 @@ v 7.8.2 - Fix response of push to repository to return "Not found" if user doesn't have access - Fix check if user is allowed to view the file attachment - Fix import check for case sensetive namespaces - - Added issue and merge request events to Slack service (Stan Hu) - Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time. v 7.8.1 diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index d24351a7b13..90ba7e080f1 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -45,7 +45,7 @@ class HipchatService < Service end def supported_events - %w(push issue merge_request note) + %w(push issue merge_request note tag_push) end def execute(data) @@ -67,7 +67,7 @@ class HipchatService < Service message = \ case object_kind - when "push" + when "push", "tag_push" create_push_message(data) when "issue" create_issue_message(data) unless is_update?(data) @@ -79,21 +79,27 @@ class HipchatService < Service end def create_push_message(push) - ref = push[:ref].gsub("refs/heads/", "") + if push[:ref].starts_with?('refs/tags/') + ref_type = 'tag' + ref = push[:ref].gsub('refs/tags/', '') + else + ref_type = 'branch' + ref = push[:ref].gsub('refs/heads/', '') + end + before = push[:before] after = push[:after] message = "" message << "#{push[:user_name]} " if before.include?('000000') - message << "pushed new branch #{ref}"\ - " to "\ - "#{project_url}\n" + " to #{project_link}\n" elsif after.include?('000000') - message << "removed branch #{ref} from #{project.name_with_namespace.gsub!(/\s/,'')} \n" + message << "removed #{ref_type} #{ref} from #{project_name} \n" else - message << "pushed to branch #{ref} " message << "of #{project.name_with_namespace.gsub!(/\s/,'')} " message << "(Compare changes)" @@ -119,7 +125,7 @@ class HipchatService < Service end def create_issue_message(data) - username = data[:user][:username] + user_name = data[:user][:name] obj_attr = data[:object_attributes] obj_attr = HashWithIndifferentAccess.new(obj_attr) @@ -129,8 +135,8 @@ class HipchatService < Service issue_url = obj_attr[:url] description = obj_attr[:description] - issue_link = "##{issue_iid}" - message = "#{username} #{state} issue #{issue_link} in #{project_link}: #{title}" + issue_link = "issue ##{issue_iid}" + message = "#{user_name} #{state} #{issue_link} in #{project_link}: #{title}" if description description = format_body(description) @@ -141,7 +147,7 @@ class HipchatService < Service end def create_merge_request_message(data) - username = data[:user][:username] + user_name = data[:user][:name] obj_attr = data[:object_attributes] obj_attr = HashWithIndifferentAccess.new(obj_attr) @@ -153,8 +159,8 @@ class HipchatService < Service title = obj_attr[:title] merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" - merge_request_link = "##{merge_request_id}" - message = "#{username} #{state} merge request #{merge_request_link} in " \ + merge_request_link = "merge request ##{merge_request_id}" + message = "#{user_name} #{state} #{merge_request_link} in " \ "#{project_link}: #{title}" if description @@ -171,7 +177,7 @@ class HipchatService < Service def create_note_message(data) data = HashWithIndifferentAccess.new(data) - username = data[:user][:username] + user_name = data[:user][:name] repo_attr = HashWithIndifferentAccess.new(data[:repository]) @@ -208,7 +214,7 @@ class HipchatService < Service end subject_html = "#{subject_type} #{subject_desc}" - message = "#{username} commented on #{subject_html} in #{project_link}: " + message = "#{user_name} commented on #{subject_html} in #{project_link}: " message << title if note diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index a58840116f4..36d9874edd3 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -44,7 +44,7 @@ class SlackService < Service end def supported_events - %w(push issue merge_request note) + %w(push issue merge_request note tag_push) end def execute(data) @@ -64,7 +64,7 @@ class SlackService < Service message = \ case object_kind - when "push" + when "push", "tag_push" PushMessage.new(data) when "issue" IssueMessage.new(data) unless is_update?(data) diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb index e2fed0bb1b9..5af24a80609 100644 --- a/app/models/project_services/slack_service/issue_message.rb +++ b/app/models/project_services/slack_service/issue_message.rb @@ -1,6 +1,6 @@ class SlackService class IssueMessage < BaseMessage - attr_reader :username + attr_reader :user_name attr_reader :title attr_reader :project_name attr_reader :project_url @@ -11,7 +11,7 @@ class SlackService attr_reader :description def initialize(params) - @username = params[:user][:username] + @user_name = params[:user][:name] @project_name = params[:project_name] @project_url = params[:project_url] @@ -34,7 +34,7 @@ class SlackService private def message - "#{username} #{state} issue #{issue_link} in #{project_link}: #{title}" + "#{user_name} #{state} #{issue_link} in #{project_link}: *#{title}*" end def opened_issue? @@ -50,7 +50,7 @@ class SlackService end def issue_link - "[##{issue_iid}](#{issue_url})" + "[issue ##{issue_iid}](#{issue_url})" end end end diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb index 4dcce1d15a0..e792c258f73 100644 --- a/app/models/project_services/slack_service/merge_message.rb +++ b/app/models/project_services/slack_service/merge_message.rb @@ -1,15 +1,16 @@ class SlackService class MergeMessage < BaseMessage - attr_reader :username + attr_reader :user_name attr_reader :project_name attr_reader :project_url attr_reader :merge_request_id attr_reader :source_branch attr_reader :target_branch attr_reader :state + attr_reader :title def initialize(params) - @username = params[:user][:username] + @user_name = params[:user][:name] @project_name = params[:project_name] @project_url = params[:project_url] @@ -19,6 +20,7 @@ class SlackService @source_branch = obj_attr[:source_branch] @target_branch = obj_attr[:target_branch] @state = obj_attr[:state] + @title = format_title(obj_attr[:title]) end def pretext @@ -31,6 +33,10 @@ class SlackService private + def format_title(title) + '*' + title.lines.first.chomp + '*' + end + def message merge_request_message end @@ -40,11 +46,11 @@ class SlackService end def merge_request_message - "#{username} #{state} merge request #{merge_request_link} in #{project_link}" + "#{user_name} #{state} #{merge_request_link} in #{project_link}: #{title}" end def merge_request_link - "[##{merge_request_id}](#{merge_request_url})" + "[merge request ##{merge_request_id}](#{merge_request_url})" end def merge_request_url diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb index f93dc358f61..074478b292d 100644 --- a/app/models/project_services/slack_service/note_message.rb +++ b/app/models/project_services/slack_service/note_message.rb @@ -1,7 +1,7 @@ class SlackService class NoteMessage < BaseMessage attr_reader :message - attr_reader :username + attr_reader :user_name attr_reader :project_name attr_reader :project_link attr_reader :note @@ -10,7 +10,7 @@ class SlackService def initialize(params) params = HashWithIndifferentAccess.new(params) - @username = params[:user][:username] + @user_name = params[:user][:name] @project_name = params[:project_name] @project_url = params[:project_url] @@ -47,28 +47,28 @@ class SlackService commit_sha = Commit.truncate_sha(commit_sha) commit_link = "[commit #{commit_sha}](#{@note_url})" title = format_title(commit[:message]) - @message = "#{@username} commented on #{commit_link} in #{project_link}: *#{title}*" + @message = "#{@user_name} commented on #{commit_link} in #{project_link}: *#{title}*" end def create_issue_note(issue) issue_iid = issue[:iid] note_link = "[issue ##{issue_iid}](#{@note_url})" title = format_title(issue[:title]) - @message = "#{@username} commented on #{note_link} in #{project_link}: *#{title}*" + @message = "#{@user_name} commented on #{note_link} in #{project_link}: *#{title}*" end def create_merge_note(merge_request) merge_request_id = merge_request[:iid] merge_request_link = "[merge request ##{merge_request_id}](#{@note_url})" title = format_title(merge_request[:title]) - @message = "#{@username} commented on #{merge_request_link} in #{project_link}: *#{title}*" + @message = "#{@user_name} commented on #{merge_request_link} in #{project_link}: *#{title}*" end def create_snippet_note(snippet) snippet_id = snippet[:id] snippet_link = "[snippet ##{snippet_id}](#{@note_url})" title = format_title(snippet[:title]) - @message = "#{@username} commented on #{snippet_link} in #{project_link}: *#{title}*" + @message = "#{@user_name} commented on #{snippet_link} in #{project_link}: *#{title}*" end def description_message diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/slack_service/push_message.rb index 2e566bc317b..3dc2df04764 100644 --- a/app/models/project_services/slack_service/push_message.rb +++ b/app/models/project_services/slack_service/push_message.rb @@ -6,7 +6,8 @@ class SlackService attr_reader :project_name attr_reader :project_url attr_reader :ref - attr_reader :username + attr_reader :ref_type + attr_reader :user_name def initialize(params) @after = params[:after] @@ -14,8 +15,14 @@ class SlackService @commits = params.fetch(:commits, []) @project_name = params[:project_name] @project_url = params[:project_url] - @ref = params[:ref].gsub('refs/heads/', '') - @username = params[:user_name] + if params[:ref].starts_with?('refs/tags/') + @ref_type = 'tag' + @ref = params[:ref].gsub('refs/tags/', '') + else + @ref_type = 'branch' + @ref = params[:ref].gsub('refs/heads/', '') + end + @user_name = params[:user_name] end def pretext @@ -45,15 +52,15 @@ class SlackService end def new_branch_message - "#{username} pushed new branch #{branch_link} to #{project_link}" + "#{user_name} pushed new #{ref_type} #{branch_link} to #{project_link}" end def removed_branch_message - "#{username} removed branch #{ref} from #{project_link}" + "#{user_name} removed #{ref_type} #{ref} from #{project_link}" end def push_message - "#{username} pushed to branch #{branch_link} of #{project_link} (#{compare_link})" + "#{user_name} pushed to #{ref_type} #{branch_link} of #{project_link} (#{compare_link})" end def commit_messages diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 95ce4f8e4aa..b9f2bee148d 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -50,6 +50,35 @@ describe HipchatService do expect(WebMock).to have_requested(:post, api_url).once end + + it "should create a push message" do + message = hipchat.send(:create_push_message, push_sample_data) + + obj_attr = push_sample_data[:object_attributes] + branch = push_sample_data[:ref].gsub('refs/heads/', '') + expect(message).to include("#{user.name} pushed to branch " \ + "#{branch} of " \ + "#{project_name}") + end + end + + context 'tag_push events' do + let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, '000000', '111111', 'refs/tags/test', []) } + + it "should call Hipchat API for tag push events" do + hipchat.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, api_url).once + end + + it "should create a tag push message" do + message = hipchat.send(:create_push_message, push_sample_data) + + obj_attr = push_sample_data[:object_attributes] + expect(message).to eq("#{user.name} pushed new tag " \ + "test to " \ + "#{project_name}\n") + end end context 'issue events' do @@ -67,8 +96,8 @@ describe HipchatService do message = hipchat.send(:create_issue_message, issues_sample_data) obj_attr = issues_sample_data[:object_attributes] - expect(message).to eq("#{user.username} opened issue " \ - "##{obj_attr["iid"]} in " \ + expect(message).to eq("#{user.name} opened " \ + "issue ##{obj_attr["iid"]} in " \ "#{project_name}: " \ "Awesome issue" \ "
    please fix
    ") @@ -91,8 +120,8 @@ describe HipchatService do merge_sample_data) obj_attr = merge_sample_data[:object_attributes] - expect(message).to eq("#{user.username} opened merge request " \ - "##{obj_attr["iid"]} in " \ + expect(message).to eq("#{user.name} opened " \ + "merge request ##{obj_attr["iid"]} in " \ "#{project_name}: " \ "Awesome merge request" \ "
    please fix
    ") @@ -122,7 +151,7 @@ describe HipchatService do commit_id = Commit.truncate_sha(data[:commit][:id]) title = hipchat.send(:format_title, data[:commit][:message]) - expect(message).to eq("#{user.username} commented on " \ + expect(message).to eq("#{user.name} commented on " \ "commit #{commit_id} in " \ "#{project_name}: " \ "#{title}" \ @@ -141,7 +170,7 @@ describe HipchatService do merge_id = data[:merge_request]['iid'] title = data[:merge_request]['title'] - expect(message).to eq("#{user.username} commented on " \ + expect(message).to eq("#{user.name} commented on " \ "merge request ##{merge_id} in " \ "#{project_name}: " \ "#{title}" \ @@ -158,7 +187,7 @@ describe HipchatService do issue_id = data[:issue]['iid'] title = data[:issue]['title'] - expect(message).to eq("#{user.username} commented on " \ + expect(message).to eq("#{user.name} commented on " \ "issue ##{issue_id} in " \ "#{project_name}: " \ "#{title}" \ @@ -177,7 +206,7 @@ describe HipchatService do snippet_id = data[:snippet]['id'] title = data[:snippet]['title'] - expect(message).to eq("#{user.username} commented on " \ + expect(message).to eq("#{user.name} commented on " \ "snippet ##{snippet_id} in " \ "#{project_name}: " \ "#{title}" \ diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb index a23a7cc068e..8bca1fef44c 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/slack_service/issue_message_spec.rb @@ -6,7 +6,8 @@ describe SlackService::IssueMessage do let(:args) { { user: { - username: 'username' + name: 'Test User', + username: 'Test User' }, project_name: 'project_name', project_url: 'somewhere.com', @@ -29,8 +30,8 @@ describe SlackService::IssueMessage do context 'open' do it 'returns a message regarding opening of issues' do expect(subject.pretext).to eq( - 'username opened issue in : '\ - 'Issue title') + 'Test User opened in : '\ + '*Issue title*') expect(subject.attachments).to eq([ { text: "issue description", @@ -47,8 +48,8 @@ describe SlackService::IssueMessage do end it 'returns a message regarding closing of issues' do expect(subject.pretext). to eq( - 'username closed issue in : '\ - 'Issue title') + 'Test User closed in : '\ + '*Issue title*') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb index 25d03cd8736..aeb408aa766 100644 --- a/spec/models/project_services/slack_service/merge_message_spec.rb +++ b/spec/models/project_services/slack_service/merge_message_spec.rb @@ -6,13 +6,14 @@ describe SlackService::MergeMessage do let(:args) { { user: { - username: 'username' + name: 'Test User', + username: 'Test User' }, project_name: 'project_name', project_url: 'somewhere.com', object_attributes: { - title: 'Issue title', + title: "Issue title\nSecond line", id: 10, iid: 100, assignee_id: 1, @@ -30,8 +31,8 @@ describe SlackService::MergeMessage do context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'username opened merge request '\ - 'in ') + 'Test User opened '\ + 'in : *Issue title*') expect(subject.attachments).to be_empty end end @@ -42,8 +43,8 @@ describe SlackService::MergeMessage do end it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'username closed merge request '\ - 'in ') + 'Test User closed '\ + 'in : *Issue title*') expect(subject.attachments).to be_empty end end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index f2516c10008..21fb575480b 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -37,7 +37,7 @@ describe SlackService::NoteMessage do it 'returns a message regarding notes on commits' do message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("username commented on " \ + expect(message.pretext).to eq("Test User commented on " \ " in : " \ "*Added a commit message*") expected_attachments = [ @@ -62,7 +62,7 @@ describe SlackService::NoteMessage do end it 'returns a message regarding notes on a merge request' do message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("username commented on " \ + expect(message.pretext).to eq("Test User commented on " \ " in : " \ "*merge request title*") expected_attachments = [ @@ -89,7 +89,7 @@ describe SlackService::NoteMessage do it 'returns a message regarding notes on an issue' do message = SlackService::NoteMessage.new(@args) expect(message.pretext).to eq( - "username commented on " \ + "Test User commented on " \ " in : " \ "*issue title*") expected_attachments = [ @@ -114,7 +114,7 @@ describe SlackService::NoteMessage do it 'returns a message regarding notes on a project snippet' do message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("username commented on " \ + expect(message.pretext).to eq("Test User commented on " \ " in : " \ "*snippet title*") expected_attachments = [ diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb index ef0e7a6ee30..3ef065459d8 100644 --- a/spec/models/project_services/slack_service/push_message_spec.rb +++ b/spec/models/project_services/slack_service/push_message_spec.rb @@ -39,6 +39,26 @@ describe SlackService::PushMessage do end end + context 'tag push' do + let(:args) { + { + after: 'after', + before: '000000', + project_name: 'project_name', + ref: 'refs/tags/new_tag', + user_name: 'user_name', + project_url: 'url' + } + } + + it 'returns a message regarding pushes' do + expect(subject.pretext).to eq('user_name pushed new tag ' \ + ' to ' \ + '') + expect(subject.attachments).to be_empty + end + end + context 'new branch' do before do args[:before] = '000000' -- GitLab From 663b3c968f73f8ffebf32059fed86192ecbee5d8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 17:14:19 +0100 Subject: [PATCH 1208/1609] Condense commits already in target branch when updating merge request source branch. --- CHANGELOG | 1 + app/models/note.rb | 31 ++++++++++++++++--- .../merge_requests/refresh_service.rb | 8 ++++- .../merge_requests/refresh_service_spec.rb | 2 +- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b927b60140e..2cea709f167 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 7.9.0 (unreleased) - Add Bitbucket omniauth provider. - Add Bitbucket importer. - Support referencing issues to a project whose name starts with a digit + - Condense commits already in target branch when updating merge request source branch. v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/models/note.rb b/app/models/note.rb index e6c258ffbe9..e79b7a88344 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -151,18 +151,41 @@ class Note < ActiveRecord::Base ) end - def create_new_commits_note(noteable, project, author, commits) - commits_text = ActionController::Base.helpers.pluralize(commits.size, 'new commit') + def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = []) + total_count = new_commits.length + existing_commits.length + commits_text = ActionController::Base.helpers.pluralize(total_count, 'commit') body = "Added #{commits_text}:\n\n" - commits.each do |commit| + if existing_commits.length > 0 + commit_ids = + if existing_commits.length == 1 + existing_commits.first.short_id + else + "#{existing_commits.first.short_id}...#{existing_commits.last.short_id}" + end + + commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit') + + branch = + if merge_request.for_fork? + "#{merge_request.target_project_namespace}:#{merge_request.target_branch}" + else + merge_request.target_branch + end + + message = "* #{commit_ids} - _#{commits_text} from branch `#{branch}`_" + body << message + body << "\n" + end + + new_commits.each do |commit| message = "* #{commit.short_id} - #{commit.title}" body << message body << "\n" end create( - noteable: noteable, + noteable: merge_request, project: project, author: author, note: body, diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 96761bec99f..ea846472766 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -82,8 +82,14 @@ module MergeRequests merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| + mr_commit_ids = Set.new(merge_request.commits.map(&:id)) + + new_commits, existing_commits = @commits.partition do |commit| + mr_commit_ids.include?(commit.id) + end + Note.create_new_commits_note(merge_request, merge_request.project, - @current_user, @commits) + @current_user, new_commits, existing_commits) end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 2830da87814..879df0c9c67 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -61,7 +61,7 @@ describe MergeRequests::RefreshService do it { expect(@merge_request.notes).to be_empty } it { expect(@merge_request).to be_open } - it { expect(@fork_merge_request.notes.last.note).to include('new commit') } + it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') } it { expect(@fork_merge_request).to be_open } end -- GitLab From 9f089ac48c22b2f7cfbc7dd0ca29da924c566363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 6 Mar 2015 19:49:38 +0100 Subject: [PATCH 1209/1609] use constant-time string compare for internal api authentication Ruby str_equal uses memcmp internally to compare String. Memcmp is vunerable to timing attacks because it returns early on mismatch (on most x32 platforms memcmp uses a bytewise comparision). Devise.secure_compare implements a constant time comparision instead. --- lib/api/helpers.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 228a719fbdf..ee678d84c84 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -83,7 +83,10 @@ module API end def authenticate_by_gitlab_shell_token! - unauthorized! unless secret_token == params['secret_token'].try(:chomp) + input = params['secret_token'].try(:chomp) + unless Devise.secure_compare(secret_token, input) + unauthorized! + end end def authenticated_as_admin! -- GitLab From 84ebc22ad2d9296d23b442948dd3435b33d36cbe Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 23:12:26 +0100 Subject: [PATCH 1210/1609] Use 2 periods instead of 3 to signify inclusive range. --- app/models/note.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/note.rb b/app/models/note.rb index e79b7a88344..43981a0044d 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -161,7 +161,7 @@ class Note < ActiveRecord::Base if existing_commits.length == 1 existing_commits.first.short_id else - "#{existing_commits.first.short_id}...#{existing_commits.last.short_id}" + "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" end commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit') -- GitLab From 4dddaef8661c8bfb5127d5db12b91d18cfcf0b8f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 23:08:28 +0100 Subject: [PATCH 1211/1609] Automatically link commit ranges to compare page. --- CHANGELOG | 1 + lib/gitlab/markdown.rb | 28 +++++++++++- lib/gitlab/reference_extractor.rb | 16 +++++-- spec/helpers/gitlab_markdown_helper_spec.rb | 48 +++++++++++++++++++++ spec/lib/gitlab/reference_extractor_spec.rb | 19 ++++++++ 5 files changed, 108 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 611c6c77d54..06eb3c1c2c8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 7.9.0 (unreleased) - Add Bitbucket omniauth provider. - Add Bitbucket importer. - Support referencing issues to a project whose name starts with a digit + - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index d85c2ee4f2d..2dfa18da482 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -14,6 +14,7 @@ module Gitlab # * !123 for merge requests # * $123 for snippets # * 123456 for commits + # * 123456...7890123 for commit ranges (comparisons) # # It also parses Emoji codes to insert images. See # http://www.emoji-cheat-sheet.com/ for a list of the supported icons. @@ -133,13 +134,14 @@ module Gitlab |#{PROJ_STR}?\#(?([a-zA-Z\-]+-)?\d+) # Issue ID |#{PROJ_STR}?!(?\d+) # MR ID |\$(?\d+) # Snippet ID + |(#{PROJ_STR}@)?(?[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range |(#{PROJ_STR}@)?(?[\h]{6,40}) # Commit ID |(?gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit ) (?\W)? # Suffix }x.freeze - TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit].freeze + TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit, :commit_range].freeze def parse_references(text, project = @project) # parse reference links @@ -290,6 +292,30 @@ module Gitlab end end + def reference_commit_range(identifier, project = @project, prefix_text = nil) + from_id, to_id = identifier.split(/\.{2,3}/, 2) + + inclusive = identifier !~ /\.{3}/ + from_id << "^" if inclusive + + if project.valid_repo? && + from = project.repository.commit(from_id) && + to = project.repository.commit(to_id) + + options = html_options.merge( + title: "Commits #{from_id} through #{to_id}", + class: "gfm gfm-commit_range #{html_options[:class]}" + ) + prefix_text = "#{prefix_text}@" if prefix_text + + link_to( + "#{prefix_text}#{identifier}", + namespace_project_compare_url(project.namespace, project, from: from_id, to: to_id), + options + ) + end + end + def reference_external_issue(identifier, project = @project, prefix_text = nil) url = url_for_issue(identifier, project) diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 7e5c991a222..5b9772de168 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,13 +1,13 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor - attr_accessor :users, :labels, :issues, :merge_requests, :snippets, :commits + attr_accessor :users, :labels, :issues, :merge_requests, :snippets, :commits, :commit_ranges include Markdown def initialize - @users, @labels, @issues, @merge_requests, @snippets, @commits = - [], [], [], [], [], [] + @users, @labels, @issues, @merge_requests, @snippets, @commits, @commit_ranges = + [], [], [], [], [], [], [] end def analyze(string, project) @@ -60,6 +60,16 @@ module Gitlab end.reject(&:nil?) end + def commit_ranges_for(project = nil) + commit_ranges.map do |entry| + repo = entry[:project].repository if entry[:project] + if repo && should_lookup?(project, entry[:project]) + from_id, to_id = entry[:id].split(/\.{2,3}/, 2) + [repo.commit(from_id), repo.commit(to_id)] + end + end.reject(&:nil?) + end + private def reference_link(type, identifier, project, _) diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 76fcf888a6a..74a42932fe8 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -9,6 +9,7 @@ describe GitlabMarkdownHelper do let(:user) { create(:user, username: 'gfm') } let(:commit) { project.repository.commit } + let(:earlier_commit){ project.repository.commit("HEAD~2") } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:snippet) { create(:project_snippet, project: project) } @@ -53,6 +54,53 @@ describe GitlabMarkdownHelper do to have_selector('a.gfm.foo') end + describe "referencing a commit range" do + let(:expected) { namespace_project_compare_path(project.namespace, project, from: earlier_commit.id, to: commit.id) } + + it "should link using a full id" do + actual = "What happened in #{earlier_commit.id}...#{commit.id}" + expect(gfm(actual)).to match(expected) + end + + it "should link using a short id" do + actual = "What happened in #{earlier_commit.short_id}...#{commit.short_id}" + expected = namespace_project_compare_path(project.namespace, project, from: earlier_commit.short_id, to: commit.short_id) + expect(gfm(actual)).to match(expected) + end + + it "should link inclusively" do + actual = "What happened in #{earlier_commit.id}..#{commit.id}" + expected = namespace_project_compare_path(project.namespace, project, from: "#{earlier_commit.id}^", to: commit.id) + expect(gfm(actual)).to match(expected) + end + + it "should link with adjacent text" do + actual = "(see #{earlier_commit.id}...#{commit.id})" + expect(gfm(actual)).to match(expected) + end + + it "should keep whitespace intact" do + actual = "Changes #{earlier_commit.id}...#{commit.id} dramatically" + expected = /Changes #{earlier_commit.id}...#{commit.id}<\/a> dramatically/ + expect(gfm(actual)).to match(expected) + end + + it "should not link with an invalid id" do + actual = expected = "What happened in #{earlier_commit.id.reverse}...#{commit.id.reverse}" + expect(gfm(actual)).to eq(expected) + end + + it "should include a title attribute" do + actual = "What happened in #{earlier_commit.id}...#{commit.id}" + expect(gfm(actual)).to match(/title="Commits #{earlier_commit.id} through #{commit.id}"/) + end + + it "should include standard gfm classes" do + actual = "What happened in #{earlier_commit.id}...#{commit.id}" + expect(gfm(actual)).to match(/class="\s?gfm gfm-commit_range\s?"/) + end + end + describe "referencing a commit" do let(:expected) { namespace_project_commit_path(project.namespace, project, commit) } diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 0847c31258c..034f8ee7c45 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -31,6 +31,11 @@ describe Gitlab::ReferenceExtractor do expect(subject.commits).to eq([{ project: nil, id: '98cf0ae3' }]) end + it 'extracts commit ranges' do + subject.analyze('here you go, a commit range: 98cf0ae3...98cf0ae4', nil) + expect(subject.commit_ranges).to eq([{ project: nil, id: '98cf0ae3...98cf0ae4' }]) + end + it 'extracts multiple references and preserves their order' do subject.analyze('@me and @you both care about this', nil) expect(subject.users).to eq([ @@ -100,5 +105,19 @@ describe Gitlab::ReferenceExtractor do expect(extracted[0].sha).to eq(commit.sha) expect(extracted[0].message).to eq(commit.message) end + + it 'accesses valid commit ranges' do + commit = project.repository.commit('master') + earlier_commit = project.repository.commit('master~2') + + subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}", + project) + extracted = subject.commit_ranges_for(project) + expect(extracted.size).to eq(1) + expect(extracted[0][0].sha).to eq(earlier_commit.sha) + expect(extracted[0][0].message).to eq(earlier_commit.message) + expect(extracted[0][1].sha).to eq(commit.sha) + expect(extracted[0][1].message).to eq(commit.message) + end end end -- GitLab From 3bfd53149c2791b1598f3fc14e37cd33b7aef2d5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Mar 2015 18:36:22 -0800 Subject: [PATCH 1212/1609] Replace bs-callout with alert --- app/assets/stylesheets/gl_bootstrap.scss | 46 ------------------- app/views/admin/groups/_form.html.haml | 2 +- app/views/admin/services/_form.html.haml | 2 +- app/views/profiles/show.html.haml | 2 +- app/views/projects/blob/_blob.html.haml | 2 +- app/views/projects/diffs/_warning.html.haml | 2 +- app/views/projects/imports/new.html.haml | 2 +- app/views/projects/labels/_form.html.haml | 2 +- .../merge_requests/_new_submit.html.haml | 4 +- .../merge_requests/show/_diffs.html.haml | 2 +- app/views/projects/new.html.haml | 2 +- .../protected_branches/index.html.haml | 2 +- app/views/projects/services/_form.html.haml | 2 +- app/views/shared/_group_form.html.haml | 2 +- 14 files changed, 14 insertions(+), 60 deletions(-) diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/gl_bootstrap.scss index 34ddf6f8717..2f07f7202b9 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/gl_bootstrap.scss @@ -161,52 +161,6 @@ $brand-danger: $bg_danger; font-size: 12px; } - -/* - * Callouts from Bootstrap3 docs - * - * Not quite alerts, but custom and helpful notes for folks reading the docs. - * Requires a base and modifier class. - */ - -/* Common styles for all types */ -.bs-callout { - margin: 20px 0; - padding: 20px; - border-left: 3px solid #eee; - color: #666; - background: #f9f9f9; -} -.bs-callout h4 { - margin-top: 0; - margin-bottom: 5px; -} -.bs-callout p:last-child { - margin-bottom: 0; -} - -/* Variations */ -.bs-callout-danger { - background-color: #fdf7f7; - border-color: #eed3d7; - color: #b94a48; -} -.bs-callout-warning { - background-color: #faf8f0; - border-color: #faebcc; - color: #8a6d3b; -} -.bs-callout-info { - background-color: #f4f8fa; - border-color: #bce8f1; - color: #34789a; -} -.bs-callout-success { - background-color: #dff0d8; - border-color: #5cA64d; - color: #3c763d; -} - /** * fix to keep tooltips position in top navigation bar * diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 86a73200609..9e7751830a4 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -14,7 +14,7 @@ .form-group .col-sm-2 .col-sm-10 - .bs-callout.bs-callout-info + .alert.alert-info = render 'shared/group_tips' .form-actions = f.submit 'Create group', class: "btn btn-create" diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 62f4001ca66..4ddddbd46ea 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -10,7 +10,7 @@ - @service.errors.full_messages.each do |msg| %p= msg - if @service.help.present? - .bs-callout + .alert.alert-info = preserve do = markdown @service.help diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index b2808c46c00..459361a0d5b 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -89,7 +89,7 @@ = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" - if @user.public_profile? - .bs-callout.bs-callout-info + .alert.alert-info %h4 Public profile %p Your profile is publicly visible because you joined public project(s) diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 64cc3fad6cf..05b1f5e841c 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -15,7 +15,7 @@ - else = link_to title, '#' -%ul.blob-commit-info.bs-callout.bs-callout-info.hidden-xs +%ul.blob-commit-info.alert.alert-info.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index c9a6b3ebd9e..af1f342afbd 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -1,4 +1,4 @@ -.bs-callout.bs-callout-warning +.alert.alert-warning %h4 Too many changes. .pull-right diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 097374e1128..f1248ac2af5 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -12,7 +12,7 @@ %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .bs-callout.bs-callout-info + .alert.alert-info This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 95912536e42..2305fce112e 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -2,7 +2,7 @@ -if @label.errors.any? .row .col-sm-10.col-sm-offset-2 - .bs-callout.bs-callout-danger + .alert.alert-danger - @label.errors.full_messages.each do |msg| %span= msg %br diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 73eccfa556e..bf80afe8785 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -99,11 +99,11 @@ - if @diffs.present? = render "projects/diffs/diffs", diffs: @diffs, project: @project - elsif @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE - .bs-callout.bs-callout-danger + .alert.alert-danger %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. %p To preserve performance the line changes are not shown. - else - .bs-callout.bs-callout-danger + .alert.alert-danger %h4 This comparison includes a huge diff. %p To preserve performance the line changes are not shown. diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index cfef1d5e4cc..786b5f39063 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -3,7 +3,7 @@ - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else - .bs-callout.bs-callout-warning + .alert.alert-warning %h4 Changes view for this comparison is extremely large. %p diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 025c4fd5506..5daf8470d88 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -34,7 +34,7 @@ %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .bs-callout.bs-callout-info + .alert.alert-info This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index dc20e96732e..cfe28084170 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -2,7 +2,7 @@ %p.light Keep stable branches secure and force developers to use Merge Requests %hr -.bs-callout.bs-callout-info +.alert.alert-info %p Protected branches are designed to %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index defcdbe268e..8afae91d757 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -18,7 +18,7 @@ %li= msg - if @service.help.present? - .bs-callout + .alert.alert-info = preserve do = markdown @service.help diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index 5875f71bac2..b34dd53e3b5 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -15,7 +15,7 @@ = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false - if @group.persisted? - .bs-callout.bs-callout-danger + .alert.alert-danger %ul %li Changing group path can have unintended side effects. %li Renaming group path will rename directory for all related projects -- GitLab From cd73b26e0732ab3ce979e971e6ceeb0c2417b5c3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Mar 2015 19:02:12 -0800 Subject: [PATCH 1213/1609] Refactor gitlab css state colors --- app/assets/stylesheets/generic/buttons.scss | 8 ++-- app/assets/stylesheets/generic/common.scss | 2 +- app/assets/stylesheets/generic/issue_box.scss | 6 +-- app/assets/stylesheets/generic/jquery.scss | 4 +- app/assets/stylesheets/generic/selects.scss | 2 +- app/assets/stylesheets/gl_bootstrap.scss | 44 ++++++++++++------- app/assets/stylesheets/main/variables.scss | 27 +++--------- .../stylesheets/sections/dashboard.scss | 8 ++-- .../stylesheets/sections/merge_requests.scss | 16 +++---- app/assets/stylesheets/sections/notes.scss | 2 +- .../stylesheets/sections/notifications.scss | 6 +-- app/assets/stylesheets/sections/projects.scss | 6 +-- app/assets/stylesheets/sections/tree.scss | 2 +- 13 files changed, 65 insertions(+), 68 deletions(-) diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index d106e3b201e..7cc9782f539 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -41,16 +41,16 @@ } &.btn-close { - color: $bg_danger; - border-color: $border_danger; + color: $gl-danger; + border-color: $gl-danger; &:hover { color: #B94A48; } } &.btn-reopen { - color: $bg_success; - border-color: $border_success; + color: $gl-success; + border-color: $gl-success; &:hover { color: #468847; } diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 3db821fdf76..ca01e207207 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -61,7 +61,7 @@ pre { .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { - background: $bg_primary; + background: $gl-primary; color: #FFF } diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/generic/issue_box.scss index 2563ab516e2..9558f241b7c 100644 --- a/app/assets/stylesheets/generic/issue_box.scss +++ b/app/assets/stylesheets/generic/issue_box.scss @@ -11,17 +11,17 @@ margin-right: 5px; &.issue-box-closed { - background-color: $bg_danger; + background-color: $gl-danger; color: #FFF; } &.issue-box-merged { - background-color: $bg_primary; + background-color: $gl-primary; color: #FFF; } &.issue-box-open { - background-color: $bg_success; + background-color: $gl-success; color: #FFF; } diff --git a/app/assets/stylesheets/generic/jquery.scss b/app/assets/stylesheets/generic/jquery.scss index bfbbc7d25e3..871b808bad4 100644 --- a/app/assets/stylesheets/generic/jquery.scss +++ b/app/assets/stylesheets/generic/jquery.scss @@ -41,8 +41,8 @@ } .ui-state-active { - border: 1px solid $bg_primary; - background: $bg_primary; + border: 1px solid $gl-primary; + background: $gl-primary; color: #FFF; } diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index d85e80a512b..2a2f9e8eb5a 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -42,7 +42,7 @@ .select2-results { max-height: 350px; .select2-highlighted { - background: $bg_primary; + background: $gl-primary; } } } diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/gl_bootstrap.scss index 2f07f7202b9..1c29fdac37b 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/gl_bootstrap.scss @@ -4,15 +4,27 @@ */ $font-size-base: 13px !default; -$nav-pills-active-link-hover-bg: $bg_primary; -$pagination-active-bg: $bg_primary; -$list-group-active-bg: $bg_primary; +$nav-pills-active-link-hover-bg: $gl-primary; +$pagination-active-bg: $gl-primary; +$list-group-active-bg: $gl-primary; -$brand-primary: $bg_primary; -$brand-success: $bg_success; -$brand-info: #029ACF; -$brand-warning: $bg_warning; -$brand-danger: $bg_danger; +$brand-primary: $gl-primary; +$brand-success: $gl-success; +$brand-info: #029ACF; +$brand-warning: $gl-warning; +$brand-danger: $gl-danger; + +$state-primary-bg: lighten($gl-primary, 30%); +$state-success-bg: lighten($gl-success, 10%); +$state-info-bg: lighten($gl-info, 30%); +$state-warning-bg: lighten($gl-warning, 30%); +$state-danger-bg: lighten($gl-danger, 30%); + +$state-primary-txt: $gl-primary; +$state-success-txt: $gl-success; +$state-info-txt: $gl-info; +$state-warning-txt: $gl-warning; +$state-danger-txt: $gl-danger; // Core variables and mixins @import "bootstrap/variables"; @@ -226,31 +238,31 @@ $brand-danger: $bg_danger; .panel-danger { @include panel-colored; .panel-heading { - color: $border_danger; - border-color: $border_danger; + color: $gl-danger; + border-color: $gl-danger; } } .panel-success { @include panel-colored; .panel-heading { - color: $border_success; - border-color: $border_success; + color: $gl-success; + border-color: $gl-success; } } .panel-primary { @include panel-colored; .panel-heading { - color: $border_primary; - border-color: $border_primary; + color: $gl-primary; + border-color: $gl-primary; } } .panel-warning { @include panel-colored; .panel-heading { - color: $border_warning; - border-color: $border_warning; + color: $gl-warning; + border-color: $gl-warning; } } diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss index acbf5be94a3..57b881c0736 100644 --- a/app/assets/stylesheets/main/variables.scss +++ b/app/assets/stylesheets/main/variables.scss @@ -14,28 +14,13 @@ $link_hover_color: darken($link-color, 10%); $btn-border: 1px solid #ccc; /* - * Success colors (green) + * State colors: */ -$border_success: #019875; -$bg_success: #019875; - -/* - * Danger colors (red) - */ -$border_danger: #d43f3a; -$bg_danger: #d9534f; - -/* - * Primary colors (blue) - */ -$border_primary: #446e9b; -$bg_primary: #446e9b; - -/* - * Warning colors (yellow) - */ -$bg_warning: #EB9532; -$border_warning: #EB9532; +$gl-success: #019875; +$gl-danger: #d9534f; +$gl-primary: #446e9b; +$gl-info: #029ACF; +$gl-warning: #EB9532; /** * Commit Diff Colors diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index d8fd83d44b7..c8e3c7d4a2a 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -111,8 +111,8 @@ } .dash-new-project { - background: $bg_success; - border: 1px solid $border_success; + background: $gl-success; + border: 1px solid $gl-success; a { color: #FFF; @@ -120,8 +120,8 @@ } .dash-new-group { - background: $bg_success; - border: 1px solid $border_success; + background: $gl-success; + border: 1px solid $gl-success; a { color: #FFF; diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 0d2d8b0173e..01f6a705224 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -136,8 +136,8 @@ background-color: #F5F5F5; &.ci-success { - color: $bg_success; - border-color: $border_success; + color: $gl-success; + border-color: $gl-success; background-color: #F1FAF1; } @@ -148,20 +148,20 @@ } &.ci-running { - color: $bg_warning; - border-color: $border_warning; + color: $gl-warning; + border-color: $gl-warning; background-color: #FAF5F1; } &.ci-failed { - color: $bg_danger; - border-color: $border_danger; + color: $gl-danger; + border-color: $gl-danger; background-color: #FAF1F1; } &.ci-error { - color: $bg_danger; - border-color: $border_danger; + color: $gl-danger; + border-color: $gl-danger; background-color: #FAF1F1; } } diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index 40adc8b3ba7..e476b9ac776 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -194,7 +194,7 @@ ul.notes { &:hover { font-size: 24px; - background: $bg_primary; + background: $gl-primary; color: #FFF; @include show-add-diff-note; } diff --git a/app/assets/stylesheets/sections/notifications.scss b/app/assets/stylesheets/sections/notifications.scss index f11c5dff4ab..cc273f55222 100644 --- a/app/assets/stylesheets/sections/notifications.scss +++ b/app/assets/stylesheets/sections/notifications.scss @@ -10,13 +10,13 @@ } .ns-part { - color: $bg_primary; + color: $gl-primary; } .ns-watch { - color: $bg_success; + color: $gl-success; } .ns-mute { - color: $bg_danger; + color: $gl-danger; } diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/sections/projects.scss index 8bad9b139f4..586698fdd91 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/sections/projects.scss @@ -267,15 +267,15 @@ ul.nav.nav-projects-tabs { } .vs-public { - color: $bg_primary; + color: $gl-primary; } .vs-internal { - color: $bg_warning; + color: $gl-warning; } .vs-private { - color: $bg_success; + color: $gl-success; } .breadcrumb.repo-breadcrumb { diff --git a/app/assets/stylesheets/sections/tree.scss b/app/assets/stylesheets/sections/tree.scss index 60a1c00b04b..9f91c27f901 100644 --- a/app/assets/stylesheets/sections/tree.scss +++ b/app/assets/stylesheets/sections/tree.scss @@ -46,7 +46,7 @@ } i { - color: $bg_primary; + color: $gl-primary; } img { -- GitLab From 3cbc0b1c6d21ce6797900416ac9c1b22caf94845 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Mar 2015 20:02:34 -0800 Subject: [PATCH 1214/1609] Cleanup css variables --- app/assets/stylesheets/generic/common.scss | 2 +- .../stylesheets/generic/typography.scss | 72 +- app/assets/stylesheets/gl_bootstrap.scss | 24 +- app/assets/stylesheets/gl_variables.scss | 863 ++++++++++++++++++ app/assets/stylesheets/main/mixins.scss | 8 - app/assets/stylesheets/main/variables.scss | 38 +- app/assets/stylesheets/sections/notes.scss | 7 +- app/assets/stylesheets/sections/projects.scss | 2 +- app/assets/stylesheets/sections/tree.scss | 5 - 9 files changed, 882 insertions(+), 139 deletions(-) create mode 100644 app/assets/stylesheets/gl_variables.scss diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index ca01e207207..431f1d68a2e 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -71,7 +71,7 @@ pre { /** FLASH message **/ .author_link { - color: $link_color; + color: $gl-link-color; } .help li { color:$style_color; } diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index c547ebb3aaf..4d940ee6b29 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -2,28 +2,12 @@ * Headers * */ -h1.page-title { - @include page-title; - font-size: 28px; -} - -h2.page-title { - @include page-title; - font-size: 24px; -} - -h3.page-title { - @include page-title; - font-size: 22px; -} - -h4.page-title { +.page-title { margin-top: 0px; -} - -h6 { - color: #888; - text-transform: uppercase; + color: #333; + line-height: 1.5; + font-weight: normal; + margin-bottom: 5px; } /** CODE **/ @@ -36,52 +20,6 @@ pre { } } -/** - * Links - * - */ -a { - outline: none; - color: $link_color; - &:hover { - text-decoration: underline; - color: $link_hover_color; - } - - &:focus { - text-decoration: underline; - } - - &.darken { - color: $style_color; - } - - &.lined { - text-decoration: underline; - &:hover { text-decoration: underline; } - } - - &.gray { - color: gray; - } - - &.supp_diff_link { - text-align: center; - padding: 20px 0; - background: #f1f1f1; - width: 100%; - float: left; - } - - &.neib { - margin-right: 15px; - } -} - -a:focus { - outline: none; -} - .monospace { font-family: $monospace_font; } diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/gl_bootstrap.scss index 1c29fdac37b..18a6be409d3 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/gl_bootstrap.scss @@ -2,29 +2,7 @@ * Twitter bootstrap with GitLab customizations/additions * */ - -$font-size-base: 13px !default; -$nav-pills-active-link-hover-bg: $gl-primary; -$pagination-active-bg: $gl-primary; -$list-group-active-bg: $gl-primary; - -$brand-primary: $gl-primary; -$brand-success: $gl-success; -$brand-info: #029ACF; -$brand-warning: $gl-warning; -$brand-danger: $gl-danger; - -$state-primary-bg: lighten($gl-primary, 30%); -$state-success-bg: lighten($gl-success, 10%); -$state-info-bg: lighten($gl-info, 30%); -$state-warning-bg: lighten($gl-warning, 30%); -$state-danger-bg: lighten($gl-danger, 30%); - -$state-primary-txt: $gl-primary; -$state-success-txt: $gl-success; -$state-info-txt: $gl-info; -$state-warning-txt: $gl-warning; -$state-danger-txt: $gl-danger; +@import "gl_variables"; // Core variables and mixins @import "bootstrap/variables"; diff --git a/app/assets/stylesheets/gl_variables.scss b/app/assets/stylesheets/gl_variables.scss new file mode 100644 index 00000000000..d40ab1b9162 --- /dev/null +++ b/app/assets/stylesheets/gl_variables.scss @@ -0,0 +1,863 @@ +// Override Bootstrap variables here (defaults from bootstrap-sass v3.3.3): + +// +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +// $gray-base: #000 +// $gray-darker: lighten($gray-base, 13.5%) // #222 +// $gray-dark: lighten($gray-base, 20%) // #333 +// $gray: lighten($gray-base, 33.5%) // #555 +// $gray-light: lighten($gray-base, 46.7%) // #777 +// $gray-lighter: lighten($gray-base, 93.5%) // #eee + +$brand-primary: $gl-primary; +$brand-success: $gl-success; +$brand-info: $gl-info; +$brand-warning: $gl-warning; +$brand-danger: $gl-danger; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for ``. +// $body-bg: #fff +//** Global text color on ``. +// $text-color: $gray-dark + +//** Global textual link color. +$link-color: $gl-link-color; +//** Link hover color set via `darken()` function. +// $link-hover-color: darken($link-color, 15%) +//** Link hover decoration. +// $link-hover-decoration: underline + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +// $font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif +// $font-family-serif: Georgia, "Times New Roman", Times, serif +//** Default monospace fonts for ``, ``, and `
    `.
    +// $font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace
    +// $font-family-base:        $font-family-sans-serif
    +
    +$font-size-base:          $gl-font-size;
    +// $font-size-large:         ceil(($font-size-base * 1.25)) // ~18px
    +// $font-size-small:         ceil(($font-size-base * 0.85)) // ~12px
    +
    +// $font-size-h1:            floor(($font-size-base * 2.6)) // ~36px
    +// $font-size-h2:            floor(($font-size-base * 2.15)) // ~30px
    +// $font-size-h3:            ceil(($font-size-base * 1.7)) // ~24px
    +// $font-size-h4:            ceil(($font-size-base * 1.25)) // ~18px
    +// $font-size-h5:            $font-size-base
    +// $font-size-h6:            ceil(($font-size-base * 0.85)) // ~12px
    +
    +//** Unit-less `line-height` for use in components like buttons.
    +// $line-height-base:        1.428571429 // 20/14
    +//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
    +// $line-height-computed:    floor(($font-size-base * $line-height-base)) // ~20px
    +
    +//** By default, this inherits from the ``.
    +// $headings-font-family:    inherit
    +// $headings-font-weight:    500
    +// $headings-line-height:    1.1
    +// $headings-color:          inherit
    +
    +
    +//== Iconography
    +//
    +//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
    +
    +//** Load fonts from this directory.
    +
    +// [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
    +// [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
    +// $icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../fonts/bootstrap/")
    +
    +//** File name for all font files.
    +// $icon-font-name:          "glyphicons-halflings-regular"
    +//** Element ID within SVG icon file.
    +// $icon-font-svg-id:        "glyphicons_halflingsregular"
    +
    +
    +//== Components
    +//
    +//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
    +
    +// $padding-base-vertical:     6px
    +// $padding-base-horizontal:   12px
    +
    +// $padding-large-vertical:    10px
    +// $padding-large-horizontal:  16px
    +
    +// $padding-small-vertical:    5px
    +// $padding-small-horizontal:  10px
    +
    +// $padding-xs-vertical:       1px
    +// $padding-xs-horizontal:     5px
    +
    +// $line-height-large:         1.3333333 // extra decimals for Win 8.1 Chrome
    +// $line-height-small:         1.5
    +
    +// $border-radius-base:        4px
    +// $border-radius-large:       6px
    +// $border-radius-small:       3px
    +
    +//** Global color for active items (e.g., navs or dropdowns).
    +// $component-active-color:    #fff
    +//** Global background color for active items (e.g., navs or dropdowns).
    +// $component-active-bg:       $brand-primary
    +
    +//** Width of the `border` for generating carets that indicator dropdowns.
    +// $caret-width-base:          4px
    +//** Carets increase slightly in size for larger components.
    +// $caret-width-large:         5px
    +
    +
    +//== Tables
    +//
    +//## Customizes the `.table` component with basic values, each used across all table variations.
    +
    +//** Padding for `
    `s and ``s. +// $table-cell-padding: 8px +//** Padding for cells in `.table-condensed`. +// $table-condensed-cell-padding: 5px + +//** Default background color used for all tables. +// $table-bg: transparent +//** Background color used for `.table-striped`. +// $table-bg-accent: #f9f9f9 +//** Background color used for `.table-hover`. +// $table-bg-hover: #f5f5f5 +// $table-bg-active: $table-bg-hover + +//** Border color for table and cell borders. +// $table-border-color: #ddd + + +//== Buttons +// +//## For each of Bootstrap's buttons, define text, background and border color. + +// $btn-font-weight: normal + +// $btn-default-color: #333 +// $btn-default-bg: #fff +// $btn-default-border: #ccc + +// $btn-primary-color: #fff +// $btn-primary-bg: $brand-primary +// $btn-primary-border: darken($btn-primary-bg, 5%) + +// $btn-success-color: #fff +// $btn-success-bg: $brand-success +// $btn-success-border: darken($btn-success-bg, 5%) + +// $btn-info-color: #fff +// $btn-info-bg: $brand-info +// $btn-info-border: darken($btn-info-bg, 5%) + +// $btn-warning-color: #fff +// $btn-warning-bg: $brand-warning +// $btn-warning-border: darken($btn-warning-bg, 5%) + +// $btn-danger-color: #fff +// $btn-danger-bg: $brand-danger +// $btn-danger-border: darken($btn-danger-bg, 5%) + +// $btn-link-disabled-color: $gray-light + + +//== Forms +// +//## + +//** `` background color +// $input-bg: #fff +//** `` background color +// $input-bg-disabled: $gray-lighter + +//** Text color for ``s +// $input-color: $gray +//** `` border color +// $input-border: #ccc + +// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4 +//** Default `.form-control` border radius +// This has no effect on ``s in CSS. +// $input-border-radius: $border-radius-base +//** Large `.form-control` border radius +// $input-border-radius-large: $border-radius-large +//** Small `.form-control` border radius +// $input-border-radius-small: $border-radius-small + +//** Border color for inputs on focus +// $input-border-focus: #66afe9 + +//** Placeholder text color +// $input-color-placeholder: #999 + +//** Default `.form-control` height +// $input-height-base: ($line-height-computed + ($padding-base-vertical * 2) + 2) +//** Large `.form-control` height +// $input-height-large: (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) +//** Small `.form-control` height +// $input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) + +// $legend-color: $gray-dark +// $legend-border-color: #e5e5e5 + +//** Background color for textual input addons +// $input-group-addon-bg: $gray-lighter +//** Border color for textual input addons +// $input-group-addon-border-color: $input-border + +//** Disabled cursor for form controls and buttons. +// $cursor-disabled: not-allowed + + +//== Dropdowns +// +//## Dropdown menu container and contents. + +//** Background for the dropdown menu. +// $dropdown-bg: #fff +//** Dropdown menu `border-color`. +// $dropdown-border: rgba(0,0,0,.15) +//** Dropdown menu `border-color` **for IE8**. +// $dropdown-fallback-border: #ccc +//** Divider color for between dropdown items. +// $dropdown-divider-bg: #e5e5e5 + +//** Dropdown link text color. +// $dropdown-link-color: $gray-dark +//** Hover color for dropdown links. +// $dropdown-link-hover-color: darken($gray-dark, 5%) +//** Hover background for dropdown links. +// $dropdown-link-hover-bg: #f5f5f5 + +//** Active dropdown menu item text color. +// $dropdown-link-active-color: $component-active-color +//** Active dropdown menu item background color. +// $dropdown-link-active-bg: $component-active-bg + +//** Disabled dropdown menu item background color. +// $dropdown-link-disabled-color: $gray-light + +//** Text color for headers within dropdown menus. +// $dropdown-header-color: $gray-light + +//** Deprecated `$dropdown-caret-color` as of v3.1.0 +// $dropdown-caret-color: #000 + + +//-- Z-index master list +// +// Warning: Avoid customizing these values. They're used for a bird's eye view +// of components dependent on the z-axis and are designed to all work together. +// +// Note: These variables are not generated into the Customizer. + +// $zindex-navbar: 1000 +// $zindex-dropdown: 1000 +// $zindex-popover: 1060 +// $zindex-tooltip: 1070 +// $zindex-navbar-fixed: 1030 +// $zindex-modal: 1040 + + +//== Media queries breakpoints +// +//## Define the breakpoints at which your layout will change, adapting to different screen sizes. + +// Extra small screen / phone +//** Deprecated `$screen-xs` as of v3.0.1 +// $screen-xs: 480px +//** Deprecated `$screen-xs-min` as of v3.2.0 +// $screen-xs-min: $screen-xs +//** Deprecated `$screen-phone` as of v3.0.1 +// $screen-phone: $screen-xs-min + +// Small screen / tablet +//** Deprecated `$screen-sm` as of v3.0.1 +// $screen-sm: 768px +// $screen-sm-min: $screen-sm +//** Deprecated `$screen-tablet` as of v3.0.1 +// $screen-tablet: $screen-sm-min + +// Medium screen / desktop +//** Deprecated `$screen-md` as of v3.0.1 +// $screen-md: 992px +// $screen-md-min: $screen-md +//** Deprecated `$screen-desktop` as of v3.0.1 +// $screen-desktop: $screen-md-min + +// Large screen / wide desktop +//** Deprecated `$screen-lg` as of v3.0.1 +// $screen-lg: 1200px +// $screen-lg-min: $screen-lg +//** Deprecated `$screen-lg-desktop` as of v3.0.1 +// $screen-lg-desktop: $screen-lg-min + +// So media queries don't overlap when required, provide a maximum +// $screen-xs-max: ($screen-sm-min - 1) +// $screen-sm-max: ($screen-md-min - 1) +// $screen-md-max: ($screen-lg-min - 1) + + +//== Grid system +// +//## Define your custom responsive grid. + +//** Number of columns in the grid. +// $grid-columns: 12 +//** Padding between columns. Gets divided in half for the left and right. +// $grid-gutter-width: 30px +// Navbar collapse +//** Point at which the navbar becomes uncollapsed. +// $grid-float-breakpoint: $screen-sm-min +//** Point at which the navbar begins collapsing. +// $grid-float-breakpoint-max: ($grid-float-breakpoint - 1) + + +//== Container sizes +// +//## Define the maximum width of `.container` for different screen sizes. + +// Small screen / tablet +// $container-tablet: (720px + $grid-gutter-width) +//** For `$screen-sm-min` and up. +// $container-sm: $container-tablet + +// Medium screen / desktop +// $container-desktop: (940px + $grid-gutter-width) +//** For `$screen-md-min` and up. +// $container-md: $container-desktop + +// Large screen / wide desktop +// $container-large-desktop: (1140px + $grid-gutter-width) +//** For `$screen-lg-min` and up. +// $container-lg: $container-large-desktop + + +//== Navbar +// +//## + +// Basics of a navbar +// $navbar-height: 50px +// $navbar-margin-bottom: $line-height-computed +// $navbar-border-radius: $border-radius-base +// $navbar-padding-horizontal: floor(($grid-gutter-width / 2)) +// $navbar-padding-vertical: (($navbar-height - $line-height-computed) / 2) +// $navbar-collapse-max-height: 340px + +// $navbar-default-color: #777 +// $navbar-default-bg: #f8f8f8 +// $navbar-default-border: darken($navbar-default-bg, 6.5%) + +// Navbar links +// $navbar-default-link-color: #777 +// $navbar-default-link-hover-color: #333 +// $navbar-default-link-hover-bg: transparent +// $navbar-default-link-active-color: #555 +// $navbar-default-link-active-bg: darken($navbar-default-bg, 6.5%) +// $navbar-default-link-disabled-color: #ccc +// $navbar-default-link-disabled-bg: transparent + +// Navbar brand label +// $navbar-default-brand-color: $navbar-default-link-color +// $navbar-default-brand-hover-color: darken($navbar-default-brand-color, 10%) +// $navbar-default-brand-hover-bg: transparent + +// Navbar toggle +// $navbar-default-toggle-hover-bg: #ddd +// $navbar-default-toggle-icon-bar-bg: #888 +// $navbar-default-toggle-border-color: #ddd + + +// Inverted navbar +// Reset inverted navbar basics +// $navbar-inverse-color: lighten($gray-light, 15%) +// $navbar-inverse-bg: #222 +// $navbar-inverse-border: darken($navbar-inverse-bg, 10%) + +// Inverted navbar links +// $navbar-inverse-link-color: lighten($gray-light, 15%) +// $navbar-inverse-link-hover-color: #fff +// $navbar-inverse-link-hover-bg: transparent +// $navbar-inverse-link-active-color: $navbar-inverse-link-hover-color +// $navbar-inverse-link-active-bg: darken($navbar-inverse-bg, 10%) +// $navbar-inverse-link-disabled-color: #444 +// $navbar-inverse-link-disabled-bg: transparent + +// Inverted navbar brand label +// $navbar-inverse-brand-color: $navbar-inverse-link-color +// $navbar-inverse-brand-hover-color: #fff +// $navbar-inverse-brand-hover-bg: transparent + +// Inverted navbar toggle +// $navbar-inverse-toggle-hover-bg: #333 +// $navbar-inverse-toggle-icon-bar-bg: #fff +// $navbar-inverse-toggle-border-color: #333 + + +//== Navs +// +//## + +//=== Shared nav styles +// $nav-link-padding: 10px 15px +// $nav-link-hover-bg: $gray-lighter + +// $nav-disabled-link-color: $gray-light +// $nav-disabled-link-hover-color: $gray-light + +//== Tabs +// $nav-tabs-border-color: #ddd + +// $nav-tabs-link-hover-border-color: $gray-lighter + +// $nav-tabs-active-link-hover-bg: $body-bg +// $nav-tabs-active-link-hover-color: $gray +// $nav-tabs-active-link-hover-border-color: #ddd + +// $nav-tabs-justified-link-border-color: #ddd +// $nav-tabs-justified-active-link-border-color: $body-bg + +//== Pills +// $nav-pills-border-radius: $border-radius-base +// $nav-pills-active-link-hover-bg: $component-active-bg +// $nav-pills-active-link-hover-color: $component-active-color + + +//== Pagination +// +//## + +// $pagination-color: $link-color +// $pagination-bg: #fff +// $pagination-border: #ddd + +// $pagination-hover-color: $link-hover-color +// $pagination-hover-bg: $gray-lighter +// $pagination-hover-border: #ddd + +// $pagination-active-color: #fff +// $pagination-active-bg: $brand-primary +// $pagination-active-border: $brand-primary + +// $pagination-disabled-color: $gray-light +// $pagination-disabled-bg: #fff +// $pagination-disabled-border: #ddd + + +//== Pager +// +//## + +// $pager-bg: $pagination-bg +// $pager-border: $pagination-border +// $pager-border-radius: 15px + +// $pager-hover-bg: $pagination-hover-bg + +// $pager-active-bg: $pagination-active-bg +// $pager-active-color: $pagination-active-color + +// $pager-disabled-color: $pagination-disabled-color + + +//== Jumbotron +// +//## + +// $jumbotron-padding: 30px +// $jumbotron-color: inherit +// $jumbotron-bg: $gray-lighter +// $jumbotron-heading-color: inherit +// $jumbotron-font-size: ceil(($font-size-base * 1.5)) + + +//== Form states and alerts +// +//## Define colors for form feedback states and, by default, alerts. + +// $state-success-text: #3c763d +// $state-success-bg: #dff0d8 +// $state-success-border: darken(adjust-hue($state-success-bg, -10), 5%) + +// $state-info-text: #31708f +// $state-info-bg: #d9edf7 +// $state-info-border: darken(adjust-hue($state-info-bg, -10), 7%) + +// $state-warning-text: #8a6d3b +// $state-warning-bg: #fcf8e3 +// $state-warning-border: darken(adjust-hue($state-warning-bg, -10), 5%) + +// $state-danger-text: #a94442 +// $state-danger-bg: #f2dede +// $state-danger-border: darken(adjust-hue($state-danger-bg, -10), 5%) + + +//== Tooltips +// +//## + +//** Tooltip max width +// $tooltip-max-width: 200px +//** Tooltip text color +// $tooltip-color: #fff +//** Tooltip background color +// $tooltip-bg: #000 +// $tooltip-opacity: .9 + +//** Tooltip arrow width +// $tooltip-arrow-width: 5px +//** Tooltip arrow color +// $tooltip-arrow-color: $tooltip-bg + + +//== Popovers +// +//## + +//** Popover body background color +// $popover-bg: #fff +//** Popover maximum width +// $popover-max-width: 276px +//** Popover border color +// $popover-border-color: rgba(0,0,0,.2) +//** Popover fallback border color +// $popover-fallback-border-color: #ccc + +//** Popover title background color +// $popover-title-bg: darken($popover-bg, 3%) + +//** Popover arrow width +// $popover-arrow-width: 10px +//** Popover arrow color +// $popover-arrow-color: $popover-bg + +//** Popover outer arrow width +// $popover-arrow-outer-width: ($popover-arrow-width + 1) +//** Popover outer arrow color +// $popover-arrow-outer-color: fade_in($popover-border-color, 0.05) +//** Popover outer arrow fallback color +// $popover-arrow-outer-fallback-color: darken($popover-fallback-border-color, 20%) + + +//== Labels +// +//## + +//** Default label background color +// $label-default-bg: $gray-light +//** Primary label background color +// $label-primary-bg: $brand-primary +//** Success label background color +// $label-success-bg: $brand-success +//** Info label background color +// $label-info-bg: $brand-info +//** Warning label background color +// $label-warning-bg: $brand-warning +//** Danger label background color +// $label-danger-bg: $brand-danger + +//** Default label text color +// $label-color: #fff +//** Default text color of a linked label +// $label-link-hover-color: #fff + + +//== Modals +// +//## + +//** Padding applied to the modal body +// $modal-inner-padding: 15px + +//** Padding applied to the modal title +// $modal-title-padding: 15px +//** Modal title line-height +// $modal-title-line-height: $line-height-base + +//** Background color of modal content area +// $modal-content-bg: #fff +//** Modal content border color +// $modal-content-border-color: rgba(0,0,0,.2) +//** Modal content border color **for IE8** +// $modal-content-fallback-border-color: #999 + +//** Modal backdrop background color +// $modal-backdrop-bg: #000 +//** Modal backdrop opacity +// $modal-backdrop-opacity: .5 +//** Modal header border color +// $modal-header-border-color: #e5e5e5 +//** Modal footer border color +// $modal-footer-border-color: $modal-header-border-color + +// $modal-lg: 900px +// $modal-md: 600px +// $modal-sm: 300px + + +//== Alerts +// +//## Define alert colors, border radius, and padding. + +// $alert-padding: 15px +// $alert-border-radius: $border-radius-base +// $alert-link-font-weight: bold + +// $alert-success-bg: $state-success-bg +// $alert-success-text: $state-success-text +// $alert-success-border: $state-success-border + +// $alert-info-bg: $state-info-bg +// $alert-info-text: $state-info-text +// $alert-info-border: $state-info-border + +// $alert-warning-bg: $state-warning-bg +// $alert-warning-text: $state-warning-text +// $alert-warning-border: $state-warning-border + +// $alert-danger-bg: $state-danger-bg +// $alert-danger-text: $state-danger-text +// $alert-danger-border: $state-danger-border + + +//== Progress bars +// +//## + +//** Background color of the whole progress component +// $progress-bg: #f5f5f5 +//** Progress bar text color +// $progress-bar-color: #fff +//** Variable for setting rounded corners on progress bar. +// $progress-border-radius: $border-radius-base + +//** Default progress bar color +// $progress-bar-bg: $brand-primary +//** Success progress bar color +// $progress-bar-success-bg: $brand-success +//** Warning progress bar color +// $progress-bar-warning-bg: $brand-warning +//** Danger progress bar color +// $progress-bar-danger-bg: $brand-danger +//** Info progress bar color +// $progress-bar-info-bg: $brand-info + + +//== List group +// +//## + +//** Background color on `.list-group-item` +// $list-group-bg: #fff +//** `.list-group-item` border color +// $list-group-border: #ddd +//** List group border radius +// $list-group-border-radius: $border-radius-base + +//** Background color of single list items on hover +// $list-group-hover-bg: #f5f5f5 +//** Text color of active list items +// $list-group-active-color: $component-active-color +//** Background color of active list items +// $list-group-active-bg: $component-active-bg +//** Border color of active list elements +// $list-group-active-border: $list-group-active-bg +//** Text color for content within active list items +// $list-group-active-text-color: lighten($list-group-active-bg, 40%) + +//** Text color of disabled list items +// $list-group-disabled-color: $gray-light +//** Background color of disabled list items +// $list-group-disabled-bg: $gray-lighter +//** Text color for content within disabled list items +// $list-group-disabled-text-color: $list-group-disabled-color + +// $list-group-link-color: #555 +// $list-group-link-hover-color: $list-group-link-color +// $list-group-link-heading-color: #333 + + +//== Panels +// +//## + +// $panel-bg: #fff +// $panel-body-padding: 15px +// $panel-heading-padding: 10px 15px +// $panel-footer-padding: $panel-heading-padding +// $panel-border-radius: $border-radius-base + +//** Border color for elements within panels +// $panel-inner-border: #ddd +// $panel-footer-bg: #f5f5f5 + +// $panel-default-text: $gray-dark +// $panel-default-border: #ddd +// $panel-default-heading-bg: #f5f5f5 + +// $panel-primary-text: #fff +// $panel-primary-border: $brand-primary +// $panel-primary-heading-bg: $brand-primary + +// $panel-success-text: $state-success-text +// $panel-success-border: $state-success-border +// $panel-success-heading-bg: $state-success-bg + +// $panel-info-text: $state-info-text +// $panel-info-border: $state-info-border +// $panel-info-heading-bg: $state-info-bg + +// $panel-warning-text: $state-warning-text +// $panel-warning-border: $state-warning-border +// $panel-warning-heading-bg: $state-warning-bg + +// $panel-danger-text: $state-danger-text +// $panel-danger-border: $state-danger-border +// $panel-danger-heading-bg: $state-danger-bg + + +//== Thumbnails +// +//## + +//** Padding around the thumbnail image +// $thumbnail-padding: 4px +//** Thumbnail background color +// $thumbnail-bg: $body-bg +//** Thumbnail border color +// $thumbnail-border: #ddd +//** Thumbnail border radius +// $thumbnail-border-radius: $border-radius-base + +//** Custom text color for thumbnail captions +// $thumbnail-caption-color: $text-color +//** Padding around the thumbnail caption +// $thumbnail-caption-padding: 9px + + +//== Wells +// +//## + +// $well-bg: #f5f5f5 +// $well-border: darken($well-bg, 7%) + + +//== Badges +// +//## + +// $badge-color: #fff +//** Linked badge text color on hover +// $badge-link-hover-color: #fff +// $badge-bg: $gray-light + +//** Badge text color in active nav link +// $badge-active-color: $link-color +//** Badge background color in active nav link +// $badge-active-bg: #fff + +// $badge-font-weight: bold +// $badge-line-height: 1 +// $badge-border-radius: 10px + + +//== Breadcrumbs +// +//## + +// $breadcrumb-padding-vertical: 8px +// $breadcrumb-padding-horizontal: 15px +//** Breadcrumb background color +// $breadcrumb-bg: #f5f5f5 +//** Breadcrumb text color +// $breadcrumb-color: #ccc +//** Text color of current page in the breadcrumb +// $breadcrumb-active-color: $gray-light +//** Textual separator for between breadcrumb elements +// $breadcrumb-separator: "/" + + +//== Carousel +// +//## + +// $carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6) + +// $carousel-control-color: #fff +// $carousel-control-width: 15% +// $carousel-control-opacity: .5 +// $carousel-control-font-size: 20px + +// $carousel-indicator-active-bg: #fff +// $carousel-indicator-border-color: #fff + +// $carousel-caption-color: #fff + + +//== Close +// +//## + +// $close-font-weight: bold +// $close-color: #000 +// $close-text-shadow: 0 1px 0 #fff + + +//== Code +// +//## + +// $code-color: #c7254e +// $code-bg: #f9f2f4 + +// $kbd-color: #fff +// $kbd-bg: #333 + +// $pre-bg: #f5f5f5 +// $pre-color: $gray-dark +// $pre-border-color: #ccc +// $pre-scrollable-max-height: 340px + + +//== Type +// +//## + +//** Horizontal offset for forms and lists. +// $component-offset-horizontal: 180px +//** Text muted color +// $text-muted: $gray-light +//** Abbreviations and acronyms border color +// $abbr-border-color: $gray-light +//** Headings small color +// $headings-small-color: $gray-light +//** Blockquote small color +// $blockquote-small-color: $gray-light +//** Blockquote font size +// $blockquote-font-size: ($font-size-base * 1.25) +//** Blockquote border color +// $blockquote-border-color: $gray-lighter +//** Page header border color +// $page-header-border-color: $gray-lighter +//** Width of horizontal description list titles +// $dl-horizontal-offset: $component-offset-horizontal +//** Horizontal line color. +// $hr-border: $gray-lighter diff --git a/app/assets/stylesheets/main/mixins.scss b/app/assets/stylesheets/main/mixins.scss index e54482d14c3..80cb0c15650 100644 --- a/app/assets/stylesheets/main/mixins.scss +++ b/app/assets/stylesheets/main/mixins.scss @@ -121,14 +121,6 @@ } } -@mixin page-title { - color: #333; - line-height: 1.5; - font-weight: normal; - margin-top: 0px; - margin-bottom: 10px; -} - @mixin str-truncated($max_width: 82%) { display: inline-block; overflow: hidden; diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss index 57b881c0736..6be81b23351 100644 --- a/app/assets/stylesheets/main/variables.scss +++ b/app/assets/stylesheets/main/variables.scss @@ -1,17 +1,14 @@ -/* - * General Colors - */ $style_color: #474D57; $hover: #FFF3EB; $box_bg: #F9F9F9; - -/* - * Link colors - */ -$link_color: #446e9b; -$link_hover_color: darken($link-color, 10%); - -$btn-border: 1px solid #ccc; +$gl-link-color: #446e9b; +$nprogress-color: #c0392b; +$gl-font-size: 13px; +$list-font-size: 15px; +$sidebar_width: 230px; +$avatar_radius: 50%; +$code_font_size: 13px; +$code_line_height: 1.5; /* * State colors: @@ -27,22 +24,3 @@ $gl-warning: #EB9532; */ $added: #63c363; $deleted: #f77; - -/** - * NProgress customize - */ -$nprogress-color: #c0392b; - -/** - * Font sizes - */ -$list-font-size: 15px; - -/** - * Sidebar navigation width - */ -$sidebar_width: 230px; - -$avatar_radius: 50%; -$code_font_size: 13px; -$code_line_height: 1.5; diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/sections/notes.scss index e476b9ac776..73f23626d57 100644 --- a/app/assets/stylesheets/sections/notes.scss +++ b/app/assets/stylesheets/sections/notes.scss @@ -40,7 +40,7 @@ ul.notes { font-weight: bold; font-size: 14px; &:hover { - color: $link_color; + color: $gl-link-color; } } .author-username { @@ -70,7 +70,7 @@ ul.notes { a[href*="/uploads/"] { &:before { margin-right: 4px; - + font: normal normal normal 14px/1 FontAwesome; font-size: inherit; text-rendering: auto; @@ -153,7 +153,6 @@ ul.notes { @extend .cgray; &:hover { - color: $link_hover_color; &.danger { @extend .cred; } } } @@ -181,7 +180,7 @@ ul.notes { background: #FFF; padding: 4px; font-size: 16px; - color: $link_color; + color: $gl-link-color; margin-left: -60px; position: absolute; z-index: 10; diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/sections/projects.scss index 586698fdd91..3a912d234fa 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/sections/projects.scss @@ -108,7 +108,7 @@ .btn { background: none; - color: $link_color; + color: $gl-link-color; &.active { background-color: #f5f5f5; diff --git a/app/assets/stylesheets/sections/tree.scss b/app/assets/stylesheets/sections/tree.scss index 9f91c27f901..3305abc7d2a 100644 --- a/app/assets/stylesheets/sections/tree.scss +++ b/app/assets/stylesheets/sections/tree.scss @@ -39,11 +39,6 @@ .tree-item-file-name { max-width: 320px; vertical-align: middle; - a { - &:hover { - color: $link_hover_color; - } - } i { color: $gl-primary; -- GitLab From ceb8f9dfeb77c99dfac121fb349ee3c8a9969ae9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Mar 2015 21:15:15 -0800 Subject: [PATCH 1215/1609] Remove custom css for panels and navs --- app/assets/stylesheets/gl_bootstrap.scss | 54 ------------------- app/assets/stylesheets/gl_variables.scss | 4 +- app/assets/stylesheets/main/mixins.scss | 11 ---- .../stylesheets/sections/dashboard.scss | 7 +-- 4 files changed, 4 insertions(+), 72 deletions(-) diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/gl_bootstrap.scss index 18a6be409d3..e8e58511f3b 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/gl_bootstrap.scss @@ -131,10 +131,6 @@ } } } - - &.nav-small-tabs > li > a { - padding: 6px 9px; - } } .nav-tabs > li > a, @@ -142,15 +138,6 @@ color: #666; } -.nav-compact > li > a { - padding: 6px 12px; -} - -.nav-small > li > a { - padding: 3px 5px; - font-size: 12px; -} - /** * fix to keep tooltips position in top navigation bar * @@ -165,10 +152,7 @@ * */ .panel { - @include border-radius(0px); - .panel-heading { - @include border-radius(0px); font-size: 14px; line-height: 18px; @@ -206,41 +190,3 @@ } } } - -.panel-default { - .panel-heading { - background-color: #EEE; - } -} - -.panel-danger { - @include panel-colored; - .panel-heading { - color: $gl-danger; - border-color: $gl-danger; - } -} - -.panel-success { - @include panel-colored; - .panel-heading { - color: $gl-success; - border-color: $gl-success; - } -} - -.panel-primary { - @include panel-colored; - .panel-heading { - color: $gl-primary; - border-color: $gl-primary; - } -} - -.panel-warning { - @include panel-colored; - .panel-heading { - color: $gl-warning; - border-color: $gl-warning; - } -} diff --git a/app/assets/stylesheets/gl_variables.scss b/app/assets/stylesheets/gl_variables.scss index d40ab1b9162..4f54551a22c 100644 --- a/app/assets/stylesheets/gl_variables.scss +++ b/app/assets/stylesheets/gl_variables.scss @@ -617,7 +617,7 @@ $font-size-base: $gl-font-size; //## Define alert colors, border radius, and padding. // $alert-padding: 15px -// $alert-border-radius: $border-radius-base +$alert-border-radius: 0; // $alert-link-font-weight: bold // $alert-success-bg: $state-success-bg @@ -702,7 +702,7 @@ $font-size-base: $gl-font-size; // $panel-body-padding: 15px // $panel-heading-padding: 10px 15px // $panel-footer-padding: $panel-heading-padding -// $panel-border-radius: $border-radius-base +$panel-border-radius: 0; //** Border color for elements within panels // $panel-inner-border: #ddd diff --git a/app/assets/stylesheets/main/mixins.scss b/app/assets/stylesheets/main/mixins.scss index 80cb0c15650..ccba65e3fd5 100644 --- a/app/assets/stylesheets/main/mixins.scss +++ b/app/assets/stylesheets/main/mixins.scss @@ -129,14 +129,3 @@ white-space: nowrap; max-width: $max_width; } - -@mixin panel-colored { - border: 1px solid #EEE; - background: $box_bg; - @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); - - .panel-heading { - font-weight: bold; - background-color: $box_bg; - } -} diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index c8e3c7d4a2a..96f84b7122b 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -31,11 +31,8 @@ li { &.active { a { - background-color: #EEE; - border-bottom: 1px solid #EEE !important; - &:hover { - background: #eee; - } + background-color: whitesmoke !important; + border-bottom: 1px solid whitesmoke !important; } } -- GitLab From 887cf5c7101c058d30b10d767c186d548e6c5e92 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 6 Mar 2015 21:40:23 -0800 Subject: [PATCH 1216/1609] Better name for pages css dir --- .../stylesheets/{sections => generic}/nav_sidebar.scss | 0 app/assets/stylesheets/main/fonts.scss | 3 --- app/assets/stylesheets/main/variables.scss | 8 +++++++- app/assets/stylesheets/{sections => pages}/admin.scss | 0 app/assets/stylesheets/{sections => pages}/commit.scss | 0 app/assets/stylesheets/{sections => pages}/commits.scss | 0 app/assets/stylesheets/{sections => pages}/dashboard.scss | 0 app/assets/stylesheets/{sections => pages}/diff.scss | 0 app/assets/stylesheets/{sections => pages}/editor.scss | 0 app/assets/stylesheets/{sections => pages}/errors.scss | 0 app/assets/stylesheets/{sections => pages}/events.scss | 0 app/assets/stylesheets/{sections => pages}/explore.scss | 0 app/assets/stylesheets/{sections => pages}/graph.scss | 0 app/assets/stylesheets/{sections => pages}/groups.scss | 0 app/assets/stylesheets/{sections => pages}/header.scss | 0 app/assets/stylesheets/{sections => pages}/help.scss | 0 app/assets/stylesheets/{sections => pages}/import.scss | 0 app/assets/stylesheets/{sections => pages}/issuable.scss | 0 app/assets/stylesheets/{sections => pages}/issues.scss | 0 app/assets/stylesheets/{sections => pages}/labels.scss | 0 app/assets/stylesheets/{sections => pages}/login.scss | 0 .../stylesheets/{sections => pages}/markdown_area.scss | 0 .../stylesheets/{sections => pages}/merge_requests.scss | 0 app/assets/stylesheets/{sections => pages}/milestone.scss | 0 app/assets/stylesheets/{sections => pages}/note_form.scss | 0 app/assets/stylesheets/{sections => pages}/notes.scss | 0 .../stylesheets/{sections => pages}/notifications.scss | 0 app/assets/stylesheets/{sections => pages}/profile.scss | 0 app/assets/stylesheets/{sections => pages}/projects.scss | 0 app/assets/stylesheets/{sections => pages}/search.scss | 0 app/assets/stylesheets/{sections => pages}/snippets.scss | 0 .../stylesheets/{sections => pages}/stat_graph.scss | 0 app/assets/stylesheets/{sections => pages}/themes.scss | 0 app/assets/stylesheets/{sections => pages}/tree.scss | 0 app/assets/stylesheets/{sections => pages}/votes.scss | 0 app/assets/stylesheets/{sections => pages}/wiki.scss | 0 36 files changed, 7 insertions(+), 4 deletions(-) rename app/assets/stylesheets/{sections => generic}/nav_sidebar.scss (100%) delete mode 100644 app/assets/stylesheets/main/fonts.scss rename app/assets/stylesheets/{sections => pages}/admin.scss (100%) rename app/assets/stylesheets/{sections => pages}/commit.scss (100%) rename app/assets/stylesheets/{sections => pages}/commits.scss (100%) rename app/assets/stylesheets/{sections => pages}/dashboard.scss (100%) rename app/assets/stylesheets/{sections => pages}/diff.scss (100%) rename app/assets/stylesheets/{sections => pages}/editor.scss (100%) rename app/assets/stylesheets/{sections => pages}/errors.scss (100%) rename app/assets/stylesheets/{sections => pages}/events.scss (100%) rename app/assets/stylesheets/{sections => pages}/explore.scss (100%) rename app/assets/stylesheets/{sections => pages}/graph.scss (100%) rename app/assets/stylesheets/{sections => pages}/groups.scss (100%) rename app/assets/stylesheets/{sections => pages}/header.scss (100%) rename app/assets/stylesheets/{sections => pages}/help.scss (100%) rename app/assets/stylesheets/{sections => pages}/import.scss (100%) rename app/assets/stylesheets/{sections => pages}/issuable.scss (100%) rename app/assets/stylesheets/{sections => pages}/issues.scss (100%) rename app/assets/stylesheets/{sections => pages}/labels.scss (100%) rename app/assets/stylesheets/{sections => pages}/login.scss (100%) rename app/assets/stylesheets/{sections => pages}/markdown_area.scss (100%) rename app/assets/stylesheets/{sections => pages}/merge_requests.scss (100%) rename app/assets/stylesheets/{sections => pages}/milestone.scss (100%) rename app/assets/stylesheets/{sections => pages}/note_form.scss (100%) rename app/assets/stylesheets/{sections => pages}/notes.scss (100%) rename app/assets/stylesheets/{sections => pages}/notifications.scss (100%) rename app/assets/stylesheets/{sections => pages}/profile.scss (100%) rename app/assets/stylesheets/{sections => pages}/projects.scss (100%) rename app/assets/stylesheets/{sections => pages}/search.scss (100%) rename app/assets/stylesheets/{sections => pages}/snippets.scss (100%) rename app/assets/stylesheets/{sections => pages}/stat_graph.scss (100%) rename app/assets/stylesheets/{sections => pages}/themes.scss (100%) rename app/assets/stylesheets/{sections => pages}/tree.scss (100%) rename app/assets/stylesheets/{sections => pages}/votes.scss (100%) rename app/assets/stylesheets/{sections => pages}/wiki.scss (100%) diff --git a/app/assets/stylesheets/sections/nav_sidebar.scss b/app/assets/stylesheets/generic/nav_sidebar.scss similarity index 100% rename from app/assets/stylesheets/sections/nav_sidebar.scss rename to app/assets/stylesheets/generic/nav_sidebar.scss diff --git a/app/assets/stylesheets/main/fonts.scss b/app/assets/stylesheets/main/fonts.scss deleted file mode 100644 index f945aaca848..00000000000 --- a/app/assets/stylesheets/main/fonts.scss +++ /dev/null @@ -1,3 +0,0 @@ -/** Typo **/ -$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; -$regular_font: "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss index 6be81b23351..d751678f1b3 100644 --- a/app/assets/stylesheets/main/variables.scss +++ b/app/assets/stylesheets/main/variables.scss @@ -19,8 +19,14 @@ $gl-primary: #446e9b; $gl-info: #029ACF; $gl-warning: #EB9532; -/** +/* * Commit Diff Colors */ $added: #63c363; $deleted: #f77; + +/* + * Fonts + */ +$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; +$regular_font: "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/app/assets/stylesheets/sections/admin.scss b/app/assets/stylesheets/pages/admin.scss similarity index 100% rename from app/assets/stylesheets/sections/admin.scss rename to app/assets/stylesheets/pages/admin.scss diff --git a/app/assets/stylesheets/sections/commit.scss b/app/assets/stylesheets/pages/commit.scss similarity index 100% rename from app/assets/stylesheets/sections/commit.scss rename to app/assets/stylesheets/pages/commit.scss diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/pages/commits.scss similarity index 100% rename from app/assets/stylesheets/sections/commits.scss rename to app/assets/stylesheets/pages/commits.scss diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss similarity index 100% rename from app/assets/stylesheets/sections/dashboard.scss rename to app/assets/stylesheets/pages/dashboard.scss diff --git a/app/assets/stylesheets/sections/diff.scss b/app/assets/stylesheets/pages/diff.scss similarity index 100% rename from app/assets/stylesheets/sections/diff.scss rename to app/assets/stylesheets/pages/diff.scss diff --git a/app/assets/stylesheets/sections/editor.scss b/app/assets/stylesheets/pages/editor.scss similarity index 100% rename from app/assets/stylesheets/sections/editor.scss rename to app/assets/stylesheets/pages/editor.scss diff --git a/app/assets/stylesheets/sections/errors.scss b/app/assets/stylesheets/pages/errors.scss similarity index 100% rename from app/assets/stylesheets/sections/errors.scss rename to app/assets/stylesheets/pages/errors.scss diff --git a/app/assets/stylesheets/sections/events.scss b/app/assets/stylesheets/pages/events.scss similarity index 100% rename from app/assets/stylesheets/sections/events.scss rename to app/assets/stylesheets/pages/events.scss diff --git a/app/assets/stylesheets/sections/explore.scss b/app/assets/stylesheets/pages/explore.scss similarity index 100% rename from app/assets/stylesheets/sections/explore.scss rename to app/assets/stylesheets/pages/explore.scss diff --git a/app/assets/stylesheets/sections/graph.scss b/app/assets/stylesheets/pages/graph.scss similarity index 100% rename from app/assets/stylesheets/sections/graph.scss rename to app/assets/stylesheets/pages/graph.scss diff --git a/app/assets/stylesheets/sections/groups.scss b/app/assets/stylesheets/pages/groups.scss similarity index 100% rename from app/assets/stylesheets/sections/groups.scss rename to app/assets/stylesheets/pages/groups.scss diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/pages/header.scss similarity index 100% rename from app/assets/stylesheets/sections/header.scss rename to app/assets/stylesheets/pages/header.scss diff --git a/app/assets/stylesheets/sections/help.scss b/app/assets/stylesheets/pages/help.scss similarity index 100% rename from app/assets/stylesheets/sections/help.scss rename to app/assets/stylesheets/pages/help.scss diff --git a/app/assets/stylesheets/sections/import.scss b/app/assets/stylesheets/pages/import.scss similarity index 100% rename from app/assets/stylesheets/sections/import.scss rename to app/assets/stylesheets/pages/import.scss diff --git a/app/assets/stylesheets/sections/issuable.scss b/app/assets/stylesheets/pages/issuable.scss similarity index 100% rename from app/assets/stylesheets/sections/issuable.scss rename to app/assets/stylesheets/pages/issuable.scss diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/pages/issues.scss similarity index 100% rename from app/assets/stylesheets/sections/issues.scss rename to app/assets/stylesheets/pages/issues.scss diff --git a/app/assets/stylesheets/sections/labels.scss b/app/assets/stylesheets/pages/labels.scss similarity index 100% rename from app/assets/stylesheets/sections/labels.scss rename to app/assets/stylesheets/pages/labels.scss diff --git a/app/assets/stylesheets/sections/login.scss b/app/assets/stylesheets/pages/login.scss similarity index 100% rename from app/assets/stylesheets/sections/login.scss rename to app/assets/stylesheets/pages/login.scss diff --git a/app/assets/stylesheets/sections/markdown_area.scss b/app/assets/stylesheets/pages/markdown_area.scss similarity index 100% rename from app/assets/stylesheets/sections/markdown_area.scss rename to app/assets/stylesheets/pages/markdown_area.scss diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss similarity index 100% rename from app/assets/stylesheets/sections/merge_requests.scss rename to app/assets/stylesheets/pages/merge_requests.scss diff --git a/app/assets/stylesheets/sections/milestone.scss b/app/assets/stylesheets/pages/milestone.scss similarity index 100% rename from app/assets/stylesheets/sections/milestone.scss rename to app/assets/stylesheets/pages/milestone.scss diff --git a/app/assets/stylesheets/sections/note_form.scss b/app/assets/stylesheets/pages/note_form.scss similarity index 100% rename from app/assets/stylesheets/sections/note_form.scss rename to app/assets/stylesheets/pages/note_form.scss diff --git a/app/assets/stylesheets/sections/notes.scss b/app/assets/stylesheets/pages/notes.scss similarity index 100% rename from app/assets/stylesheets/sections/notes.scss rename to app/assets/stylesheets/pages/notes.scss diff --git a/app/assets/stylesheets/sections/notifications.scss b/app/assets/stylesheets/pages/notifications.scss similarity index 100% rename from app/assets/stylesheets/sections/notifications.scss rename to app/assets/stylesheets/pages/notifications.scss diff --git a/app/assets/stylesheets/sections/profile.scss b/app/assets/stylesheets/pages/profile.scss similarity index 100% rename from app/assets/stylesheets/sections/profile.scss rename to app/assets/stylesheets/pages/profile.scss diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/pages/projects.scss similarity index 100% rename from app/assets/stylesheets/sections/projects.scss rename to app/assets/stylesheets/pages/projects.scss diff --git a/app/assets/stylesheets/sections/search.scss b/app/assets/stylesheets/pages/search.scss similarity index 100% rename from app/assets/stylesheets/sections/search.scss rename to app/assets/stylesheets/pages/search.scss diff --git a/app/assets/stylesheets/sections/snippets.scss b/app/assets/stylesheets/pages/snippets.scss similarity index 100% rename from app/assets/stylesheets/sections/snippets.scss rename to app/assets/stylesheets/pages/snippets.scss diff --git a/app/assets/stylesheets/sections/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss similarity index 100% rename from app/assets/stylesheets/sections/stat_graph.scss rename to app/assets/stylesheets/pages/stat_graph.scss diff --git a/app/assets/stylesheets/sections/themes.scss b/app/assets/stylesheets/pages/themes.scss similarity index 100% rename from app/assets/stylesheets/sections/themes.scss rename to app/assets/stylesheets/pages/themes.scss diff --git a/app/assets/stylesheets/sections/tree.scss b/app/assets/stylesheets/pages/tree.scss similarity index 100% rename from app/assets/stylesheets/sections/tree.scss rename to app/assets/stylesheets/pages/tree.scss diff --git a/app/assets/stylesheets/sections/votes.scss b/app/assets/stylesheets/pages/votes.scss similarity index 100% rename from app/assets/stylesheets/sections/votes.scss rename to app/assets/stylesheets/pages/votes.scss diff --git a/app/assets/stylesheets/sections/wiki.scss b/app/assets/stylesheets/pages/wiki.scss similarity index 100% rename from app/assets/stylesheets/sections/wiki.scss rename to app/assets/stylesheets/pages/wiki.scss -- GitLab From 433b4c76fc7d23d03811fa05e1589e3a8a788e82 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 7 Mar 2015 00:28:53 -0800 Subject: [PATCH 1217/1609] Apply some styles from flatly theme --- app/assets/stylesheets/application.scss | 2 +- app/assets/stylesheets/gl_variables.scss | 49 +++++++++++----------- app/assets/stylesheets/main/variables.scss | 5 +++ app/assets/stylesheets/pages/projects.scss | 14 ------- app/views/projects/new.html.haml | 4 +- app/views/projects/show.html.haml | 2 +- app/views/shared/_no_ssh.html.haml | 6 +-- 7 files changed, 37 insertions(+), 45 deletions(-) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e5bb5e21bb0..2f9f09b4c6f 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -39,7 +39,7 @@ * Page specific styles (issues, projects etc): */ -@import "sections/*"; +@import "pages/*"; /** * Code highlight diff --git a/app/assets/stylesheets/gl_variables.scss b/app/assets/stylesheets/gl_variables.scss index 4f54551a22c..090eff7f29b 100644 --- a/app/assets/stylesheets/gl_variables.scss +++ b/app/assets/stylesheets/gl_variables.scss @@ -444,21 +444,21 @@ $font-size-base: $gl-font-size; // //## -// $pagination-color: $link-color -// $pagination-bg: #fff -// $pagination-border: #ddd +$pagination-color: #fff; +$pagination-bg: $brand-success; +$pagination-border: transparent; -// $pagination-hover-color: $link-hover-color -// $pagination-hover-bg: $gray-lighter -// $pagination-hover-border: #ddd +$pagination-hover-color: #fff; +$pagination-hover-bg: darken($brand-success, 15%); +$pagination-hover-border: transparent; -// $pagination-active-color: #fff -// $pagination-active-bg: $brand-primary -// $pagination-active-border: $brand-primary +$pagination-active-color: #fff; +$pagination-active-bg: darken($brand-success, 15%); +$pagination-active-border: transparent; -// $pagination-disabled-color: $gray-light -// $pagination-disabled-bg: #fff -// $pagination-disabled-border: #ddd +$pagination-disabled-color: #b4bcc2; +$pagination-disabled-bg: lighten($brand-success, 15%); +$pagination-disabled-border: transparent; //== Pager @@ -492,21 +492,22 @@ $font-size-base: $gl-font-size; // //## Define colors for form feedback states and, by default, alerts. -// $state-success-text: #3c763d -// $state-success-bg: #dff0d8 -// $state-success-border: darken(adjust-hue($state-success-bg, -10), 5%) -// $state-info-text: #31708f -// $state-info-bg: #d9edf7 -// $state-info-border: darken(adjust-hue($state-info-bg, -10), 7%) +$state-success-text: #fff; +$state-success-bg: $brand-success; +$state-success-border: $brand-success; -// $state-warning-text: #8a6d3b -// $state-warning-bg: #fcf8e3 -// $state-warning-border: darken(adjust-hue($state-warning-bg, -10), 5%) +$state-info-text: #fff; +$state-info-bg: $brand-info; +$state-info-border: $brand-info; -// $state-danger-text: #a94442 -// $state-danger-bg: #f2dede -// $state-danger-border: darken(adjust-hue($state-danger-bg, -10), 5%) +$state-warning-text: #fff; +$state-warning-bg: $brand-warning; +$state-warning-border: $brand-warning; + +$state-danger-text: #fff; +$state-danger-bg: $brand-danger; +$state-danger-border: $brand-danger; //== Tooltips diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss index d751678f1b3..30e084ecd62 100644 --- a/app/assets/stylesheets/main/variables.scss +++ b/app/assets/stylesheets/main/variables.scss @@ -19,6 +19,11 @@ $gl-primary: #446e9b; $gl-info: #029ACF; $gl-warning: #EB9532; +$gl-primary: #2C3E50; +$gl-success: #18BC9C; +$gl-info: #3498DB; +$gl-warning: #F39C12; +$gl-danger: #E74C3C; /* * Commit Diff Colors */ diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 3a912d234fa..98ce4150ff2 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -101,23 +101,9 @@ .btn, .form-control { - border: 1px solid #E1E1E1; - box-shadow: none; padding: 6px 9px; } - .btn { - background: none; - color: $gl-link-color; - - &.active { - background-color: #f5f5f5; - border: 1px solid rgba(0,0,0,0.195); - color: #333; - font-weight: bold; - } - } - .form-control { cursor: auto; @extend .monospace; diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 5daf8470d88..00b912742b2 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -34,7 +34,7 @@ %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .alert.alert-info + .alert.alert-info.prepend-top-10 This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. @@ -65,7 +65,7 @@ %i.fa.fa-bitbucket Import projects from Bitbucket = render 'bitbucket_import_modal' - + - unless request.host == 'gitlab.com' .project-import.form-group .col-sm-2 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 787cfd9304f..74b07395650 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -40,7 +40,7 @@ %p Repository is read-only - if @project.forked_from_project - .alert.alert-success + .well %i.fa.fa-code-fork.project-fork-icon Forked from: %br diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index 1a2946baccb..089179e677a 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? .no-ssh-key-message.alert.alert-warning.hidden-xs - You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path} to your profile + You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', new_profile_key_path, class: 'alert-link'} to your profile .pull-right - = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put + = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' | - = link_to 'Remind later', '#', class: 'hide-no-ssh-message' + = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' -- GitLab From 4f3d704845c1ead8d39f49b04812305a8f8ab93a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 7 Mar 2015 00:29:25 -0800 Subject: [PATCH 1218/1609] Move mixins and bootstrap css to base directory --- app/assets/stylesheets/application.scss | 9 +++++++-- app/assets/stylesheets/{ => base}/gl_bootstrap.scss | 1 - app/assets/stylesheets/{ => base}/gl_variables.scss | 0 app/assets/stylesheets/{main => base}/layout.scss | 0 app/assets/stylesheets/{main => base}/mixins.scss | 0 app/assets/stylesheets/{main => base}/variables.scss | 0 6 files changed, 7 insertions(+), 3 deletions(-) rename app/assets/stylesheets/{ => base}/gl_bootstrap.scss (99%) rename app/assets/stylesheets/{ => base}/gl_variables.scss (100%) rename app/assets/stylesheets/{main => base}/layout.scss (100%) rename app/assets/stylesheets/{main => base}/mixins.scss (100%) rename app/assets/stylesheets/{main => base}/variables.scss (100%) diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 2f9f09b4c6f..015ff2ce4ec 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,12 +11,17 @@ *= require cal-heatmap */ -@import "main/*"; + +@import "base/variables"; +@import "base/mixins"; +@import "base/layout"; + /** * Customized Twitter bootstrap */ -@import 'gl_bootstrap'; +@import 'base/gl_variables'; +@import 'base/gl_bootstrap'; /** * NProgress load bar css diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss similarity index 99% rename from app/assets/stylesheets/gl_bootstrap.scss rename to app/assets/stylesheets/base/gl_bootstrap.scss index e8e58511f3b..b0e2a678fc3 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -2,7 +2,6 @@ * Twitter bootstrap with GitLab customizations/additions * */ -@import "gl_variables"; // Core variables and mixins @import "bootstrap/variables"; diff --git a/app/assets/stylesheets/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss similarity index 100% rename from app/assets/stylesheets/gl_variables.scss rename to app/assets/stylesheets/base/gl_variables.scss diff --git a/app/assets/stylesheets/main/layout.scss b/app/assets/stylesheets/base/layout.scss similarity index 100% rename from app/assets/stylesheets/main/layout.scss rename to app/assets/stylesheets/base/layout.scss diff --git a/app/assets/stylesheets/main/mixins.scss b/app/assets/stylesheets/base/mixins.scss similarity index 100% rename from app/assets/stylesheets/main/mixins.scss rename to app/assets/stylesheets/base/mixins.scss diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/base/variables.scss similarity index 100% rename from app/assets/stylesheets/main/variables.scss rename to app/assets/stylesheets/base/variables.scss -- GitLab From e2b8025abe929007fb82e952b0be954a7598e528 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 7 Mar 2015 01:06:16 -0800 Subject: [PATCH 1219/1609] Increase input padding and fit issue form in laptop screen --- app/assets/stylesheets/base/gl_bootstrap.scss | 8 ++++++++ app/assets/stylesheets/base/gl_variables.scss | 4 ++-- app/assets/stylesheets/generic/gfm.scss | 2 +- app/assets/stylesheets/generic/selects.scss | 4 ++-- app/assets/stylesheets/pages/projects.scss | 5 ----- app/views/projects/_issuable_form.html.haml | 2 ++ app/views/projects/blob/_blob.html.haml | 2 +- 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index b0e2a678fc3..d1dba6d66b5 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -189,3 +189,11 @@ } } } + +.alert { + a { + @extend .alert-link; + color: #fff; + text-decoration: underline; + } +} diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index 090eff7f29b..2aa57e46a0a 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -93,8 +93,8 @@ $font-size-base: $gl-font-size; // //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). -// $padding-base-vertical: 6px -// $padding-base-horizontal: 12px +$padding-base-vertical: 8px; +$padding-base-horizontal: 14px; // $padding-large-vertical: 10px // $padding-large-horizontal: 16px diff --git a/app/assets/stylesheets/generic/gfm.scss b/app/assets/stylesheets/generic/gfm.scss index 1427b6a5ae4..617d91154db 100644 --- a/app/assets/stylesheets/generic/gfm.scss +++ b/app/assets/stylesheets/generic/gfm.scss @@ -3,7 +3,7 @@ */ .issue-form, .merge-request-form, .wiki-form { .description { - height: 20em; + height: 18em; border-top-left-radius: 0; } } diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index 2a2f9e8eb5a..41ed736bbe5 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -3,7 +3,7 @@ .select2-choice { background: #FFF; border-color: #BBB; - padding: 6px 12px; + padding: 8px 14px; font-size: 13px; line-height: 18px; height: auto; @@ -20,7 +20,7 @@ } .select2-container-multi .select2-choices .select2-search-field input { - padding: 6px 12px; + padding: 8px 14px; font-size: 13px; line-height: 18px; height: auto; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 98ce4150ff2..bfd05973d75 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -99,11 +99,6 @@ margin-right: 45px; } - .btn, - .form-control { - padding: 6px 9px; - } - .form-control { cursor: auto; @extend .monospace; diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index bfacab5e48e..a7cd129b631 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -50,6 +50,7 @@ = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, { class: 'select2' }) - else + .prepend-top-10 %span.light No open milestones available.   - if can? current_user, :admin_milestone, issuable.project @@ -63,6 +64,7 @@ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, { selected: issuable.label_ids }, multiple: true, class: 'select2' - else + .prepend-top-10 %span.light No labels yet.   - if can? current_user, :admin_label, issuable.project diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 05b1f5e841c..9ff61f3887f 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -15,7 +15,7 @@ - else = link_to title, '#' -%ul.blob-commit-info.alert.alert-info.hidden-xs +%ul.blob-commit-info.well.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project -- GitLab From bf02072a86d4b0d06c246ecbea4f980523983941 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 12:50:35 +0100 Subject: [PATCH 1220/1609] Properly handle autosave local storage exceptions. --- CHANGELOG | 1 + app/assets/javascripts/autosave.js.coffee | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 37aee53bc0a..f6079092f55 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 7.9.0 (unreleased) - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options - Add grouped milestones from all projects to dashboard. + - Properly handle autosave local storage exceptions. v 7.8.1 - Fix run of custom post receive hooks diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee index 3450f4b55f7..5d3fe81da74 100644 --- a/app/assets/javascripts/autosave.js.coffee +++ b/app/assets/javascripts/autosave.js.coffee @@ -14,7 +14,11 @@ class @Autosave restore: -> return unless window.localStorage? - text = window.localStorage.getItem @key + try + text = window.localStorage.getItem @key + catch + return + @field.val text if text?.length > 0 @field.trigger "input" @@ -23,11 +27,13 @@ class @Autosave text = @field.val() if text?.length > 0 - window.localStorage.setItem @key, text + try + window.localStorage.setItem @key, text else @reset() reset: -> return unless window.localStorage? - window.localStorage.removeItem @key \ No newline at end of file + try + window.localStorage.removeItem @key -- GitLab From be3edeb6a1db341e5a10f9031c79f5fbc25e6f59 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 7 Mar 2015 10:35:17 -0800 Subject: [PATCH 1221/1609] Small UI improvements after css refactoring --- app/assets/stylesheets/base/gl_bootstrap.scss | 2 +- app/assets/stylesheets/generic/buttons.scss | 7 ------- app/assets/stylesheets/generic/gfm.scss | 2 +- app/assets/stylesheets/generic/selects.scss | 3 ++- app/assets/stylesheets/pages/issues.scss | 2 +- app/views/admin/projects/index.html.haml | 4 ++-- app/views/admin/users/index.html.haml | 4 ++-- app/views/groups/projects.html.haml | 2 +- app/views/profiles/show.html.haml | 8 +++++--- 9 files changed, 15 insertions(+), 19 deletions(-) diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index d1dba6d66b5..0775c171817 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -157,7 +157,7 @@ .panel-head-actions { position: relative; - top: -7px; + top: -6px; float: right; } } diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index 7cc9782f539..0224484d82b 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -6,12 +6,10 @@ } &.btn-create { - @extend .wide; @extend .btn-success; } &.btn-save { - @extend .wide; @extend .btn-primary; } @@ -23,11 +21,6 @@ float: right; } - &.wide { - padding-left: 20px; - padding-right: 20px; - } - &.btn-small { padding: 2px 10px; font-size: 12px; diff --git a/app/assets/stylesheets/generic/gfm.scss b/app/assets/stylesheets/generic/gfm.scss index 617d91154db..8fac5e534fa 100644 --- a/app/assets/stylesheets/generic/gfm.scss +++ b/app/assets/stylesheets/generic/gfm.scss @@ -3,7 +3,7 @@ */ .issue-form, .merge-request-form, .wiki-form { .description { - height: 18em; + height: 16em; border-top-left-radius: 0; } } diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index 41ed736bbe5..2773ee11fd4 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -3,10 +3,11 @@ .select2-choice { background: #FFF; border-color: #BBB; - padding: 8px 14px; + padding: 6px 14px; font-size: 13px; line-height: 18px; height: auto; + margin: 2px 0; .select2-arrow { background: #FFF; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index b909725bff5..46522e9ece9 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -40,7 +40,7 @@ } .check-all-holder { - height: 32px; + height: 36px; float: left; margin-right: 12px; padding: 6px 15px; diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index b984188eb9d..3a1e61d5d8d 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -44,7 +44,7 @@ Projects (#{@projects.total_count}) .panel-head-actions .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} %span.light sort: - if @sort.present? = sort_options_hash[@sort] @@ -63,7 +63,7 @@ = sort_title_oldest_updated = link_to admin_namespaces_projects_path(sort: sort_value_largest_repo) do = sort_title_largest_repo - = link_to 'New Project', new_project_path, class: "btn btn-new" + = link_to 'New Project', new_project_path, class: "btn btn-sm btn-success" %ul.well-list - @projects.each do |project| %li diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 4a4f0549ada..35e9fd5154f 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -35,7 +35,7 @@ Users (#{@users.total_count}) .panel-head-actions .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"} %span.light sort: - if @sort.present? = sort_options_hash[@sort] @@ -59,7 +59,7 @@ = link_to admin_users_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated - = link_to 'New User', new_admin_user_path, class: "btn btn-new" + = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm" %ul.well-list - @users.each do |user| %li diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 8c829654fb0..c95347b3a55 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -4,7 +4,7 @@ projects: - if can? current_user, :manage_group, @group .panel-head-actions - = link_to new_project_path(namespace_id: @group.id), class: "btn btn-new" do + = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do %i.fa.fa-plus New Project %ul.well-list diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 459361a0d5b..1a7bc353bf3 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -56,7 +56,7 @@ .form-group = f.label :bio, class: "control-label" .col-sm-10 - = f.text_area :bio, rows: 6, class: "form-control", maxlength: 250 + = f.text_area :bio, rows: 4, class: "form-control", maxlength: 250 %span.help-block Tell us about yourself in fewer than 250 characters. .col-md-5 @@ -94,5 +94,7 @@ %p Your profile is publicly visible because you joined public project(s) - .form-actions - = f.submit 'Save changes', class: "btn btn-save" + .row + .col-md-7 + .col-sm-2 + = f.submit 'Save changes', class: "btn btn-success" -- GitLab From cacac147de2b317d02788c5da1cdc6010f00a340 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 1 Mar 2015 08:06:46 -0700 Subject: [PATCH 1222/1609] Move restricted visibility settings to the UI Add checkboxes to the application settings page for restricted visibility levels, and remove those settings from gitlab.yml. --- CHANGELOG | 1 + app/assets/stylesheets/generic/forms.scss | 5 +++ .../admin/application_settings_controller.rb | 10 +++++- app/helpers/application_settings_helper.rb | 16 +++++++++ app/helpers/visibility_level_helper.rb | 5 +-- app/models/application_setting.rb | 36 +++++++++++++------ app/models/project.rb | 6 ++-- app/services/base_service.rb | 4 --- app/services/update_snippet_service.rb | 22 ++++++++++++ .../application_settings/_form.html.haml | 8 +++++ config/gitlab.yml.example | 4 --- ...sibility_levels_to_application_settings.rb | 5 +++ db/schema.rb | 7 ++-- lib/gitlab/current_settings.rb | 4 +-- lib/gitlab/visibility_level.rb | 20 ++++++----- spec/models/application_setting_spec.rb | 24 +++++++------ 16 files changed, 128 insertions(+), 49 deletions(-) create mode 100644 app/services/update_snippet_service.rb create mode 100644 db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb diff --git a/CHANGELOG b/CHANGELOG index 08e66c7dc6d..b26e83d7269 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 7.9.0 (unreleased) - Improve error messages for file edit failures - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. + - Move restricted visibility settings from gitlab.yml into the web UI. - Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev) - Save web edit in new branch - Fix ordering of imported but unchanged projects (Marco Wessel) diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index c8982cdc00d..79231638a27 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -97,3 +97,8 @@ label { .wiki-content { margin-top: 35px; } + +.btn-group .btn.active { + text-shadow: 0 0 0.2em #D9534F, 0 0 0.2em #D9534F, 0 0 0.2em #D9534F; + background-color: #5487bf; +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 2b0c500e97a..8f7d5e8006f 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -20,6 +20,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def application_setting_params + restricted_levels = params[:application_setting][:restricted_visibility_levels] + unless restricted_levels.nil? + restricted_levels.map! do |level| + level.to_i + end + end + params.require(:application_setting).permit( :default_projects_limit, :default_branch_protection, @@ -28,7 +35,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :gravatar_enabled, :twitter_sharing_enabled, :sign_in_text, - :home_page_url + :home_page_url, + restricted_visibility_levels: [] ) end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 1ee086da997..2b0d8860f9b 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -18,4 +18,20 @@ module ApplicationSettingsHelper def extra_sign_in_text current_application_settings.sign_in_text end + + # Return a group of checkboxes that use Bootstrap's button plugin for a + # toggle button effect. + def restricted_level_checkboxes(help_block_id) + Gitlab::VisibilityLevel.options.map do |name, level| + checked = restricted_visibility_levels(true).include?(level) + css_class = 'btn btn-primary' + css_class += ' active' if checked + checkbox_name = 'application_setting[restricted_visibility_levels][]' + + label_tag(checkbox_name, class: css_class) do + check_box_tag(checkbox_name, level, checked, autocomplete: 'off', + 'aria-describedby' => help_block_id) + name + end + end + end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index deb9c8b4d49..7c090dc594c 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -60,7 +60,8 @@ module VisibilityLevelHelper Project.visibility_levels.key(level) end - def restricted_visibility_levels - current_user.is_admin? ? [] : gitlab_config.restricted_visibility_levels + def restricted_visibility_levels(show_all = false) + return [] if current_user.is_admin? && !show_all + current_application_settings.restricted_visibility_levels end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 588668b3d1e..6abdf0c755a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -2,25 +2,38 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) +# id :integer not null, primary key +# default_projects_limit :integer +# default_branch_protection :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# twitter_sharing_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) +# restricted_visibility_levels :text # class ApplicationSetting < ActiveRecord::Base + serialize :restricted_visibility_levels + validates :home_page_url, allow_blank: true, format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }, if: :home_page_url_column_exist + validates_each :restricted_visibility_levels do |record, attr, value| + value.each do |level| + unless Gitlab::VisibilityLevel.options.has_value?(level) + record.errors.add(attr, "'#{level}' is not a valid visibility level") + end + end + end + def self.current ApplicationSetting.last end @@ -34,6 +47,7 @@ class ApplicationSetting < ActiveRecord::Base twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], + restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'] ) end diff --git a/app/models/project.rb b/app/models/project.rb index c45338bf4eb..16b68453f5c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -34,6 +34,8 @@ require 'file_size_validator' class Project < ActiveRecord::Base include Sortable + include Gitlab::CurrentSettings + extend Gitlab::CurrentSettings include Gitlab::ShellAdapter include Gitlab::VisibilityLevel include Gitlab::ConfigHelper @@ -132,8 +134,8 @@ class Project < ActiveRecord::Base validates :issues_enabled, :merge_requests_enabled, :wiki_enabled, inclusion: { in: [true, false] } validates :visibility_level, - exclusion: { in: gitlab_config.restricted_visibility_levels }, - if: -> { gitlab_config.restricted_visibility_levels.any? } + exclusion: { in: current_application_settings.restricted_visibility_levels }, + if: -> { current_application_settings.restricted_visibility_levels.any? } validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true validates :namespace, presence: true validates_uniqueness_of :name, scope: :namespace_id diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 52ab29f1492..8b07d7a4361 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -31,10 +31,6 @@ class BaseService SystemHooksService.new end - def current_application_settings - ApplicationSetting.current - end - private def error(message, http_status = nil) diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb new file mode 100644 index 00000000000..b7a719f2526 --- /dev/null +++ b/app/services/update_snippet_service.rb @@ -0,0 +1,22 @@ +class UpdateSnippetService < BaseService + attr_accessor :snippet + + def initialize(project = nil, user, snippet, params = {}) + super(project, user, params) + @snippet = snippet + end + + def execute + # check that user is allowed to set specified visibility_level + new_visibility = params[:visibility_level] + if new_visibility && new_visibility != snippet.visibility_level + unless can?(current_user, :change_visibility_level, snippet) && + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + deny_visibility_level(snippet, new_visibility_level) + return snippet + end + end + + snippet.update_attributes(params) + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index ac64d26f9aa..da147605a88 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -34,6 +34,14 @@ = f.label :default_branch_protection, class: 'control-label col-sm-2' .col-sm-10 = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' + .form-group + = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - restricted_level_checkboxes('restricted-visibility-help').each do |level| + = level + %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets .form-group = f.label :home_page_url, class: 'control-label col-sm-2' .col-sm-10 diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 6dff07cf9df..dcd26c2317b 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -56,10 +56,6 @@ production: &base ## COLOR = 5 # default_theme: 2 # default: 2 - # Restrict setting visibility levels for non-admin users. - # The default is to allow all levels. - # restricted_visibility_levels: [ "public" ] - ## Automatic issue closing # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. # This happens when the commit is pushed or merged into the default branch of a project. diff --git a/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb new file mode 100644 index 00000000000..494c3033bff --- /dev/null +++ b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddRestrictedVisibilityLevelsToApplicationSettings < ActiveRecord::Migration + def change + add_column :application_settings, :restricted_visibility_levels, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index a686bb4b3cd..e539afdda41 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150225065047) do +ActiveRecord::Schema.define(version: 20150301014758) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -25,8 +25,9 @@ ActiveRecord::Schema.define(version: 20150225065047) do t.datetime "created_at" t.datetime "updated_at" t.string "home_page_url" - t.integer "default_branch_protection", default: 2 - t.boolean "twitter_sharing_enabled", default: true + t.integer "default_branch_protection", default: 2 + t.boolean "twitter_sharing_enabled", default: true + t.text "restricted_visibility_levels" end create_table "broadcast_messages", force: true do |t| diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 1a25eebe7d1..0ebebfa09c4 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -5,8 +5,7 @@ module Gitlab RequestStore.store[key] ||= begin if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('application_settings') - RequestStore.store[:current_application_settings] = - (ApplicationSetting.current || ApplicationSetting.create_from_defaults) + ApplicationSetting.current || ApplicationSetting.create_from_defaults else fake_application_settings end @@ -21,6 +20,7 @@ module Gitlab signin_enabled: Settings.gitlab['signin_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], + restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'] ) end end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index d0b6cde3c7e..1851e76067c 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -5,6 +5,8 @@ # module Gitlab module VisibilityLevel + extend CurrentSettings + PRIVATE = 0 unless const_defined?(:PRIVATE) INTERNAL = 10 unless const_defined?(:INTERNAL) PUBLIC = 20 unless const_defined?(:PUBLIC) @@ -23,21 +25,21 @@ module Gitlab end def allowed_for?(user, level) - user.is_admin? || allowed_level?(level) + user.is_admin? || allowed_level?(level.to_i) end - # Level can be a string `"public"` or a value `20`, first check if valid, - # then check if the corresponding string appears in the config + # Return true if the specified level is allowed for the current user. + # Level should be a numeric value, e.g. `20`. def allowed_level?(level) - if options.has_key?(level.to_s) - non_restricted_level?(level) - elsif options.has_value?(level.to_i) - non_restricted_level?(options.key(level.to_i).downcase) - end + valid_level?(level) && non_restricted_level?(level) end def non_restricted_level?(level) - ! Gitlab.config.gitlab.restricted_visibility_levels.include?(level) + ! current_application_settings.restricted_visibility_levels.include?(level) + end + + def valid_level?(level) + options.has_value?(level) end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index d1027f64d13..b4f0b2c201a 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -2,17 +2,19 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) +# id :integer not null, primary key +# default_projects_limit :integer +# default_branch_protection :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) +# restricted_visibility_levels :text # require 'spec_helper' -- GitLab From df13c9cac927af8d9b72d674b44c3d8017e1c846 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 12:05:15 -0700 Subject: [PATCH 1223/1609] Hide user page sidebar for mobilde devices --- app/views/users/show.html.haml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 5e82d5780cf..abd6b229782 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -1,5 +1,7 @@ .row - .col-md-8 + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left + %section.col-md-8 %h3.page-title = image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: '' = @user.name @@ -19,10 +21,11 @@ = render 'groups', groups: @groups %hr - .user-calendar - %h4.center.light - %i.fa.fa-spinner.fa-spin - %hr + .hidden-xs + .user-calendar + %h4.center.light + %i.fa.fa-spinner.fa-spin + %hr %h4 User Activity @@ -33,7 +36,7 @@ %i.fa.fa-rss = render @events - .col-md-4 + %aside.col-md-4 = render 'profile', user: @user = render 'projects' -- GitLab From 2e99d7c9157040612669993b54cbb236f199367a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 12:15:02 -0700 Subject: [PATCH 1224/1609] Prevent date overflow on issue page on mobile devices --- app/assets/stylesheets/generic/mobile.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index b3727c33672..1b0e056216f 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -43,8 +43,10 @@ } } - .page-title .new-issue-link { - display: none; + .page-title { + .note_created_ago, .new-issue-link { + display: none; + } } .issue_edited_ago, .note_edited_ago { -- GitLab From b5c3e1a43158314cc1c624cff6294546ee64b418 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 14:46:22 -0700 Subject: [PATCH 1225/1609] Add GitLab UI development kit --- app/assets/stylesheets/pages/ui_dev_kit.scss | 9 + app/controllers/help_controller.rb | 3 + app/views/help/ui.html.haml | 192 +++++++++++++++++++ config/routes.rb | 9 +- 4 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 app/assets/stylesheets/pages/ui_dev_kit.scss create mode 100644 app/views/help/ui.html.haml diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss new file mode 100644 index 00000000000..277afa1db9e --- /dev/null +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -0,0 +1,9 @@ +.gitlab-ui-dev-kit { + > h2 { + font-size: 27px; + border-bottom: 1px solid #CCC; + color: #666; + margin: 30px 0; + font-weight: bold; + } +} diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index fc498559d6b..c4d620d87b1 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -15,4 +15,7 @@ class HelpController < ApplicationController def shortcuts end + + def ui + end end diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml new file mode 100644 index 00000000000..cf6833c92c8 --- /dev/null +++ b/app/views/help/ui.html.haml @@ -0,0 +1,192 @@ +- lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare." + +.gitlab-ui-dev-kit + %h1 GitLab UI development kit + %p.light + Use page inspector in your browser to check element classes and structure + of examples below. + %hr + %ul + %li + = link_to 'Blocks', '#blocks' + %li + = link_to 'Lists', '#lists' + %li + = link_to 'Tables', '#tables' + %li + = link_to 'Buttons', '#buttons' + %li + = link_to 'Panels', '#panels' + %li + = link_to 'Alerts', '#alerts' + %li + = link_to 'Forms', '#forms' + + %h2#blocks Blocks + + %h3 + %code .well + + + .well + %h4 Something + = lorem + + + %h2#lists Lists + + %h3 + %code .well-list + %ul.well-list + %li + One item + %li + One item + %li + One item + + %h3 + %code .panel .well-list + + .panel.panel-default + .panel-heading My list + %ul.well-list + %li + One item + %li + One item + %li + One item + + %h3 + %code .bordered-list + %ul.bordered-list + %li + One item + %li + One item + %li + One item + + + + %h2#tables Tables + + .example + %table.table + %thead + %tr + %th # + %th First Name + %th Last Name + %th Username + %tbody + %tr + %td 1 + %td Mark + %td Otto + %td @mdo + %tr + %td 2 + %td Jacob + %td Thornton + %td @fat + %tr + %td 3 + %td Larry + %td the Bird + %td @twitter + + + %h2#buttons Buttons + + .example + %button.btn.btn-default{:type => "button"} Default + %button.btn.btn-primary{:type => "button"} Primary + %button.btn.btn-success{:type => "button"} Success + %button.btn.btn-info{:type => "button"} Info + %button.btn.btn-warning{:type => "button"} Warning + %button.btn.btn-danger{:type => "button"} Danger + %button.btn.btn-link{:type => "button"} Link + + %h2#panels Panels + + .row + .col-md-6 + .panel.panel-success + .panel-heading Success + .panel-body + = lorem + .panel.panel-primary + .panel-heading Primary + .panel-body + = lorem + .panel.panel-info + .panel-heading Info + .panel-body + = lorem + .col-md-6 + .panel.panel-warning + .panel-heading Warning + .panel-body + = lorem + .panel.panel-danger + .panel-heading Danger + .panel-body + = lorem + + %h2#alert Alerts + + .row + .col-md-6 + .alert.alert-success + = lorem + .alert.alert-primary + = lorem + .alert.alert-info + = lorem + .col-md-6 + .alert.alert-warning + = lorem + .alert.alert-danger + = lorem + + %h2#forms Forms + + %h3 + %code form.horizontal-form + + %form.form-horizontal + .form-group + %label.col-sm-2.control-label{:for => "inputEmail3"} Email + .col-sm-10 + %input#inputEmail3.form-control{:placeholder => "Email", :type => "email"}/ + .form-group + %label.col-sm-2.control-label{:for => "inputPassword3"} Password + .col-sm-10 + %input#inputPassword3.form-control{:placeholder => "Password", :type => "password"}/ + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + %label + %input{:type => "checkbox"}/ + Remember me + .form-group + .col-sm-offset-2.col-sm-10 + %button.btn.btn-default{:type => "submit"} Sign in + + %h3 + %code form + + %form + .form-group + %label{:for => "exampleInputEmail1"} Email address + %input#exampleInputEmail1.form-control{:placeholder => "Enter email", :type => "email"}/ + .form-group + %label{:for => "exampleInputPassword1"} Password + %input#exampleInputPassword1.form-control{:placeholder => "Password", :type => "password"}/ + .checkbox + %label + %input{:type => "checkbox"}/ + Remember me + %button.btn.btn-default{:type => "submit"} Sign in diff --git a/config/routes.rb b/config/routes.rb index 5348c86ea9d..6dd9ded0193 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,9 +7,8 @@ Gitlab::Application.routes.draw do authorized_applications: 'oauth/authorized_applications', authorizations: 'oauth/authorizations' end - # + # Search - # get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete @@ -33,13 +32,11 @@ Gitlab::Application.routes.draw do receive_pack: Gitlab.config.gitlab_shell.receive_pack }), at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post] - # # Help - # - get 'help' => 'help#index' get 'help/:category/:file' => 'help#show', as: :help_page get 'help/shortcuts' + get 'help/ui' => 'help#ui' # # Global snippets @@ -73,7 +70,7 @@ Gitlab::Application.routes.draw do get :callback get :jobs end - + resource :gitorious, only: [:create, :new], controller: :gitorious do get :status get :callback -- GitLab From 69d2e1d8291621174cfa397e5f85d882da421031 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 14:56:45 -0700 Subject: [PATCH 1226/1609] Add UI guide to GitLab development help --- doc/development/README.md | 1 + doc/development/ui_guide.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 doc/development/ui_guide.md diff --git a/doc/development/README.md b/doc/development/README.md index c31e5d7ae97..d5d264be19d 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -5,3 +5,4 @@ - [Rake tasks](rake_tasks.md) for development - [CI setup](ci_setup.md) for testing GitLab - [Sidekiq debugging](sidekiq_debugging.md) +- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md new file mode 100644 index 00000000000..2f01defc11d --- /dev/null +++ b/doc/development/ui_guide.md @@ -0,0 +1,12 @@ +# UI Guide for building GitLab + +## Best practices for creating new pages in GitLab + +TODO: write some best practices when develop GitLab features. + +## GitLab UI development kit + +We created a page inside GitLab where you can check commonly used html and css elements. + +When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples +you can use during GitLab development. -- GitLab From 285c5341855f8af6cbea5e964e3104a4698fa450 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 7 Mar 2015 11:23:43 -0700 Subject: [PATCH 1227/1609] Allow admins to override restricted visibility Allow admins to use restricted visibility levels when creating or updating projects. --- CHANGELOG | 1 + app/models/project.rb | 5 ---- app/services/projects/base_service.rb | 18 +++++++++++++ app/services/projects/create_service.rb | 11 +++++--- app/services/projects/update_service.rb | 11 +++++--- doc/public_access/public_access.md | 2 +- lib/api/helpers.rb | 2 +- lib/api/projects.rb | 6 ++--- spec/requests/api/projects_spec.rb | 26 ++++++++++++++++++ spec/services/projects/create_service_spec.rb | 27 +++++++++++++++++++ spec/services/projects/update_service_spec.rb | 6 ++--- 11 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 app/services/projects/base_service.rb diff --git a/CHANGELOG b/CHANGELOG index b26e83d7269..5e9f69f3e9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ v 7.9.0 (unreleased) - Improve error messages for file edit failures - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. + - Allow admins to override restricted project visibility settings. - Move restricted visibility settings from gitlab.yml into the web UI. - Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev) - Save web edit in new branch diff --git a/app/models/project.rb b/app/models/project.rb index 16b68453f5c..dae2b6425c4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -34,8 +34,6 @@ require 'file_size_validator' class Project < ActiveRecord::Base include Sortable - include Gitlab::CurrentSettings - extend Gitlab::CurrentSettings include Gitlab::ShellAdapter include Gitlab::VisibilityLevel include Gitlab::ConfigHelper @@ -133,9 +131,6 @@ class Project < ActiveRecord::Base message: Gitlab::Regex.path_regex_message } validates :issues_enabled, :merge_requests_enabled, :wiki_enabled, inclusion: { in: [true, false] } - validates :visibility_level, - exclusion: { in: current_application_settings.restricted_visibility_levels }, - if: -> { current_application_settings.restricted_visibility_levels.any? } validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true validates :namespace, presence: true validates_uniqueness_of :name, scope: :namespace_id diff --git a/app/services/projects/base_service.rb b/app/services/projects/base_service.rb new file mode 100644 index 00000000000..2a683e0d40a --- /dev/null +++ b/app/services/projects/base_service.rb @@ -0,0 +1,18 @@ +module Projects + class BaseService < ::BaseService + # Add an error to the project for restricted visibility levels + def deny_visibility_level(project, denied_visibility_level = nil) + denied_visibility_level ||= project.visibility_level + + level_name = 'Unknown' + Gitlab::VisibilityLevel.options.each do |name, level| + level_name = name if level == denied_visibility_level + end + + project.errors.add( + :visibility_level, + "#{level_name} visibility has been restricted by your GitLab administrator" + ) + end + end +end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 4fe790b98f1..5f166a9a30b 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -1,5 +1,5 @@ module Projects - class CreateService < BaseService + class CreateService < Projects::BaseService def initialize(user, params) @current_user, @params = user, params.dup end @@ -7,9 +7,12 @@ module Projects def execute @project = Project.new(params) - # Reset visibility level if is not allowed to set it - unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) - @project.visibility_level = default_features.visibility_level + # Make sure that the user is allowed to use the specified visibility + # level + unless Gitlab::VisibilityLevel.allowed_for?(current_user, + params[:visibility_level]) + deny_visibility_level(@project) + return @project end # Set project name from path diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 36877a61679..823afadc186 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -1,9 +1,14 @@ module Projects - class UpdateService < BaseService + class UpdateService < Projects::BaseService def execute # check that user is allowed to set specified visibility_level - unless can?(current_user, :change_visibility_level, project) && Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) - params[:visibility_level] = project.visibility_level + new_visibility = params[:visibility_level] + if new_visibility && new_visibility.to_i != project.visibility_level + unless can?(current_user, :change_visibility_level, project) && + Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) + deny_visibility_level(project, new_visibility) + return project + end end new_branch = params[:default_branch] diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index 4712c387021..7c5a6c04639 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -41,4 +41,4 @@ When visiting the public page of an user, you will only see listed projects whic ## Restricting the use of public or internal projects -In [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/dbd88d453b8e6c78a423fa7e692004b1db6ea069/config/gitlab.yml.example#L64) you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. +In [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/dbd88d453b8e6c78a423fa7e692004b1db6ea069/config/gitlab.yml.example#L64) you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. The restricted visibility settings do not apply to admin users. diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 228a719fbdf..f46dc8b456e 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -204,7 +204,7 @@ module API end def render_validation_error!(model) - unless model.valid? + if model.errors.any? render_api_error!(model.errors.messages || '400 Bad Request', 400) end end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 0677e85beab..83f65eec6cc 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -233,10 +233,10 @@ module API ::Projects::UpdateService.new(user_project, current_user, attrs).execute - if user_project.valid? - present user_project, with: Entities::Project - else + if user_project.errors.any? render_validation_error!(user_project) + else + present user_project, with: Entities::Project end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 0b3a47e3273..98b31a6e0af 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers + include Gitlab::CurrentSettings let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } @@ -202,6 +203,31 @@ describe API::API, api: true do expect(json_response['public']).to be_falsey expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) end + + context 'when a visibility level is restricted' do + before do + @project = attributes_for(:project, { public: true }) + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return([20]) + ) + end + + it 'should not allow a non-admin to use a restricted visibility level' do + post api('/projects', user), @project + expect(response.status).to eq(400) + expect(json_response['message']['visibility_level'].first).to( + match('restricted by your GitLab administrator') + ) + end + + it 'should allow an admin to override restricted visibility settings' do + post api('/projects', admin), @project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to( + eq(Gitlab::VisibilityLevel::PUBLIC) + ) + end + end end describe 'POST /projects/user/:id' do diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 8bb48346202..337dae592dd 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -55,6 +55,33 @@ describe Projects::CreateService do it { expect(File.exists?(@path)).to be_falsey } end end + + context 'restricted visibility level' do + before do + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return([20]) + ) + + @opts.merge!( + visibility_level: Gitlab::VisibilityLevel.options['Public'] + ) + end + + it 'should not allow a restricted visibility level for non-admins' do + project = create_project(@user, @opts) + expect(project).to respond_to(:errors) + expect(project.errors.messages).to have_key(:visibility_level) + expect(project.errors.messages[:visibility_level].first).to( + match('restricted by your GitLab administrator') + ) + end + + it 'should allow a restricted visibility level for admins' do + project = create_project(@admin, @opts) + expect(project.errors.any?).to be(false) + expect(project.saved?).to be(true) + end + end end def create_project(user, opts) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 10dbc548e86..ea5b8813105 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -47,9 +47,9 @@ describe Projects::UpdateService do context 'respect configured visibility restrictions setting' do before(:each) do - @restrictions = double("restrictions") - allow(@restrictions).to receive(:restricted_visibility_levels) { [ "public" ] } - Settings.stub_chain(:gitlab).and_return(@restrictions) + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return([20]) + ) end context 'should be private when updated to private' do -- GitLab From 1b5ca280d80e537a16cb9d8c22b605181a66c91f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 15:21:28 -0700 Subject: [PATCH 1228/1609] Merge two css files with same name --- app/assets/stylesheets/generic/markdown_area.scss | 10 ++++++++++ app/assets/stylesheets/pages/markdown_area.scss | 9 --------- 2 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 app/assets/stylesheets/pages/markdown_area.scss diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss index 5a87cc6c612..22b7ce6d831 100644 --- a/app/assets/stylesheets/generic/markdown_area.scss +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -77,3 +77,13 @@ } } } + +.markdown-area { + background: #FFF; + border: 1px solid #ddd; + min-height: 100px; + padding: 5px; + font-size: 14px; + box-shadow: none; + width: 100%; +} diff --git a/app/assets/stylesheets/pages/markdown_area.scss b/app/assets/stylesheets/pages/markdown_area.scss deleted file mode 100644 index 8ee8eaa4ee7..00000000000 --- a/app/assets/stylesheets/pages/markdown_area.scss +++ /dev/null @@ -1,9 +0,0 @@ -.markdown-area { - background: #FFF; - border: 1px solid #ddd; - min-height: 100px; - padding: 5px; - font-size: 14px; - box-shadow: none; - width: 100%; -} -- GitLab From f3d78ee31a23b012e1aa6d149a9cab4d9d329d06 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 15:21:47 -0700 Subject: [PATCH 1229/1609] Add markdown info to UI dev kit page --- app/views/help/ui.html.haml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index cf6833c92c8..58de5b7c869 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -21,6 +21,8 @@ = link_to 'Alerts', '#alerts' %li = link_to 'Forms', '#forms' + %li + = link_to 'Markdown', '#markdown' %h2#blocks Blocks @@ -190,3 +192,17 @@ %input{:type => "checkbox"}/ Remember me %button.btn.btn-default{:type => "submit"} Sign in + + %h2#markdown Markdown + %h3 + %code .md or .wiki and others + + Markdown rendering has a bit different css and presented in next UI elements: + + %ul + %li comment + %li issue, merge request description + %li wiki page + %li help page + + You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown", "markdown")}. -- GitLab From a6bb345d585e8aa9ae80ef4f9745f104085537db Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 16:34:26 -0700 Subject: [PATCH 1230/1609] Add DevKit and development README links to CONTRIBUTING.md --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73a8f9eb49f..42b5ce22e32 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,6 +63,8 @@ Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint. +To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file. + ### Merge request guidelines If you can, please submit a merge request with the fix or improvements including tests. If you don't know how to fix the issue but can write a test that exposes the issue we will accept that as well. In general bug fixes that include a regression test are merged quickly while new features without proper tests are least likely to receive timely feedback. The workflow to make a merge request is as follows: -- GitLab From 928fc94c3d900069902b097d6464acee712a886c Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 7 Mar 2015 12:47:06 -0700 Subject: [PATCH 1231/1609] Enforce restricted visibilities for snippets Add new service classes to create and update project and personal snippets. These classes are responsible for enforcing restricted visibility settings for non-admin users. --- .../projects/snippets_controller.rb | 24 ++++++++----------- app/controllers/snippets_controller.rb | 18 +++++--------- app/helpers/gitlab_routing_helper.rb | 3 ++- app/services/base_service.rb | 15 ++++++++++++ app/services/create_snippet_service.rb | 20 ++++++++++++++++ app/services/projects/base_service.rb | 18 -------------- app/services/projects/create_service.rb | 2 +- app/services/projects/update_service.rb | 2 +- app/services/update_snippet_service.rb | 6 ++--- lib/api/project_snippets.rb | 22 ++++++++++------- lib/gitlab/url_builder.rb | 6 ++--- spec/requests/api/projects_spec.rb | 3 ++- 12 files changed, 76 insertions(+), 63 deletions(-) create mode 100644 app/services/create_snippet_service.rb delete mode 100644 app/services/projects/base_service.rb diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 6c250e4ffed..ed268400373 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -28,26 +28,22 @@ class Projects::SnippetsController < Projects::ApplicationController end def create - @snippet = @project.snippets.build(snippet_params) - @snippet.author = current_user - - if @snippet.save - redirect_to namespace_project_snippet_path(@project.namespace, @project, - @snippet) - else - respond_with(@snippet) - end + @snippet = CreateSnippetService.new(@project, current_user, + snippet_params).execute + respond_with(@snippet, + location: namespace_project_snippet_path(@project.namespace, + @project, @snippet)) end def edit end def update - if @snippet.update_attributes(snippet_params) - redirect_to namespace_project_snippet_path(@project.namespace, @project, @snippet) - else - respond_with(@snippet) - end + UpdateSnippetService.new(project, current_user, @snippet, + snippet_params).execute + respond_with(@snippet, + location: namespace_project_snippet_path(@project.namespace, + @project, @snippet)) end def show diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 6ac048e4b83..dc0a5554723 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -42,25 +42,19 @@ class SnippetsController < ApplicationController end def create - @snippet = PersonalSnippet.new(snippet_params) - @snippet.author = current_user + @snippet = CreateSnippetService.new(nil, current_user, + snippet_params).execute - if @snippet.save - redirect_to snippet_path(@snippet) - else - respond_with @snippet - end + respond_with @snippet.becomes(Snippet) end def edit end def update - if @snippet.update_attributes(snippet_params) - redirect_to snippet_path(@snippet) - else - respond_with @snippet - end + UpdateSnippetService.new(nil, current_user, @snippet, + snippet_params).execute + respond_with @snippet.becomes(Snippet) end def show diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 8518a47a3a0..b005cb8e417 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -45,7 +45,8 @@ module GitlabRoutingHelper namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) end - def snippet_url(entity, *args) + def project_snippet_url(entity, *args) namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) + end end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 8b07d7a4361..6d9ed345914 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -31,6 +31,21 @@ class BaseService SystemHooksService.new end + # Add an error to the specified model for restricted visibility levels + def deny_visibility_level(model, denied_visibility_level = nil) + denied_visibility_level ||= model.visibility_level + + level_name = 'Unknown' + Gitlab::VisibilityLevel.options.each do |name, level| + level_name = name if level == denied_visibility_level + end + + model.errors.add( + :visibility_level, + "#{level_name} visibility has been restricted by your GitLab administrator" + ) + end + private def error(message, http_status = nil) diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb new file mode 100644 index 00000000000..101a3df5eee --- /dev/null +++ b/app/services/create_snippet_service.rb @@ -0,0 +1,20 @@ +class CreateSnippetService < BaseService + def execute + if project.nil? + snippet = PersonalSnippet.new(params) + else + snippet = project.snippets.build(params) + end + + unless Gitlab::VisibilityLevel.allowed_for?(current_user, + params[:visibility_level]) + deny_visibility_level(snippet) + return snippet + end + + snippet.author = current_user + + snippet.save + snippet + end +end diff --git a/app/services/projects/base_service.rb b/app/services/projects/base_service.rb deleted file mode 100644 index 2a683e0d40a..00000000000 --- a/app/services/projects/base_service.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Projects - class BaseService < ::BaseService - # Add an error to the project for restricted visibility levels - def deny_visibility_level(project, denied_visibility_level = nil) - denied_visibility_level ||= project.visibility_level - - level_name = 'Unknown' - Gitlab::VisibilityLevel.options.each do |name, level| - level_name = name if level == denied_visibility_level - end - - project.errors.add( - :visibility_level, - "#{level_name} visibility has been restricted by your GitLab administrator" - ) - end - end -end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 5f166a9a30b..7ffd0b3882a 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -1,5 +1,5 @@ module Projects - class CreateService < Projects::BaseService + class CreateService < BaseService def initialize(user, params) @current_user, @params = user, params.dup end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 823afadc186..69bdd045ddf 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -1,5 +1,5 @@ module Projects - class UpdateService < Projects::BaseService + class UpdateService < BaseService def execute # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb index b7a719f2526..9d181c2d2ab 100644 --- a/app/services/update_snippet_service.rb +++ b/app/services/update_snippet_service.rb @@ -1,7 +1,7 @@ class UpdateSnippetService < BaseService attr_accessor :snippet - def initialize(project = nil, user, snippet, params = {}) + def initialize(project, user, snippet, params) super(project, user, params) @snippet = snippet end @@ -9,10 +9,10 @@ class UpdateSnippetService < BaseService def execute # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] - if new_visibility && new_visibility != snippet.visibility_level + if new_visibility && new_visibility.to_i != snippet.visibility_level unless can?(current_user, :change_visibility_level, snippet) && Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) - deny_visibility_level(snippet, new_visibility_level) + deny_visibility_level(snippet, new_visibility) return snippet end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 0c2d282f785..25f34a3dab5 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -42,18 +42,19 @@ module API # title (required) - The title of a snippet # file_name (required) - The name of a snippet file # code (required) - The content of a snippet + # visibility_level (required) - The snippet's visibility # Example Request: # POST /projects/:id/snippets post ":id/snippets" do authorize! :write_project_snippet, user_project - required_attributes! [:title, :file_name, :code] + required_attributes! [:title, :file_name, :code, :visibility_level] - attrs = attributes_for_keys [:title, :file_name] + attrs = attributes_for_keys [:title, :file_name, :visibility_level] attrs[:content] = params[:code] if params[:code].present? - @snippet = user_project.snippets.new attrs - @snippet.author = current_user + @snippet = CreateSnippetservice.new(user_project, current_user, + attrs).execute - if @snippet.save + if @snippet.saved? present @snippet, with: Entities::ProjectSnippet else render_validation_error!(@snippet) @@ -68,19 +69,22 @@ module API # title (optional) - The title of a snippet # file_name (optional) - The name of a snippet file # code (optional) - The content of a snippet + # visibility_level (optional) - The snippet's visibility # Example Request: # PUT /projects/:id/snippets/:snippet_id put ":id/snippets/:snippet_id" do @snippet = user_project.snippets.find(params[:snippet_id]) authorize! :modify_project_snippet, @snippet - attrs = attributes_for_keys [:title, :file_name] + attrs = attributes_for_keys [:title, :file_name, :visibility_level] attrs[:content] = params[:code] if params[:code].present? - if @snippet.update_attributes attrs - present @snippet, with: Entities::ProjectSnippet - else + UpdateSnippetService.new(user_project, current_user, @snippet, + attrs).execute + if @snippet.errors.any? render_validation_error!(@snippet) + else + present @snippet, with: Entities::ProjectSnippet end end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 6830d15875a..11b0d44f340 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -51,9 +51,9 @@ module Gitlab anchor: "note_#{note.id}") elsif note.for_project_snippet? snippet = Snippet.find(note.noteable_id) - snippet_url(snippet, - host: Gitlab.config.gitlab['url'], - anchor: "note_#{note.id}") + project_snippet_url(snippet, + host: Gitlab.config.gitlab['url'], + anchor: "note_#{note.id}") end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 98b31a6e0af..f28dfea3ccf 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -425,7 +425,8 @@ describe API::API, api: true do describe 'POST /projects/:id/snippets' do it 'should create a new project snippet' do post api("/projects/#{project.id}/snippets", user), - title: 'api test', file_name: 'sample.rb', code: 'test' + title: 'api test', file_name: 'sample.rb', code: 'test', + visibility_level: '0' expect(response.status).to eq(201) expect(json_response['title']).to eq('api test') end -- GitLab From 9b3e156e43b8a14e4eb294a47bda6a477c8573b0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 17:03:30 -0700 Subject: [PATCH 1232/1609] Move profile groups page to dashboard --- CHANGELOG | 1 + .../{profiles => dashboard}/groups_controller.rb | 6 ++---- .../{profiles => dashboard}/groups/index.html.haml | 2 +- .../groups/group_members/_group_member.html.haml | 2 +- app/views/layouts/nav/_dashboard.html.haml | 5 +++++ app/views/layouts/nav/_profile.html.haml | 5 ----- config/routes.rb | 11 ++++++----- 7 files changed, 16 insertions(+), 16 deletions(-) rename app/controllers/{profiles => dashboard}/groups_controller.rb (71%) rename app/views/{profiles => dashboard}/groups/index.html.haml (83%) diff --git a/CHANGELOG b/CHANGELOG index b8f95994213..d88e45c0fc3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ v 7.9.0 (unreleased) - Condense commits already in target branch when updating merge request source branch. - Send notifications and leave system comments when bulk updating issues. - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) + - Move groups page from profile to dashboard v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/controllers/profiles/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb similarity index 71% rename from app/controllers/profiles/groups_controller.rb rename to app/controllers/dashboard/groups_controller.rb index ce9dd50df67..61d691e6368 100644 --- a/app/controllers/profiles/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,6 +1,4 @@ -class Profiles::GroupsController < ApplicationController - layout "profile" - +class Dashboard::GroupsController < ApplicationController def index @user_groups = current_user.group_members.page(params[:page]).per(20) end @@ -9,7 +7,7 @@ class Profiles::GroupsController < ApplicationController @users_group = group.group_members.where(user_id: current_user.id).first if can?(current_user, :destroy, @users_group) @users_group.destroy - redirect_to(profile_groups_path, info: "You left #{group.name} group.") + redirect_to(dashboard_groups_path, info: "You left #{group.name} group.") else return render_403 end diff --git a/app/views/profiles/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml similarity index 83% rename from app/views/profiles/groups/index.html.haml rename to app/views/dashboard/groups/index.html.haml index daf76636ff2..fd7bbb5500c 100644 --- a/app/views/profiles/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -23,7 +23,7 @@ Settings - if can?(current_user, :destroy, user_group) - = link_to leave_profile_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-small btn btn-grouped", title: 'Remove user from group' do + = link_to leave_dashboard_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-small btn btn-grouped", title: 'Remove user from group' do %i.fa.fa-sign-out Leave diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 21029c3a07d..30c3c2b00df 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -19,7 +19,7 @@ %i.fa.fa-pencil-square-o - if can?(current_user, :destroy, member) - if current_user == member.user - = link_to leave_profile_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = link_to leave_dashboard_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse - else = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 304744ba251..a22ddaf1cfd 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -9,6 +9,11 @@ %i.fa.fa-cube %span Projects + = nav_link(controller: :groups) do + = link_to dashboard_groups_path, title: 'Groups' do + %i.fa.fa-group + %span + Groups = nav_link(controller: :milestones) do = link_to dashboard_milestones_path, title: 'Milestones' do %i.fa.fa-clock-o diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 0914d2a167a..d88e862829d 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -43,11 +43,6 @@ %i.fa.fa-image %span Design - = nav_link(controller: :groups) do - = link_to profile_groups_path, title: 'Groups' do - %i.fa.fa-group - %span - Groups = nav_link(path: 'profiles#history') do = link_to history_profile_path, title: 'History' do %i.fa.fa-history diff --git a/config/routes.rb b/config/routes.rb index 6dd9ded0193..1b855cd7a32 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -193,11 +193,6 @@ Gitlab::Application.routes.draw do end resources :keys resources :emails, only: [:index, :create, :destroy] - resources :groups, only: [:index] do - member do - delete :leave - end - end resource :avatar, only: [:destroy] end end @@ -220,6 +215,12 @@ Gitlab::Application.routes.draw do scope module: :dashboard do resources :milestones, only: [:index, :show] + + resources :groups, only: [:index] do + member do + delete :leave + end + end end end -- GitLab From bb7be246f6928368d0bdadfb7a3258d610d48252 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 17:30:01 -0700 Subject: [PATCH 1233/1609] Show active users(non-blocked) on admin dashboard --- app/views/admin/dashboard/index.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 931b0c5c107..34e4e336261 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -32,9 +32,9 @@ %span.light.pull-right = Milestone.count %p - Users who signed in during last 30 days + Users %span.light.pull-right - = User.where("current_sign_in_at > ?", 30.days.ago).count + = User.count .col-md-4 %h4 Features @@ -91,10 +91,10 @@ = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 .light-well - %h4 Users + %h4 Active Users .data = link_to admin_users_path do - %h1= User.count + %h1= User.active.count %hr = link_to 'New User', new_admin_user_path, class: "btn btn-new" .col-sm-4 -- GitLab From 27e3b47b7f9ba6daaef61677c6743e99c3c6dc60 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 17:35:49 -0700 Subject: [PATCH 1234/1609] Fix user fixtures for development --- db/fixtures/development/05_users.rb | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb index b697f58d4ef..24952a1f661 100644 --- a/db/fixtures/development/05_users.rb +++ b/db/fixtures/development/05_users.rb @@ -1,30 +1,31 @@ Gitlab::Seeder.quiet do (2..20).each do |i| begin - User.seed(:id, [{ - id: i, + User.create!( username: Faker::Internet.user_name, name: Faker::Name.name, email: Faker::Internet.email, - confirmed_at: DateTime.now - }]) + confirmed_at: DateTime.now, + password: '12345678' + ) + print '.' - rescue ActiveRecord::RecordNotSaved + rescue ActiveRecord::RecordInvalid print 'F' end end (1..5).each do |i| begin - User.seed do |s| - s.username = "user#{i}" - s.name = "User #{i}" - s.email = "user#{i}@example.com" - s.confirmed_at = DateTime.now - s.password = '12345678' - end + User.create!( + username: "user#{i}", + name: "User #{i}", + email: "user#{i}@example.com", + confirmed_at: DateTime.now, + password: '12345678' + ) print '.' - rescue ActiveRecord::RecordNotSaved + rescue ActiveRecord::RecordInvalid print 'F' end end -- GitLab From 21aff22adb37af1fc16667e6691ed5346e72c9e1 Mon Sep 17 00:00:00 2001 From: Martin Bastien Date: Sun, 8 Mar 2015 22:26:46 -0400 Subject: [PATCH 1235/1609] Fix link to bitbucket import documentation Signed-off-by: Martin Bastien --- app/views/projects/_bitbucket_import_modal.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml index 5c52f91927d..07d4d602769 100644 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -10,4 +10,4 @@ you need to - else your GitLab administrator needs to - == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/butbucket.md'}. \ No newline at end of file + == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/bitbucket.md'}. -- GitLab From bb9560a35a7860e6de44c51b6d2300f3ee1e27ae Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 20:45:03 -0700 Subject: [PATCH 1236/1609] Show total user count on dashboard page --- app/views/admin/dashboard/index.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 34e4e336261..d1c586328a2 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -32,9 +32,9 @@ %span.light.pull-right = Milestone.count %p - Users + Active Users %span.light.pull-right - = User.count + = User.active.count .col-md-4 %h4 Features @@ -91,10 +91,10 @@ = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 .light-well - %h4 Active Users + %h4 Users .data = link_to admin_users_path do - %h1= User.active.count + %h1= User.count %hr = link_to 'New User', new_admin_user_path, class: "btn btn-new" .col-sm-4 -- GitLab From 7e4258777f8eed31cfe202bb96178823218f3b1c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 20:52:22 -0700 Subject: [PATCH 1237/1609] Move Profile groups tests to Dashboard group tests --- features/{profile => dashboard}/group.feature | 16 +-- .../steps/{profile => dashboard}/group.rb | 2 +- features/steps/shared/paths.rb | 16 +-- .../security/dashboard_access_spec.rb | 8 ++ spec/features/security/profile_access_spec.rb | 107 ++++++++---------- 5 files changed, 73 insertions(+), 76 deletions(-) rename features/{profile => dashboard}/group.feature (85%) rename features/steps/{profile => dashboard}/group.rb (95%) diff --git a/features/profile/group.feature b/features/dashboard/group.feature similarity index 85% rename from features/profile/group.feature rename to features/dashboard/group.feature index e2fbfde77be..0e4acb325b7 100644 --- a/features/profile/group.feature +++ b/features/dashboard/group.feature @@ -1,4 +1,4 @@ -@profile +@dashboard Feature: Profile Group Background: Given I sign in as "John Doe" @@ -10,18 +10,18 @@ Feature: Profile Group @javascript Scenario: Owner should be able to leave from group if he is not the last owner Given "Mary Jane" is owner of group "Owned" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list When I click on the "Leave" button for group "Owned" - And I visit profile groups page + And I visit dashboard groups page Then I should not see group "Owned" in group list Then I should see group "Guest" in group list @javascript Scenario: Owner should not be able to leave from group if he is the last owner Given "Mary Jane" is guest of group "Owned" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list Then I should not see the "Leave" button for group "Owned" @@ -29,20 +29,20 @@ Feature: Profile Group @javascript Scenario: Guest should be able to leave from group Given "Mary Jane" is guest of group "Guest" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list When I click on the "Leave" button for group "Guest" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should not see group "Guest" in group list @javascript Scenario: Guest should be able to leave from group even if he is the only user in the group - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should see group "Guest" in group list When I click on the "Leave" button for group "Guest" - When I visit profile groups page + When I visit dashboard groups page Then I should see group "Owned" in group list Then I should not see group "Guest" in group list diff --git a/features/steps/profile/group.rb b/features/steps/dashboard/group.rb similarity index 95% rename from features/steps/profile/group.rb rename to features/steps/dashboard/group.rb index 0a10e04e219..09d7717b67b 100644 --- a/features/steps/profile/group.rb +++ b/features/steps/dashboard/group.rb @@ -1,4 +1,4 @@ -class Spinach::Features::ProfileGroup < Spinach::FeatureSteps +class Spinach::Features::DashboardGroup < Spinach::FeatureSteps include SharedAuthentication include SharedGroup include SharedPaths diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 835b644e6c7..db6417bf951 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -87,6 +87,14 @@ module SharedPaths visit help_path end + step 'I visit dashboard groups page' do + visit dashboard_groups_path + end + + step 'I should be redirected to the dashboard groups page' do + current_path.should == dashboard_groups_path + end + # ---------------------------------------- # Profile # ---------------------------------------- @@ -119,14 +127,6 @@ module SharedPaths visit history_profile_path end - step 'I visit profile groups page' do - visit profile_groups_path - end - - step 'I should be redirected to the profile groups page' do - current_path.should == profile_groups_path - end - # ---------------------------------------- # Admin # ---------------------------------------- diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index d1f00a3dd82..3d2d8a3502c 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -52,4 +52,12 @@ describe "Dashboard access", feature: true do it { expect(new_group_path).to be_allowed_for :user } it { expect(new_group_path).to be_denied_for :visitor } end + + describe "GET /profile/groups" do + subject { dashboard_groups_path } + + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end end diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb index 5f254c42e58..2512a9c0e3d 100644 --- a/spec/features/security/profile_access_spec.rb +++ b/spec/features/security/profile_access_spec.rb @@ -1,76 +1,65 @@ require 'spec_helper' -describe "Users Security", feature: true do - describe "Project" do - before do - @u1 = create(:user) - end - - describe "GET /login" do - it { expect(new_user_session_path).not_to be_404_for :visitor } - end - - describe "GET /profile/keys" do - subject { profile_keys_path } +describe "Profile access", feature: true do + before do + @u1 = create(:user) + end - it { is_expected.to be_allowed_for @u1 } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end + describe "GET /login" do + it { expect(new_user_session_path).not_to be_404_for :visitor } + end - describe "GET /profile" do - subject { profile_path } + describe "GET /profile/keys" do + subject { profile_keys_path } - it { is_expected.to be_allowed_for @u1 } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/account" do - subject { profile_account_path } + describe "GET /profile" do + subject { profile_path } - it { is_expected.to be_allowed_for @u1 } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/design" do - subject { design_profile_path } + describe "GET /profile/account" do + subject { profile_account_path } - it { is_expected.to be_allowed_for @u1 } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/history" do - subject { history_profile_path } + describe "GET /profile/design" do + subject { design_profile_path } - it { is_expected.to be_allowed_for @u1 } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/notifications" do - subject { profile_notifications_path } + describe "GET /profile/history" do + subject { history_profile_path } - it { is_expected.to be_allowed_for @u1 } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } + end - describe "GET /profile/groups" do - subject { profile_groups_path } + describe "GET /profile/notifications" do + subject { profile_notifications_path } - it { is_expected.to be_allowed_for @u1 } - it { is_expected.to be_allowed_for :admin } - it { is_expected.to be_allowed_for :user } - it { is_expected.to be_denied_for :visitor } - end + it { is_expected.to be_allowed_for @u1 } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :visitor } end end -- GitLab From de11c13ac074baa2b63165a51c59f2925c0dff83 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 8 Mar 2015 22:39:37 -0700 Subject: [PATCH 1238/1609] Fix dashboard groups test --- features/dashboard/group.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature index 0e4acb325b7..92c1379ba77 100644 --- a/features/dashboard/group.feature +++ b/features/dashboard/group.feature @@ -1,5 +1,5 @@ @dashboard -Feature: Profile Group +Feature: Dashboard Group Background: Given I sign in as "John Doe" And "John Doe" is owner of group "Owned" -- GitLab From 224e104d8dc79e081fd897a1e52799dc1a0082bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Mon, 9 Mar 2015 12:52:45 +0100 Subject: [PATCH 1239/1609] fix mass SQL statements on initial push This commit disables process_commit_messages() for the initial push to the default branch. This fixes the mass SQL statements (~500000) that were executed during the initial push of the linux kernel for example. --- CHANGELOG | 1 + app/services/git_push_service.rb | 4 +++- spec/services/git_push_service_spec.rb | 9 --------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b8f95994213..588e645c69f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) + - Fix mass SQL statements on initial push (Hannes Rosenögger) - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) - Add comment notification events to HipChat and Slack services (Stan Hu) - Add issue and merge request events to HipChat and Slack services (Stan Hu) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 13def127763..4e1afea6d50 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -42,8 +42,10 @@ class GitPushService # as a heuristic. This may include more commits than are actually pushed, but # that shouldn't matter because we check for existing cross-references later. @push_commits = project.repository.commits_between(project.default_branch, newrev) + + # don't process commits for the initial push to the default branch + process_commit_messages(ref) end - process_commit_messages(ref) elsif push_to_existing_branch?(ref, oldrev) # Collect data for this git push @push_commits = project.repository.commits_between(oldrev, newrev) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index e264072b573..1b1e3ca5f8b 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -197,15 +197,6 @@ describe GitPushService do service.execute(project, user, @blankrev, @newrev, 'refs/heads/other') end - - it "finds references in the first push to a default branch" do - allow(project.repository).to receive(:commits_between).with(@blankrev, @newrev).and_return([]) - allow(project.repository).to receive(:commits).with(@newrev).and_return([commit]) - - expect(Note).to receive(:create_cross_reference_note).with(issue, commit, commit_author, project) - - service.execute(project, user, @blankrev, @newrev, 'refs/heads/master') - end end describe "closing issues from pushed commits" do -- GitLab From fc64b3f7b78fefde211caf56bb4f0b06edc0b045 Mon Sep 17 00:00:00 2001 From: Kuo-Cheng Yeu Date: Mon, 9 Mar 2015 23:20:44 +0800 Subject: [PATCH 1240/1609] remove duplicate right braces ('}') in configuration examples of GitHub, GitLab, and Google. --- doc/integration/github.md | 4 ++-- doc/integration/gitlab.md | 4 ++-- doc/integration/google.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/integration/github.md b/doc/integration/github.md index a9f1bc31bb4..b64501c2aaa 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -53,7 +53,7 @@ GitHub will generate an application ID and secret key for you to use. "app_id" => "YOUR_APP_ID", "app_secret" => "YOUR_APP_SECRET", "url" => "https://github.com/", - "args" => { "scope" => "user:email" } } + "args" => { "scope" => "user:email" } } ] ``` @@ -76,4 +76,4 @@ GitHub will generate an application ID and secret key for you to use. On the sign in page there should now be a GitHub icon below the regular sign in form. Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application. -If everything goes well the user will be returned to GitLab and will be signed in. \ No newline at end of file +If everything goes well the user will be returned to GitLab and will be signed in. diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index 49ffaa62af8..216f1f11a9b 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -58,7 +58,7 @@ GitLab.com will generate an application ID and secret key for you to use. "name" => "gitlab", "app_id" => "YOUR_APP_ID", "app_secret" => "YOUR_APP_SECRET", - "args" => { "scope" => "api" } } + "args" => { "scope" => "api" } } ] ``` @@ -81,4 +81,4 @@ GitLab.com will generate an application ID and secret key for you to use. On the sign in page there should now be a GitLab.com icon below the regular sign in form. Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application. -If everything goes well the user will be returned to your GitLab instance and will be signed in. \ No newline at end of file +If everything goes well the user will be returned to your GitLab instance and will be signed in. diff --git a/doc/integration/google.md b/doc/integration/google.md index d7b741ece69..e1c14c7c948 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -55,7 +55,7 @@ To enable the Google OAuth2 OmniAuth provider you must register your application "name" => "google_oauth2", "app_id" => "YOUR_APP_ID", "app_secret" => "YOUR_APP_SECRET", - "args" => { "access_type" => "offline", "approval_prompt" => '' } } + "args" => { "access_type" => "offline", "approval_prompt" => '' } } ] ``` -- GitLab From dd24c3d4b9ab153cd74e551772412122f8c643c1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 13:02:06 -0700 Subject: [PATCH 1241/1609] Improve user block/unblock UI in admin area --- app/controllers/admin/users_controller.rb | 4 +- app/views/admin/users/show.html.haml | 77 ++++++++++++----------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index ecedb31a7f8..693970e5349 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -24,7 +24,7 @@ class Admin::UsersController < Admin::ApplicationController def block if user.block - redirect_to :back, alert: "Successfully blocked" + redirect_to :back, notice: "Successfully blocked" else redirect_to :back, alert: "Error occurred. User was not blocked" end @@ -32,7 +32,7 @@ class Admin::UsersController < Admin::ApplicationController def unblock if user.activate - redirect_to :back, alert: "Successfully unblocked" + redirect_to :back, notice: "Successfully unblocked" else redirect_to :back, alert: "Error occurred. User was not unblocked" end diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 90267897503..bae9a97bf36 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -108,45 +108,50 @@ .col-md-6 - unless @user == current_user - if @user.blocked? - .alert.alert-info - %h4 This user is blocked - %p Blocking user has the following effects: - %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li User will be removed from joined projects and groups - %li Personal projects will be left - %li Owned groups will be left - %br - = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-new", data: { confirm: 'Are you sure?' } + .panel.panel-info + .panel-heading + This user is blocked + .panel-body + %p Blocking user has the following effects: + %ul + %li User will not be able to login + %li User will not be able to access git repositories + %li User will be removed from joined projects and groups + %li Personal projects will be left + %li Owned groups will be left + %br + = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - else - .alert.alert-warning - %h4 Block this user - %p Blocking user has the following effects: + .panel.panel-warning + .panel-heading + Block this user + .panel-body + %p Blocking user has the following effects: + %ul + %li User will not be able to login + %li User will not be able to access git repositories + %li User will be removed from joined projects and groups + %li Personal projects will be left + %li Owned groups will be left + %br + = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" + + .panel.panel-danger + .panel-heading + Remove user + .panel-body + %p Deleting a user has the following effects: %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li User will be removed from joined projects and groups - %li Personal projects will be left - %li Owned groups will be left + %li All user content like authored issues, snippets, comments will be removed + - rp = @user.personal_projects.count + - unless rp.zero? + %li #{pluralize rp, 'personal project'} will be removed and cannot be restored + - if @user.solo_owned_groups.present? + %li + Next groups with all content will be removed: + %strong #{@user.solo_owned_groups.map(&:name).join(', ')} %br - = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-remove" - - .alert.alert-danger - %h4 - Remove user - %p Deleting a user has the following effects: - %ul - %li All user content like authored issues, snippets, comments will be removed - - rp = @user.personal_projects.count - - unless rp.zero? - %li #{pluralize rp, 'personal project'} will be removed and cannot be restored - - if @user.solo_owned_groups.present? - %li - Next groups with all content will be removed: - %strong #{@user.solo_owned_groups.map(&:name).join(', ')} - %br - = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" + = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" #profile.tab-pane .row -- GitLab From d36ee3190aa1fb8c1238967a3049d5b8271c9030 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 14:12:03 -0700 Subject: [PATCH 1242/1609] Add starred projects page to dashboard --- CHANGELOG | 1 + app/assets/javascripts/dispatcher.js.coffee | 3 +++ .../dashboard/projects_controller.rb | 27 +++++++++++++++++++ app/views/dashboard/_sidebar.html.haml | 4 +-- .../dashboard/projects/starred.html.haml | 23 ++++++++++++++++ app/views/events/_events.html.haml | 2 +- app/views/layouts/nav/_dashboard.html.haml | 5 ++++ config/routes.rb | 6 +++++ 8 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 app/controllers/dashboard/projects_controller.rb create mode 100644 app/views/dashboard/projects/starred.html.haml diff --git a/CHANGELOG b/CHANGELOG index 0333b1dc50f..b210a6b0155 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,6 +33,7 @@ v 7.9.0 (unreleased) - Send notifications and leave system comments when bulk updating issues. - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) - Move groups page from profile to dashboard + - Starred projects page at dashboard v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index bf94fa3aaa0..928232e95bd 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -55,6 +55,9 @@ class Dispatcher when 'dashboard:show' new Dashboard() new Activities() + when 'dashboard:projects:starred' + new Activities() + new ProjectsList() when 'projects:commit:show' new Commit() new Diff() diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb new file mode 100644 index 00000000000..56e6fcc41ca --- /dev/null +++ b/app/controllers/dashboard/projects_controller.rb @@ -0,0 +1,27 @@ +class Dashboard::ProjectsController < ApplicationController + before_filter :event_filter + + def starred + @projects = current_user.starred_projects + @projects = @projects.includes(:namespace, :forked_from_project, :tags) + @projects = @projects.sort(@sort = params[:sort]) + @groups = [] + + respond_to do |format| + format.html + + format.json do + load_events + pager_json("events/_events", @events.count) + end + end + end + + private + + def load_events + @events = Event.in_projects(@projects.pluck(:id)) + @events = @event_filter.apply_filter(@events).with_associations + @events = @events.limit(20).offset(params[:offset] || 0) + end +end diff --git a/app/views/dashboard/_sidebar.html.haml b/app/views/dashboard/_sidebar.html.haml index a980f495427..983da4aba04 100644 --- a/app/views/dashboard/_sidebar.html.haml +++ b/app/views/dashboard/_sidebar.html.haml @@ -10,9 +10,9 @@ .tab-content .tab-pane.active#projects - = render "projects", projects: @projects + = render "dashboard/projects", projects: @projects .tab-pane#groups - = render "groups", groups: @groups + = render "dashboard/groups", groups: @groups .prepend-top-20 = render 'shared/promo' diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml new file mode 100644 index 00000000000..94de6092563 --- /dev/null +++ b/app/views/dashboard/projects/starred.html.haml @@ -0,0 +1,23 @@ +- if @projects.any? + .dashboard.row + %section.activities.col-md-8 + = render 'dashboard/activities' + %aside.col-md-4 + .panel.panel-default + .panel-heading.clearfix + .input-group + = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' + - if current_user.can_create_project? + .input-group-addon.dash-new-project + = link_to new_project_path do + %strong New project + + = render 'shared/projects_list', projects: @projects, + projects_limit: 20, stars: true, avatar: false + + = link_to '#aside', class: 'show-aside' do + %i.fa.fa-angle-left + +- else + %h3 You dont have starred projects yet + %p.slead Visit project page and press on star icon and it will appear on this page. diff --git a/app/views/events/_events.html.haml b/app/views/events/_events.html.haml index 3d62d478869..68c19df092d 100644 --- a/app/views/events/_events.html.haml +++ b/app/views/events/_events.html.haml @@ -1 +1 @@ -= render @events += render partial: 'events/event', collection: @events diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index a22ddaf1cfd..b21f25e87cf 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -9,6 +9,11 @@ %i.fa.fa-cube %span Projects + = nav_link(path: 'projects#starred') do + = link_to starred_dashboard_projects_path, title: 'Starred Projects' do + %i.fa.fa-star + %span + Starred Projects = nav_link(controller: :groups) do = link_to dashboard_groups_path, title: 'Groups' do %i.fa.fa-group diff --git a/config/routes.rb b/config/routes.rb index 1b855cd7a32..637b855e661 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -221,6 +221,12 @@ Gitlab::Application.routes.draw do delete :leave end end + + resources :projects, only: [] do + collection do + get :starred + end + end end end -- GitLab From b8d73315f5e094906111ab16607b5fa2685d2ea8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 14:25:14 -0700 Subject: [PATCH 1243/1609] Add tests for starred projects page --- features/dashboard/starred_projects.feature | 12 ++++++++++++ features/steps/dashboard/starred_projects.rb | 15 +++++++++++++++ features/steps/shared/paths.rb | 4 ++++ 3 files changed, 31 insertions(+) create mode 100644 features/dashboard/starred_projects.feature create mode 100644 features/steps/dashboard/starred_projects.rb diff --git a/features/dashboard/starred_projects.feature b/features/dashboard/starred_projects.feature new file mode 100644 index 00000000000..9dfd2fbab9c --- /dev/null +++ b/features/dashboard/starred_projects.feature @@ -0,0 +1,12 @@ +@dashboard +Feature: Dashboard Starred Projects + Background: + Given I sign in as a user + And public project "Community" + And I starred project "Community" + And I own project "Shop" + And I visit dashboard starred projects page + + Scenario: I should see projects list + Then I should see project "Community" + And I should not see project "Shop" diff --git a/features/steps/dashboard/starred_projects.rb b/features/steps/dashboard/starred_projects.rb new file mode 100644 index 00000000000..b9ad2f13e29 --- /dev/null +++ b/features/steps/dashboard/starred_projects.rb @@ -0,0 +1,15 @@ +class Spinach::Features::DashboardStarredProjects < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + + step 'I starred project "Community"' do + current_user.toggle_star(Project.find_by(name: 'Community')) + end + + step 'I should not see project "Shop"' do + within 'aside' do + page.should_not have_content('Shop') + end + end +end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index db6417bf951..bb6c336d7cd 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -95,6 +95,10 @@ module SharedPaths current_path.should == dashboard_groups_path end + step 'I visit dashboard starred projects page' do + visit starred_dashboard_projects_path + end + # ---------------------------------------- # Profile # ---------------------------------------- -- GitLab From c6b242112781120233a3627098e50689f6ccf9f8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 15:20:32 -0700 Subject: [PATCH 1244/1609] Render milestone progress with one helper method --- app/helpers/milestones_helper.rb | 11 +++++++++++ app/views/dashboard/milestones/index.html.haml | 3 +-- app/views/dashboard/milestones/show.html.haml | 3 +-- app/views/groups/milestones/index.html.haml | 3 +-- app/views/groups/milestones/show.html.haml | 3 +-- app/views/projects/milestones/_milestone.html.haml | 3 +-- app/views/projects/milestones/show.html.haml | 3 +-- 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 3383b1ae5be..59fdc0d49cc 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -8,4 +8,15 @@ module MilestonesHelper dashboard_milestones_path(opts) end end + + def milestone_progress_bar(milestone) + options = { + class: 'progress-bar progress-bar-success', + style: "width: #{milestone.percent_complete}%;" + } + + content_tag :div, class: 'progress' do + content_tag :div, nil, options + end + end end diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 65fc5898518..caf3b685864 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -28,8 +28,7 @@ = pluralize milestone.merge_requests_count, 'Merge Request'   %span.light #{milestone.percent_complete}% complete - .progress.progress-info - .progress-bar{style: "width: #{milestone.percent_complete}%;"} + = milestone_progress_bar(milestone) %div %br - milestone.milestones.each do |milestone| diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index a45a52001be..57cce9ab749 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -39,8 +39,7 @@ #{@dashboard_milestone.closed_items_count} closed – #{@dashboard_milestone.open_items_count} open - .progress.progress-info - .progress-bar{style: "width: #{@dashboard_milestone.percent_complete}%;"} + = milestone_progress_bar(@dashboard_milestone) %ul.nav.nav-tabs %li.active diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index fcbcb309aa7..9febaab04a7 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -36,8 +36,7 @@ = pluralize milestone.merge_requests_count, 'Merge Request'   %span.light #{milestone.percent_complete}% complete - .progress.progress-info - .progress-bar{style: "width: #{milestone.percent_complete}%;"} + = milestone_progress_bar(milestone) %div %br - milestone.milestones.each do |milestone| diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index e3606d167ad..dd2d84499ba 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -45,8 +45,7 @@ #{@group_milestone.closed_items_count} closed – #{@group_milestone.open_items_count} open - .progress.progress-info - .progress-bar{style: "width: #{@group_milestone.percent_complete}%;"} + = milestone_progress_bar(@group_milestone) %ul.nav.nav-tabs %li.active diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index d32b2ba271f..dcf56541db8 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -23,5 +23,4 @@ = pluralize milestone.merge_requests.count, 'Merge Request'   %span.light #{milestone.percent_complete}% complete - .progress.progress-info - .progress-bar{style: "width: #{milestone.percent_complete}%;"} + = milestone_progress_bar(milestone) diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index fea96f37011..110d8967342 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -43,8 +43,7 @@   %span.light #{@milestone.percent_complete}% complete %span.pull-right= @milestone.expires_at - .progress.progress-info - .progress-bar{style: "width: #{@milestone.percent_complete}%;"} + = milestone_progress_bar(@milestone) %ul.nav.nav-tabs -- GitLab From 3e6147ada4a7a252d5e9ca604a43d85b7756a5ea Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 16:24:45 -0700 Subject: [PATCH 1245/1609] Make milestone titles in list to be bold --- app/assets/stylesheets/pages/milestone.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 29ad4e24f0c..15e3948e402 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -1,3 +1,9 @@ .issues-sortable-list .str-truncated { max-width: 90%; } + +li.milestone { + h4 { + font-weight: bold; + } +} -- GitLab From 23fabc081d7476339e96ec0dfdf7cd6744da5375 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Mon, 9 Mar 2015 16:36:41 -0700 Subject: [PATCH 1246/1609] Fixing import redirect loop While importing, don't redirect import actions to the project page, even if the repository exists --- app/controllers/projects/imports_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 79d9910ce87..b64491b4666 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -37,7 +37,7 @@ class Projects::ImportsController < Projects::ApplicationController private def require_no_repo - if @project.repository_exists? + if @project.repository_exists? && !@project.import_in_progress? redirect_to(namespace_project_path(@project.namespace, @project)) and return end end -- GitLab From 6b76ffc222e063cf4450aacb71805068e044beba Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 17:06:08 -0700 Subject: [PATCH 1247/1609] Apply more styles from Flatly theme Also add bottom margin for footer links on login page --- app/assets/stylesheets/base/gl_variables.scss | 36 +++++++++++-------- app/assets/stylesheets/generic/common.scss | 9 +++-- app/assets/stylesheets/generic/forms.scss | 2 +- app/assets/stylesheets/pages/events.scss | 3 +- app/assets/stylesheets/pages/tree.scss | 2 +- .../layouts/_public_head_panel.html.haml | 2 +- 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index 2aa57e46a0a..6244f7ccc0d 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -15,6 +15,12 @@ // $gray: lighten($gray-base, 33.5%) // #555 // $gray-light: lighten($gray-base, 46.7%) // #777 // $gray-lighter: lighten($gray-base, 93.5%) // #eee +$gray-base: #000; +$gray-darker: lighten($gray-base, 13.5%); // #222 +$gray-dark: #7b8a8b; // #333 +$gray: #95a5a6; // #555 +$gray-light: #b4bcc2; // #999 +$gray-lighter: #ecf0f1; // #eee $brand-primary: $gl-primary; $brand-success: $gl-success; @@ -30,7 +36,7 @@ $brand-danger: $gl-danger; //** Background color for ``. // $body-bg: #fff //** Global text color on ``. -// $text-color: $gray-dark +$text-color: $brand-primary; //** Global textual link color. $link-color: $gl-link-color; @@ -187,9 +193,9 @@ $padding-base-horizontal: 14px; // $input-bg-disabled: $gray-lighter //** Text color for ``s -// $input-color: $gray +$input-color: $text-color; //** `` border color -// $input-border: #ccc +$input-border: #dce4ec; // TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4 //** Default `.form-control` border radius @@ -201,7 +207,7 @@ $padding-base-horizontal: 14px; // $input-border-radius-small: $border-radius-small //** Border color for inputs on focus -// $input-border-focus: #66afe9 +$input-border-focus: $brand-info; //** Placeholder text color // $input-color-placeholder: #999 @@ -213,7 +219,7 @@ $padding-base-horizontal: 14px; //** Small `.form-control` height // $input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) -// $legend-color: $gray-dark +$legend-color: $text-color; // $legend-border-color: #e5e5e5 //** Background color for textual input addons @@ -709,7 +715,7 @@ $panel-border-radius: 0; // $panel-inner-border: #ddd // $panel-footer-bg: #f5f5f5 -// $panel-default-text: $gray-dark +$panel-default-text: $text-color; // $panel-default-border: #ddd // $panel-default-heading-bg: #f5f5f5 @@ -757,8 +763,8 @@ $panel-border-radius: 0; // //## -// $well-bg: #f5f5f5 -// $well-border: darken($well-bg, 7%) +$well-bg: $gray-lighter; +$well-border: transparent; //== Badges @@ -826,15 +832,15 @@ $panel-border-radius: 0; // //## -// $code-color: #c7254e -// $code-bg: #f9f2f4 +$code-color: #c7254e; +$code-bg: #f9f2f4; -// $kbd-color: #fff -// $kbd-bg: #333 +$kbd-color: #fff; +$kbd-bg: #333; -// $pre-bg: #f5f5f5 -// $pre-color: $gray-dark -// $pre-border-color: #ccc +$pre-bg: $gray-lighter; +$pre-color: $gray-dark; +$pre-border-color: #ccc; // $pre-scrollable-max-height: 340px diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 431f1d68a2e..821cbd08902 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -319,7 +319,7 @@ table { } .btn-sign-in { - margin-top: 7px; + margin-top: 5px; text-shadow: none; } @@ -337,8 +337,11 @@ table { overflow-x: auto; } -.footer-links a { - margin-right: 15px; +.footer-links { + margin-bottom: 20px; + a { + margin-right: 15px; + } } .search_box { diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index c8982cdc00d..19bc11086e9 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -29,7 +29,7 @@ fieldset legend { padding: 17px 20px 18px; margin-top: 18px; margin-bottom: 18px; - background-color: whitesmoke; + background-color: #ecf0f1; border-top: 1px solid #e5e5e5; } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index a477359dc88..1c03f1240fc 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -46,7 +46,6 @@ border-bottom: 1px solid #eee; .event-title { @include str-truncated(72%); - color: #333; font-weight: 500; font-size: 14px; .author_name { @@ -185,7 +184,7 @@ } .event_filter { - + li a { padding: 5px 10px; background: rgba(0,0,0,0.045); diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 3305abc7d2a..a4337c11ab7 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -41,7 +41,7 @@ vertical-align: middle; i { - color: $gl-primary; + color: $gl-info; } img { diff --git a/app/views/layouts/_public_head_panel.html.haml b/app/views/layouts/_public_head_panel.html.haml index bd6bb3c720d..3d6d2bfc00a 100644 --- a/app/views/layouts/_public_head_panel.html.haml +++ b/app/views/layouts/_public_head_panel.html.haml @@ -12,7 +12,7 @@ - unless current_controller?('sessions') .pull-right.hidden-xs - = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new' + = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new append-right-10' .navbar-collapse.collapse %ul.nav.navbar-nav -- GitLab From ddd381c9a51b3408cf303283c466c7f70baf7e6a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 17:38:42 -0700 Subject: [PATCH 1248/1609] Add criteria for requesting CVE --- doc/release/security.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release/security.md b/doc/release/security.md index b67e0f37a04..1575fcf2708 100644 --- a/doc/release/security.md +++ b/doc/release/security.md @@ -22,7 +22,7 @@ Please report suspected security vulnerabilities in private to Date: Mon, 9 Mar 2015 17:59:32 -0700 Subject: [PATCH 1249/1609] Fix wrong body padding and pre color --- app/assets/stylesheets/base/gl_variables.scss | 2 +- app/assets/stylesheets/base/layout.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index 6244f7ccc0d..455e0093979 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -839,7 +839,7 @@ $kbd-color: #fff; $kbd-bg: #333; $pre-bg: $gray-lighter; -$pre-color: $gray-dark; +$pre-color: $text-color; $pre-border-color: #ccc; // $pre-scrollable-max-height: 340px diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss index 1085e68b7d4..62c11b06368 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -4,7 +4,7 @@ html { &.touch .tooltip { display: none !important; } body { - padding-top: 47px; + padding-top: 46px; } } -- GitLab From 86a17390dd923351ed51b9b94d34460d7a5c8214 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 18:09:23 -0700 Subject: [PATCH 1250/1609] Make broadcast message look like a warning by default --- app/assets/stylesheets/generic/common.scss | 12 ------------ app/assets/stylesheets/pages/admin.scss | 11 +++++++++++ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 821cbd08902..a854bd86636 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -306,18 +306,6 @@ table { width: 100%; } -.broadcast-message { - padding: 10px; - text-align: center; - background: #555; - color: #BBB; -} - -.broadcast-message-preview { - @extend .broadcast-message; - margin-bottom: 20px; -} - .btn-sign-in { margin-top: 5px; text-shadow: none; diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index a51deee7970..144852e7874 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -50,3 +50,14 @@ line-height: 2; } } + +.broadcast-message { + @extend .alert-warning; + padding: 10px; + text-align: center; +} + +.broadcast-message-preview { + @extend .broadcast-message; + margin-bottom: 20px; +} -- GitLab From 9720240be264e12aebb3b2b3b0051e587de78414 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 18:25:32 -0700 Subject: [PATCH 1251/1609] Increate default font-size from 13 to 14px --- app/assets/stylesheets/base/variables.scss | 2 +- app/assets/stylesheets/generic/common.scss | 2 +- app/assets/stylesheets/generic/markdown_area.scss | 2 -- app/assets/stylesheets/generic/nav_sidebar.scss | 2 +- app/assets/stylesheets/pages/commit.scss | 9 --------- app/assets/stylesheets/pages/commits.scss | 1 + app/assets/stylesheets/pages/events.scss | 4 ++-- app/assets/stylesheets/pages/help.scss | 2 -- app/assets/stylesheets/pages/issues.scss | 1 + app/assets/stylesheets/pages/merge_requests.scss | 1 + app/assets/stylesheets/pages/notes.scss | 11 ++++------- 11 files changed, 12 insertions(+), 25 deletions(-) diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 30e084ecd62..54af78ee082 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -3,7 +3,7 @@ $hover: #FFF3EB; $box_bg: #F9F9F9; $gl-link-color: #446e9b; $nprogress-color: #c0392b; -$gl-font-size: 13px; +$gl-font-size: 14px; $list-font-size: 15px; $sidebar_width: 230px; $avatar_radius: 50%; diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index a854bd86636..af8e90eb1a9 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -24,7 +24,7 @@ .slead { color: #666; - font-size: 14px; + font-size: 15px; margin-bottom: 12px; font-weight: normal; line-height: 24px; diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss index 22b7ce6d831..eb39b6bb7e9 100644 --- a/app/assets/stylesheets/generic/markdown_area.scss +++ b/app/assets/stylesheets/generic/markdown_area.scss @@ -57,7 +57,6 @@ border: 1px solid #ddd; min-height: 100px; padding: 5px; - font-size: 14px; box-shadow: none; } @@ -83,7 +82,6 @@ border: 1px solid #ddd; min-height: 100px; padding: 5px; - font-size: 14px; box-shadow: none; width: 100%; } diff --git a/app/assets/stylesheets/generic/nav_sidebar.scss b/app/assets/stylesheets/generic/nav_sidebar.scss index 335f1379662..b96063827c0 100644 --- a/app/assets/stylesheets/generic/nav_sidebar.scss +++ b/app/assets/stylesheets/generic/nav_sidebar.scss @@ -154,7 +154,7 @@ .collapse-nav a { position: fixed; - top: 47px; + top: 46px; padding: 5px 13px 3px 13px; left: 197px; background: #EEE; diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 0e2d9571a45..f46d6542c03 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -45,15 +45,6 @@ } } -.commit-committer-link, -.commit-author-link { - font-size: 13px; - color: #555; - &:hover { - color: #999; - } -} - .commit-box { margin: 10px 0; border-top: 1px solid #ddd; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 683aca73593..e167d044e47 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -100,6 +100,7 @@ li.commit { .commit-row-info { color: #777; line-height: 24px; + font-size: 13px; a { color: #777; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 1c03f1240fc..3e9e36e477e 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -53,6 +53,7 @@ } } .event-body { + font-size: 13px; margin-left: 35px; margin-right: 80px; color: #777; @@ -184,11 +185,10 @@ } .event_filter { - li a { + font-size: 13px; padding: 5px 10px; background: rgba(0,0,0,0.045); margin-left: 4px; } - } diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 07c62f98c36..6da7a2511a2 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -12,7 +12,6 @@ color: #888; a { - font-size: 14px; margin-right: 3px; } } @@ -29,7 +28,6 @@ th { padding-top: 15px; - font-size: 14px; line-height: 1.5; color: #333; text-align: left diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 46522e9ece9..4ea34cc1dac 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -11,6 +11,7 @@ .issue-info { color: #999; + font-size: 13px; } .issue-check { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 01f6a705224..9bd34b7376f 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -96,6 +96,7 @@ .merge-request-info { color: #999; + font-size: 13px; .merge-request-labels { display: inline-block; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 73f23626d57..384ff6d740c 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -38,13 +38,11 @@ ul.notes { .author { color: #333; font-weight: bold; - font-size: 14px; &:hover { color: $gl-link-color; } } .author-username { - font-size: 14px; } } @@ -57,9 +55,6 @@ ul.notes { .note { display: block; position:relative; - .attachment { - font-size: 14px; - } .note-body { overflow: auto; .note-text { @@ -184,6 +179,7 @@ ul.notes { margin-left: -60px; position: absolute; z-index: 10; + width: 32px; transition: all 0.2s ease; @@ -192,8 +188,9 @@ ul.notes { filter: alpha(opacity=0); &:hover { - font-size: 24px; - background: $gl-primary; + width: 38px; + font-size: 20px; + background: $gl-info; color: #FFF; @include show-add-diff-note; } -- GitLab From fcaf0a89e8f4be206b0378c32fd683e1cf15f804 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 18:28:30 -0700 Subject: [PATCH 1252/1609] Fix heading small color to darker one --- app/assets/stylesheets/base/gl_variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index 455e0093979..ce21ffae235 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -855,7 +855,7 @@ $pre-border-color: #ccc; //** Abbreviations and acronyms border color // $abbr-border-color: $gray-light //** Headings small color -// $headings-small-color: $gray-light +$headings-small-color: $gray-dark; //** Blockquote small color // $blockquote-small-color: $gray-light //** Blockquote font size -- GitLab From 3bce263a3d535927932a3ba174b6dfc2e41743a3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 18:45:36 -0700 Subject: [PATCH 1253/1609] Reduce base vertical padding --- app/assets/stylesheets/base/gl_variables.scss | 2 +- app/assets/stylesheets/generic/selects.scss | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index 455e0093979..1db490d75fd 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -99,7 +99,7 @@ $font-size-base: $gl-font-size; // //## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). -$padding-base-vertical: 8px; +$padding-base-vertical: 6px; $padding-base-horizontal: 14px; // $padding-large-vertical: 10px diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index 2773ee11fd4..af0ecb192d6 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -4,10 +4,8 @@ background: #FFF; border-color: #BBB; padding: 6px 14px; - font-size: 13px; - line-height: 18px; + line-height: 1.42857143; height: auto; - margin: 2px 0; .select2-arrow { background: #FFF; -- GitLab From de629b4835f3ac6feac6bd567cee3ec79eb2b7d0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 19:02:30 -0700 Subject: [PATCH 1254/1609] Blocking user does not remove him/her from project/groups but show blocked label --- CHANGELOG | 1 + app/models/user.rb | 20 +------------------ app/services/notification_service.rb | 1 + app/views/admin/users/show.html.haml | 1 - .../group_members/_group_member.html.haml | 3 +++ .../team_members/_team_member.html.haml | 5 +++-- 6 files changed, 9 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b210a6b0155..81468d4013e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -34,6 +34,7 @@ v 7.9.0 (unreleased) - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) - Move groups page from profile to dashboard - Starred projects page at dashboard + - Blocking user does not remove him/her from project/groups but show blocked label v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/models/user.rb b/app/models/user.rb index 51dd6332fdb..0d40ac8309e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -154,24 +154,6 @@ class User < ActiveRecord::Base delegate :path, to: :namespace, allow_nil: true, prefix: true state_machine :state, initial: :active do - after_transition any => :blocked do |user, transition| - # Remove user from all projects and - user.project_members.find_each do |membership| - # skip owned resources - next if membership.project.owner == user - - return false unless membership.destroy - end - - # Remove user from all groups - user.group_members.find_each do |membership| - # skip owned resources - next if membership.group.last_owner?(user) - - return false unless membership.destroy - end - end - event :block do transition active: :blocked end @@ -626,7 +608,7 @@ class User < ActiveRecord::Base def contributed_projects_ids Event.where(author_id: self). where("created_at > ?", Time.now - 1.year). - where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)", + where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)", pushed: Event::PUSHED, created: Event::CREATED). reorder(project_id: :desc). select(:project_id). diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 2fc63b9f4b7..0063b7ce40c 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -268,6 +268,7 @@ class NotificationService # Also remove duplications and nil recipients def reject_muted_users(users, project = nil) users = users.to_a.compact.uniq + users = users.reject(&:blocked?) users.reject do |user| next user.notification.disabled? unless project diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index bae9a97bf36..90c9f8c2f9b 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -116,7 +116,6 @@ %ul %li User will not be able to login %li User will not be able to access git repositories - %li User will be removed from joined projects and groups %li Personal projects will be left %li Owned groups will be left %br diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 30c3c2b00df..6267006f63f 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -8,6 +8,9 @@ %span.cgray= user.username - if user == current_user %span.label.label-success It's you + - if user.blocked? + %label.label.label-danger + %strong Blocked - if show_roles %span.pull-right diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml index 61c50af31bf..eb815447407 100644 --- a/app/views/projects/team_members/_team_member.html.haml +++ b/app/views/projects/team_members/_team_member.html.haml @@ -12,6 +12,7 @@ = image_tag avatar_icon(user.email, 32), class: "avatar s32" %p %strong= user.name + - if user.blocked? + %label.label.label-danger + %strong Blocked %span.cgray= user.username - - -- GitLab From 21c99e6a7797edb6a857e90c83fee3e5f1051adc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 9 Mar 2015 19:21:42 -0700 Subject: [PATCH 1255/1609] Fix font size for collapse button --- app/assets/stylesheets/generic/nav_sidebar.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/generic/nav_sidebar.scss b/app/assets/stylesheets/generic/nav_sidebar.scss index b96063827c0..4bf2c609be0 100644 --- a/app/assets/stylesheets/generic/nav_sidebar.scss +++ b/app/assets/stylesheets/generic/nav_sidebar.scss @@ -157,6 +157,7 @@ top: 46px; padding: 5px 13px 3px 13px; left: 197px; + font-size: 13px; background: #EEE; color: black; border: 1px solid rgba(0,0,0,0.035); -- GitLab From b26ab0ceeb60723b8a75078c4d49ed99c9ea3866 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 9 Mar 2015 15:10:59 -0700 Subject: [PATCH 1256/1609] This MR extends the commit calendar so it searches for commits made with every email address the user has associated with his account. This fixes one of the problems mentioned in gitlab-org/gitlab-ce#1162 and makes the behavior of the commit calendar as described in the profile. "All email addresses will be used to identify your commits." --- app/models/repository.rb | 3 ++- spec/models/repository_spec.rb | 23 +++++++++++++++++++++++ spec/support/repo_helpers.rb | 19 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 5b52739df2b..6117db418a7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -146,7 +146,8 @@ class Repository end def timestamps_by_user_log(user) - args = %W(git log --author=#{user.email} --since=#{(Date.today - 1.year).to_s} --branches --pretty=format:%cd --date=short) + author_emails = '(' + user.all_emails.map{ |e| Regexp.escape(e) }.join('|') + ')' + args = %W(git log -E --author=#{author_emails} --since=#{(Date.today - 1.year).to_s} --branches --pretty=format:%cd --date=short) dates = Gitlab::Popen.popen(args, path_to_repo).first.split("\n") if dates.present? diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index eeb0f3d9ee0..b3a38f6c5b9 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -18,4 +18,27 @@ describe Repository do it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end + + context :timestamps_by_user_log do + before do + Date.stub(:today).and_return(Date.new(2015, 03, 01)) + end + + describe 'single e-mail for user' do + let(:user) { create(:user, email: sample_commit.author_email) } + + subject { repository.timestamps_by_user_log(user) } + + it { is_expected.to eq(["2014-08-06", "2014-07-31", "2014-07-31"]) } + end + + describe 'multiple emails for user' do + let(:email_alias) { create(:email, email: another_sample_commit.author_email) } + let(:user) { create(:user, email: sample_commit.author_email, emails: [email_alias]) } + + subject { repository.timestamps_by_user_log(user) } + + it { is_expected.to eq(["2015-01-10", "2014-08-06", "2014-07-31", "2014-07-31"]) } + end + end end diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb index 4c4775da692..aadf791bf3f 100644 --- a/spec/support/repo_helpers.rb +++ b/spec/support/repo_helpers.rb @@ -43,6 +43,25 @@ eos ) end + def another_sample_commit + OpenStruct.new( + id: "e56497bb5f03a90a51293fc6d516788730953899", + parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40', + author_full_name: "Sytse Sijbrandij", + author_email: "sytse@gitlab.com", + files_changed_count: 1, + message: < Date: Tue, 10 Mar 2015 00:51:16 -0700 Subject: [PATCH 1257/1609] Improve tree view UI --- app/assets/stylesheets/pages/tree.scss | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index a4337c11ab7..ce02cdb1652 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -40,8 +40,8 @@ max-width: 320px; vertical-align: middle; - i { - color: $gl-info; + i, a { + color: $gl-link-color; } img { @@ -61,13 +61,18 @@ .tree_author { padding-right: 8px; + + .commit-author-name { + color: gray; + } } .tree_commit { color: gray; .tree-commit-link { - color: #444; + color: gray; + &:hover { text-decoration: underline; } -- GitLab From b7a31a4b024e2c5f607003f1c42e2cd46adb2ff4 Mon Sep 17 00:00:00 2001 From: Nicole Cordes Date: Wed, 3 Sep 2014 22:28:04 +0200 Subject: [PATCH 1258/1609] Generate valid json for hooks It seems that ruby can handle 'nil' value but other json processors (like PHP) throw an error. This is always generated for empty arrays. --- CHANGELOG | 1 + app/services/system_hooks_service.rb | 2 +- lib/gitlab/push_data_builder.rb | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 81468d4013e..1842a28f84e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,7 @@ v 7.9.0 (unreleased) - Move groups page from profile to dashboard - Starred projects page at dashboard - Blocking user does not remove him/her from project/groups but show blocked label + - Improve json validation in hook data v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 46f6e91e808..c5d0b08845b 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -41,7 +41,7 @@ class SystemHooksService path_with_namespace: model.path_with_namespace, project_id: model.id, owner_name: owner.name, - owner_email: owner.respond_to?(:email) ? owner.email : nil, + owner_email: owner.respond_to?(:email) ? owner.email : "", project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase }) when User diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 5cefa67d3ab..ea06e1f7333 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -58,6 +58,7 @@ module Gitlab data[:commits] << commit.hook_attrs(project) end + data[:commits] = "" if data[:commits].count == 0 data end -- GitLab From 6653cd7312fb4d88cc77af0d29d588c329d66cca Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 10 Mar 2015 11:20:57 +0100 Subject: [PATCH 1259/1609] Mention EmailsOnPush changes in changelog. --- CHANGELOG | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 81468d4013e..9b9e5832558 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,15 @@ v 7.9.0 (unreleased) - Move groups page from profile to dashboard - Starred projects page at dashboard - Blocking user does not remove him/her from project/groups but show blocked label + - Change subject of EmailsOnPush emails to include namespace, project and branch. + - Change subject of EmailsOnPush emails to include first commit message when multiple were pushed. + - Remove confusing footer from EmailsOnPush mail body. + - Add list of changed files to EmailsOnPush emails. + - Add option to send EmailsOnPush emails from committer email if domain matches. + - Add option to disable code diffs in EmailOnPush emails. + - Wrap commit message in EmailsOnPush email. + - Send EmailsOnPush emails when deleting commits using force push. + - Fix EmailsOnPush email comparison link to include first commit. v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 -- GitLab From ed4c7190ed47f0311ba5f5a140b2c71a694b05db Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 6 Mar 2015 16:39:20 +0200 Subject: [PATCH 1260/1609] Fix importers with OCC --- app/controllers/import/base_controller.rb | 18 ++++++++---------- ...0306023112_add_unique_index_to_namespace.rb | 9 +++++++++ db/schema.rb | 8 ++++---- 3 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 db/migrate/20150306023112_add_unique_index_to_namespace.rb diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 7dc0cac8d4c..edb8bd4160b 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -3,19 +3,17 @@ class Import::BaseController < ApplicationController private def get_or_create_namespace - existing_namespace = Namespace.find_by_path_or_name(@target_namespace) - - if existing_namespace - if existing_namespace.owner == current_user - namespace = existing_namespace - else + begin + namespace = Group.create!(name: @target_namespace, path: @target_namespace, owner: current_user) + namespace.add_owner(current_user) + rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid + namespace = Namespace.find_by_path_or_name(@target_namespace) + unless namespace.owner == current_user @already_been_taken = true return false end - else - namespace = Group.create(name: @target_namespace, path: @target_namespace, owner: current_user) - namespace.add_owner(current_user) - namespace end + + namespace end end diff --git a/db/migrate/20150306023112_add_unique_index_to_namespace.rb b/db/migrate/20150306023112_add_unique_index_to_namespace.rb new file mode 100644 index 00000000000..b1f7822b4dc --- /dev/null +++ b/db/migrate/20150306023112_add_unique_index_to_namespace.rb @@ -0,0 +1,9 @@ +class AddUniqueIndexToNamespace < ActiveRecord::Migration + def change + remove_index :namespaces, :name + remove_index :namespaces, :path + + add_index :namespaces, :name, unique: true + add_index :namespaces, :path, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index a686bb4b3cd..3afbc082b70 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150225065047) do +ActiveRecord::Schema.define(version: 20150306023112) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -242,9 +242,9 @@ ActiveRecord::Schema.define(version: 20150225065047) do end add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree - add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree + add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree - add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree + add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree create_table "notes", force: true do |t| @@ -334,12 +334,12 @@ ActiveRecord::Schema.define(version: 20150225065047) do t.string "import_url" t.integer "visibility_level", default: 0, null: false t.boolean "archived", default: false, null: false - t.string "avatar" t.string "import_status" t.float "repository_size", default: 0.0 t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" + t.string "avatar" end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree -- GitLab From ca9aca927970ec81387d7cd0d7372a11d03074de Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 10 Mar 2015 13:32:28 +0100 Subject: [PATCH 1261/1609] Allow smb:// links in Markdown text. --- app/helpers/gitlab_markdown_helper.rb | 2 +- config/application.rb | 2 ++ lib/redcarpet/render/gitlab_html.rb | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index ab30f498c01..0c69900839d 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -119,7 +119,7 @@ module GitlabMarkdownHelper end def ignored_protocols - ["http://","https://", "ftp://", "mailto:"] + ["http://","https://", "ftp://", "mailto:", "smb://"] end def rebuild_path(file_path) diff --git a/config/application.rb b/config/application.rb index bd4578848c5..fa399533e52 100644 --- a/config/application.rb +++ b/config/application.rb @@ -50,6 +50,8 @@ module Gitlab # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' + config.action_view.sanitized_allowed_protocols = %w(smb) + # Relative url support # Uncomment and customize the last line to run in a non-root path # WARNING: We recommend creating a FQDN to host GitLab in a root path instead of this. diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 714261f815c..4b33d691c58 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -10,6 +10,12 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML super options end + def preprocess(full_document) + # Redcarpet doesn't allow SMB links when `safe_links_only` is enabled. + # FTP links are allowed, so we trick Redcarpet. + full_document.gsub("smb://", "ftp://smb:") + end + # If project has issue number 39, apostrophe will be linked in # regular text to the issue as Redcarpet will convert apostrophe to # #39; @@ -54,6 +60,8 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML end def postprocess(full_document) + full_document.gsub!("ftp://smb:", "smb://") + full_document.gsub!("’", "'") unless @template.instance_variable_get("@project_wiki") || @project.nil? full_document = h.create_relative_links(full_document) -- GitLab From 383c56efa1882d9cab956de5b5b72e51691c3f0c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 10 Mar 2015 11:51:36 +0100 Subject: [PATCH 1262/1609] Use Gitlab::Git helper methods and constants as much as possible. --- app/controllers/projects/tags_controller.rb | 2 +- app/mailers/emails/projects.rb | 2 +- app/models/event.rb | 12 +++++------ app/models/project_services/asana_service.rb | 2 +- .../project_services/campfire_service.rb | 6 +++--- .../project_services/hipchat_service.rb | 13 ++++-------- .../project_services/pushover_service.rb | 6 +++--- .../slack_service/push_message.rb | 13 ++++-------- .../project_services/teamcity_service.rb | 2 +- app/services/create_tag_service.rb | 4 ++-- app/services/event_create_service.rb | 8 ++++---- app/services/git_push_service.rb | 16 +++++---------- .../merge_requests/refresh_service.rb | 4 ++-- app/workers/emails_on_push_worker.rb | 2 +- app/workers/irker_worker.rb | 6 +++--- app/workers/post_receive.rb | 8 +------- lib/gitlab/git.rb | 20 +++++++++++++++++-- lib/gitlab/git_access.rb | 10 +++++----- lib/gitlab/push_data_builder.rb | 7 ++++--- .../project_services/hipchat_service_spec.rb | 2 +- .../slack_service/push_message_spec.rb | 6 +++--- 21 files changed, 73 insertions(+), 78 deletions(-) diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 08c7ce3f37d..03fface2d2a 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController tag = @repository.find_tag(params[:id]) if tag && @repository.rm_tag(tag.name) - EventCreateService.new.push_ref(@project, current_user, tag, 'rm', 'refs/tags') + EventCreateService.new.push_ref(@project, current_user, tag, 'rm', Gitlab::Git::TAG_REF_PREFIX) end respond_to do |format| diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 9ea121d83a4..b55129de292 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -23,7 +23,7 @@ module Emails @compare = compare @commits = Commit.decorate(compare.commits) @diffs = compare.diffs - @branch = branch.gsub("refs/heads/", "") + @branch = Gitlab::Git.ref_name(branch) @disable_diffs = disable_diffs @subject = "[#{@project.path_with_namespace}][#{@branch}] " diff --git a/app/models/event.rb b/app/models/event.rb index 5579ab1dbb0..8d20d7ef252 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -190,19 +190,19 @@ class Event < ActiveRecord::Base end def tag? - data[:ref]["refs/tags"] + Gitlab::Git.tag_ref?(data[:ref]) end def branch? - data[:ref]["refs/heads"] + Gitlab::Git.branch_ref?(data[:ref]) end def new_ref? - commit_from =~ /^00000/ + Gitlab::Git.blank_ref?(commit_from) end def rm_ref? - commit_to =~ /^00000/ + Gitlab::Git.blank_ref?(commit_to) end def md_ref? @@ -226,11 +226,11 @@ class Event < ActiveRecord::Base end def branch_name - @branch_name ||= data[:ref].gsub("refs/heads/", "") + @branch_name ||= Gitlab::Git.ref_name(data[:ref]) end def tag_name - @tag_name ||= data[:ref].gsub("refs/tags/", "") + @tag_name ||= Gitlab::Git.ref_name(data[:ref]) end # Max 20 commits from push DESC diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 6a622207385..d52214cdd69 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -77,7 +77,7 @@ automatically inspected. Leave blank to include all branches.' end user = data[:user_name] - branch = data[:ref].gsub('refs/heads/', '') + branch = Gitlab::Git.ref_name(data[:ref]) branch_restriction = restrict_to_branch.to_s diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 1c63444fbf9..e591afdda64 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -64,7 +64,7 @@ class CampfireService < Service end def build_message(push) - ref = push[:ref].gsub("refs/heads/", "") + ref = Gitlab::Git.ref_name(push[:ref]) before = push[:before] after = push[:after] @@ -72,9 +72,9 @@ class CampfireService < Service message << "[#{project.name_with_namespace}] " message << "#{push[:user_name]} " - if before.include?('000000') + if Gitlab::Git.blank_ref?(before) message << "pushed new branch #{ref} \n" - elsif after.include?('000000') + elsif Gitlab::Git.blank_ref?(after) message << "removed branch #{ref} \n" else message << "pushed #{push[:total_commits_count]} commits to #{ref}. " diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 90ba7e080f1..d264a56ebdf 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -79,24 +79,19 @@ class HipchatService < Service end def create_push_message(push) - if push[:ref].starts_with?('refs/tags/') - ref_type = 'tag' - ref = push[:ref].gsub('refs/tags/', '') - else - ref_type = 'branch' - ref = push[:ref].gsub('refs/heads/', '') - end + ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch' + ref = Gitlab::Git.ref_name(push[:ref]) before = push[:before] after = push[:after] message = "" message << "#{push[:user_name]} " - if before.include?('000000') + if Gitlab::Git.blank_ref?(before) message << "pushed new #{ref_type} #{ref}"\ " to #{project_link}\n" - elsif after.include?('000000') + elsif Gitlab::Git.blank_ref?(after) message << "removed #{ref_type} #{ref} from #{project_name} \n" else message << "pushed to #{ref_type} "\ diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 8cd65724cb9..dfc5677c9d4 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -21,7 +21,7 @@ class CreateTagService < BaseService new_tag = repository.find_tag(tag_name) if new_tag - EventCreateService.new.push_ref(project, current_user, new_tag, 'add', 'refs/tags') + EventCreateService.new.push_ref(project, current_user, new_tag, 'add', Gitlab::Git::TAG_REF_PREFIX) push_data = create_push_data(project, current_user, new_tag) project.execute_hooks(push_data.dup, :tag_push_hooks) @@ -41,7 +41,7 @@ class CreateTagService < BaseService def create_push_data(project, user, tag) data = Gitlab::PushDataBuilder. - build(project, user, Gitlab::Git::BLANK_SHA, tag.target, 'refs/tags/' + tag.name, []) + build(project, user, Gitlab::Git::BLANK_SHA, tag.target, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) data[:object_kind] = "tag_push" data end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index ba9547b9242..dc52d6d89df 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -62,19 +62,19 @@ class EventCreateService create_event(project, current_user, Event::CREATED) end - def push_ref(project, current_user, ref, action = 'add', prefix = 'refs/heads') + def push_ref(project, current_user, ref, action = 'add', prefix = Gitlab::Git::BRANCH_REF_PREFIX) commit = project.repository.commit(ref.target) if action.to_s == 'add' - before = '00000000' + before = Gitlab::Git::BLANK_SHA after = commit.id else before = commit.id - after = '00000000' + after = Gitlab::Git::BLANK_SHA end data = { - ref: "#{prefix}/#{ref.name}", + ref: "#{prefix}#{ref.name}", before: before, after: after } diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 4e1afea6d50..bfabfd7ade3 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -107,30 +107,24 @@ class GitPushService end def push_to_existing_branch?(ref, oldrev) - ref_parts = ref.split('/') - # Return if this is not a push to a branch (e.g. new commits) - ref_parts[1].include?('heads') && oldrev != Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && oldrev != Gitlab::Git::BLANK_SHA end def push_to_new_branch?(ref, oldrev) - ref_parts = ref.split('/') - - ref_parts[1].include?('heads') && oldrev == Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(oldrev) end def push_remove_branch?(ref, newrev) - ref_parts = ref.split('/') - - ref_parts[1].include?('heads') && newrev == Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(newrev) end def push_to_branch?(ref) - ref.include?('refs/heads') + Gitlab::Git.branch_ref?(ref) end def is_default_branch?(ref) - ref == "refs/heads/#{project.default_branch}" + Gitlab::Git.branch_ref?(ref) && Gitlab::Git.ref_name(ref) == project.default_branch end def commit_user(commit) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index ea846472766..cab8a1e880e 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -1,10 +1,10 @@ module MergeRequests class RefreshService < MergeRequests::BaseService def execute(oldrev, newrev, ref) - return true unless ref =~ /heads/ + return true unless Gitlab::Git.branch_ref?(ref) @oldrev, @newrev = oldrev, newrev - @branch_name = ref.gsub("refs/heads/", "") + @branch_name = Gitlab::Git.ref_name(ref) @fork_merge_requests = @project.fork_merge_requests.opened @commits = @project.repository.commits_between(oldrev, newrev) diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 2e783814824..e59ca81defe 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -8,7 +8,7 @@ class EmailsOnPushWorker branch = push_data["ref"] author_id = push_data["user_id"] - if before_sha =~ /^000000/ || after_sha =~ /^000000/ + if Gitlab::Git.blank_ref?(before_sha) || Gitlab::Git.blank_ref?(after_sha) # skip if new branch was pushed or branch was removed return true end diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 613bae351d8..e1a99d9cad8 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -57,9 +57,9 @@ class IrkerWorker end def send_branch_updates(push_data, project, repo_name, committer, branch) - if push_data['before'] =~ /^000000/ + if push_data['before'] == Gitlab::Git::BLANK_SHA send_new_branch project, repo_name, committer, branch - elsif push_data['after'] =~ /^000000/ + elsif push_data['after'] == Gitlab::Git::BLANK_SHA send_del_branch repo_name, committer, branch end end @@ -83,7 +83,7 @@ class IrkerWorker return if push_data['total_commits_count'] == 0 # Next message is for number of commit pushed, if any - if push_data['before'] =~ /^000000/ + if push_data['before'] == Gitlab::Git::BLANK_SHA # Tweak on push_data["before"] in order to have a nice compare URL push_data['before'] = before_on_new_branch push_data, project end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 1406cba2db3..ecc6c8e53a3 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -33,7 +33,7 @@ class PostReceive return false end - if tag?(ref) + if Gitlab::Git.tag_ref?(ref) GitTagPushService.new.execute(project, @user, oldrev, newrev, ref) else GitPushService.new.execute(project, @user, oldrev, newrev, ref) @@ -44,10 +44,4 @@ class PostReceive def log(message) Gitlab::GitLogger.error("POST-RECEIVE: #{message}") end - - private - - def tag?(ref) - !!(/refs\/tags\/(.*)/.match(ref)) - end end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 4a712c6345f..0c350d7c675 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -1,9 +1,25 @@ module Gitlab module Git BLANK_SHA = '0' * 40 + TAG_REF_PREFIX = "refs/tags/" + BRANCH_REF_PREFIX = "refs/heads/" - def self.extract_ref_name(ref) - ref.gsub(/\Arefs\/(tags|heads)\//, '') + class << self + def ref_name(ref) + ref.gsub(/\Arefs\/(tags|heads)\//, '') + end + + def tag_ref?(ref) + ref.start_with?(TAG_REF_PREFIX) + end + + def branch_ref?(ref) + ref.start_with?(BRANCH_REF_PREFIX) + end + + def blank_ref?(ref) + ref == BLANK_SHA + end end end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 9b31190a882..cb69e4b13d3 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -115,7 +115,7 @@ module Gitlab # we dont allow force push to protected branch if forced_push?(project, oldrev, newrev) :force_push_code_to_protected_branches - elsif newrev == Gitlab::Git::BLANK_SHA + elsif Gitlab::Git.blank_ref?(newrev) # and we dont allow remove of protected branch :remove_protected_branches elsif project.developers_can_push_to_protected_branch?(branch_name) @@ -135,8 +135,8 @@ module Gitlab def branch_name(ref) ref = ref.to_s - if ref.start_with?('refs/heads') - ref.sub(%r{\Arefs/heads/}, '') + if Gitlab::Git.branch_ref?(ref) + Gitlab::Git.ref_name(ref) else nil end @@ -144,8 +144,8 @@ module Gitlab def tag_name(ref) ref = ref.to_s - if ref.start_with?('refs/tags') - ref.sub(%r{\Arefs/tags/}, '') + if Gitlab::Git.tag_ref?(ref) + Gitlab::Git.ref_name(ref) else nil end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 5cefa67d3ab..9fb0bf65949 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -65,12 +65,13 @@ module Gitlab # existing project and commits to test web hooks def build_sample(project, user) commits = project.repository.commits(project.default_branch, nil, 3) - build(project, user, commits.last.id, commits.first.id, "refs/heads/#{project.default_branch}", commits) + ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}" + build(project, user, commits.last.id, commits.first.id, ref, commits) end def checkout_sha(repository, newrev, ref) - if newrev != Gitlab::Git::BLANK_SHA && ref.start_with?('refs/tags/') - tag_name = Gitlab::Git.extract_ref_name(ref) + if newrev != Gitlab::Git::BLANK_SHA && Gitlab::Git.tag_ref?(ref) + tag_name = Gitlab::Git.ref_name(ref) tag = repository.find_tag(tag_name) if tag diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index b9f2bee148d..8ab847e6432 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -63,7 +63,7 @@ describe HipchatService do end context 'tag_push events' do - let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, '000000', '111111', 'refs/tags/test', []) } + let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) } it "should call Hipchat API for tag push events" do hipchat.execute(push_sample_data) diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb index 3ef065459d8..10963481a12 100644 --- a/spec/models/project_services/slack_service/push_message_spec.rb +++ b/spec/models/project_services/slack_service/push_message_spec.rb @@ -43,7 +43,7 @@ describe SlackService::PushMessage do let(:args) { { after: 'after', - before: '000000', + before: Gitlab::Git::BLANK_SHA, project_name: 'project_name', ref: 'refs/tags/new_tag', user_name: 'user_name', @@ -61,7 +61,7 @@ describe SlackService::PushMessage do context 'new branch' do before do - args[:before] = '000000' + args[:before] = Gitlab::Git::BLANK_SHA end it 'returns a message regarding a new branch' do @@ -75,7 +75,7 @@ describe SlackService::PushMessage do context 'removed branch' do before do - args[:after] = '000000' + args[:after] = Gitlab::Git::BLANK_SHA end it 'returns a message regarding a removed branch' do -- GitLab From 76842aac754e2355c34e751002fcbb1e8187e344 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 10 Mar 2015 14:06:15 +0100 Subject: [PATCH 1263/1609] Properly move over `issues_tracker_id` from old custom issue tracker URLs. --- app/models/project_services/issue_tracker_service.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 16876335b67..0c734a544d2 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -60,9 +60,9 @@ class IssueTrackerService < Service if enabled_in_gitlab_config self.properties = { title: issues_tracker['title'], - project_url: set_project_url, - issues_url: issues_tracker['issues_url'], - new_issue_url: issues_tracker['new_issue_url'] + project_url: add_issues_tracker_id(issues_tracker['project_url']), + issues_url: add_issues_tracker_id(issues_tracker['issues_url']), + new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url']) } else self.properties = {} @@ -111,15 +111,15 @@ class IssueTrackerService < Service Gitlab.config.issues_tracker[to_param] end - def set_project_url + def add_issues_tracker_id(url) if self.project id = self.project.issues_tracker_id if id - issues_tracker['project_url'].gsub(":issues_tracker_id", id) + url = url.gsub(":issues_tracker_id", id) end end - issues_tracker['project_url'] + url end end -- GitLab From ee45fa89b62bd11b397f84e9ed42ee78dadabd20 Mon Sep 17 00:00:00 2001 From: Ben Carson Date: Mon, 9 Mar 2015 16:45:35 -0400 Subject: [PATCH 1264/1609] Added a link_to for the LinkedIn portion of the user's profile. It felt odd to me, having this section not being an active link like the rest of the entries on the page. --- app/views/users/_profile.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml index 3b44959baad..0a70b738071 100644 --- a/app/views/users/_profile.html.haml +++ b/app/views/users/_profile.html.haml @@ -12,7 +12,7 @@ - unless user.linkedin.blank? %li %span.light LinkedIn: - %strong= user.linkedin + %strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}" - unless user.twitter.blank? %li %span.light Twitter: -- GitLab From 4218a2bfcf7a3f864268c3eafe8ead28bb7808d8 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 27 Feb 2015 17:17:57 -0800 Subject: [PATCH 1265/1609] Fix code preview theme setting for comments, issues, merge requests, and snippets. Also preserve code preview color scheme in events dashboard. Assign default colors to all code blocks shown as
    
    Closes #1139
    ---
     CHANGELOG                                            |  1 +
     app/assets/stylesheets/highlight/dark.scss           |  4 ++++
     app/assets/stylesheets/highlight/monokai.scss        |  4 ++++
     app/assets/stylesheets/highlight/solarized_dark.scss |  4 ++++
     .../stylesheets/highlight/solarized_light.scss       |  4 ++++
     app/assets/stylesheets/highlight/white.scss          |  4 ++++
     app/helpers/events_helper.rb                         |  2 +-
     app/helpers/gitlab_markdown_helper.rb                |  4 +++-
     lib/redcarpet/render/gitlab_html.rb                  |  5 +++--
     spec/helpers/events_helper_spec.rb                   | 12 ++++++++++++
     10 files changed, 40 insertions(+), 4 deletions(-)
    
    diff --git a/CHANGELOG b/CHANGELOG
    index 81468d4013e..0ffdbc8391d 100644
    --- a/CHANGELOG
    +++ b/CHANGELOG
    @@ -7,6 +7,7 @@ v 7.9.0 (unreleased)
       - Add issue and merge request events to HipChat and Slack services (Stan Hu)
       - Fix merge request URL passed to Webhooks. (Stan Hu)
       - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu)
    +  - Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu)
       - Move labels/milestones tabs to sidebar
       - Upgrade Rails gem to version 4.1.9.
       - Improve error messages for file edit failures
    diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
    index fcd4d47bace..01e12323c7e 100644
    --- a/app/assets/stylesheets/highlight/dark.scss
    +++ b/app/assets/stylesheets/highlight/dark.scss
    @@ -1,6 +1,10 @@
     /* https://github.com/MozMorris/tomorrow-pygments */
    +pre.code.highlight.dark,
     .code.dark {
     
    +  background-color: #1d1f21;
    +  color: #c5c8c6;
    +
       pre.code,
       .line-numbers,
       .line-numbers a {
    diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
    index bcd2e716657..e7d62a7ca1a 100644
    --- a/app/assets/stylesheets/highlight/monokai.scss
    +++ b/app/assets/stylesheets/highlight/monokai.scss
    @@ -1,6 +1,10 @@
     /* https://github.com/richleland/pygments-css/blob/master/monokai.css */
    +pre.code.monokai,
     .code.monokai {
     
    +  background: #272822;
    +  color: #f8f8f2;
    +
       pre.highlight,
       .line-numbers,
       .line-numbers a {
    diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
    index 4a6b759bd2c..de2676e39cf 100644
    --- a/app/assets/stylesheets/highlight/solarized_dark.scss
    +++ b/app/assets/stylesheets/highlight/solarized_dark.scss
    @@ -1,6 +1,10 @@
     /* https://gist.github.com/qguv/7936275 */
    +pre.code.highlight.solarized-dark,
     .code.solarized-dark {
     
    +  background-color: #002b36;
    +  color: #93a1a1;
    +
       pre.code,
       .line-numbers,
       .line-numbers a {
    diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
    index 7254f4d7ac1..784e768914c 100644
    --- a/app/assets/stylesheets/highlight/solarized_light.scss
    +++ b/app/assets/stylesheets/highlight/solarized_light.scss
    @@ -1,6 +1,10 @@
     /* https://gist.github.com/qguv/7936275 */
    +pre.code.highlight.solarized-light,
     .code.solarized-light {
     
    +  background-color: #fdf6e3;
    +  color: #586e75;
    +
       pre.code,
       .line-numbers,
       .line-numbers a {
    diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
    index 4d6f5dfd91e..9b9b0a6bd6d 100644
    --- a/app/assets/stylesheets/highlight/white.scss
    +++ b/app/assets/stylesheets/highlight/white.scss
    @@ -1,6 +1,10 @@
     /* https://github.com/aahan/pygments-github-style */
    +pre.code.highlight.white,
     .code.white {
     
    +  background-color: #fff;
    +  color: #333;
    +
       pre.highlight,
       .line-numbers,
       .line-numbers a {
    diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
    index d38b546e1b2..779cebc0136 100644
    --- a/app/helpers/events_helper.rb
    +++ b/app/helpers/events_helper.rb
    @@ -166,7 +166,7 @@ module EventsHelper
     
       def event_note(text)
         text = first_line_in_markdown(text, 150)
    -    sanitize(text, tags: %w(a img b pre code p))
    +    sanitize(text, tags: %w(a img b pre code p span))
       end
     
       def event_commit_title(message)
    diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
    index ab30f498c01..daaefe90f18 100644
    --- a/app/helpers/gitlab_markdown_helper.rb
    +++ b/app/helpers/gitlab_markdown_helper.rb
    @@ -31,7 +31,9 @@ module GitlabMarkdownHelper
       def markdown(text, options={})
         unless (@markdown and options == @options)
           @options = options
    -      gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self, {
    +      gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self,
    +                                                          user_color_scheme_class,
    +                                                          {
                                 # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch-
                                 filter_html: true,
                                 with_toc_data: true,
    diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb
    index 714261f815c..713d7c39a11 100644
    --- a/lib/redcarpet/render/gitlab_html.rb
    +++ b/lib/redcarpet/render/gitlab_html.rb
    @@ -3,8 +3,9 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML
       attr_reader :template
       alias_method :h, :template
     
    -  def initialize(template, options = {})
    +  def initialize(template, color_scheme, options = {})
         @template = template
    +    @color_scheme = color_scheme
         @project = @template.instance_variable_get("@project")
         @options = options.dup
         super options
    @@ -34,7 +35,7 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML
         end
     
         formatter = Rugments::Formatters::HTML.new(
    -      cssclass: "code highlight white #{lexer.tag}"
    +      cssclass: "code highlight #{@color_scheme} #{lexer.tag}"
         )
         formatter.format(lexer.lex(code))
       end
    diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
    index c4a192ac1aa..b392371deb4 100644
    --- a/spec/helpers/events_helper_spec.rb
    +++ b/spec/helpers/events_helper_spec.rb
    @@ -4,6 +4,8 @@ describe EventsHelper do
       include ApplicationHelper
       include GitlabMarkdownHelper
     
    +  let(:current_user) { create(:user, email: "current@email.com") }
    +
       it 'should display one line of plain text without alteration' do
         input = 'A short, plain note'
         expect(event_note(input)).to match(input)
    @@ -50,4 +52,14 @@ describe EventsHelper do
         expect(event_note(input)).to match(link_url)
         expect(event_note(input)).to match(expected_link_text)
       end
    +
    +  it 'should preserve code color scheme' do
    +    input = "```ruby\ndef test\n  'hello world'\nend\n```"
    +    expected = '
    ' \
    +      "def test\n" \
    +      "  \'hello world\'\n" \
    +      "end\n" \
    +      '
    ' + expect(event_note(input)).to eq(expected) + end end -- GitLab From 1fec4eff361fed0fecd2928ef83d928fab5e1976 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 10 Mar 2015 16:06:58 +0000 Subject: [PATCH 1266/1609] Upgrade Docker image to GitLab v7.8.3 --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3584a754c62..4eb280f9554 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update -q \ # If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.8.1-omnibus-1_amd64.deb \ + wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.8.3-omnibus-1_amd64.deb \ && dpkg -i $TMP_FILE \ && rm -f $TMP_FILE -- GitLab From f5e42f602f8a4eb85a7087bc0f407f9510df0ea8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 10 Mar 2015 14:50:42 +0100 Subject: [PATCH 1267/1609] Reject access to group/project avatar if the user doesn't have access. --- CHANGELOG | 1 + app/controllers/uploads_controller.rb | 48 ++-- spec/controllers/uploads_controller_spec.rb | 296 ++++++++++++++++++++ 3 files changed, 329 insertions(+), 16 deletions(-) create mode 100644 spec/controllers/uploads_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 81468d4013e..03fd68e299f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,7 @@ v 7.9.0 (unreleased) - Move groups page from profile to dashboard - Starred projects page at dashboard - Blocking user does not remove him/her from project/groups but show blocked label + - Reject access to group/project avatar if the user doesn't have access. v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 810ac9f34bd..c5f3da54ea2 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -1,24 +1,15 @@ class UploadsController < ApplicationController - skip_before_filter :authenticate_user!, :reject_blocked! - before_filter :authorize_access + skip_before_filter :authenticate_user! + before_filter :find_model, :authorize_access! def show - unless upload_model && upload_mount - return not_found! - end - - model = upload_model.find(params[:id]) - uploader = model.send(upload_mount) - - if model.respond_to?(:project) && !can?(current_user, :read_project, model.project) - return not_found! - end + uploader = @model.send(upload_mount) unless uploader.file_storage? return redirect_to uploader.url end - unless uploader.file.exists? + unless uploader.file && uploader.file.exists? return not_found! end @@ -28,9 +19,34 @@ class UploadsController < ApplicationController private - def authorize_access - unless params[:mounted_as] == 'avatar' - authenticate_user! && reject_blocked! + def find_model + unless upload_model && upload_mount + return not_found! + end + + @model = upload_model.find(params[:id]) + end + + def authorize_access! + authorized = + case @model + when Project + can?(current_user, :read_project, @model) + when Group + can?(current_user, :read_group, @model) + when Note + can?(current_user, :read_project, @model.project) + else + # No authentication required for user avatars. + true + end + + return if authorized + + if current_user + not_found! + else + authenticate_user! end end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb new file mode 100644 index 00000000000..0f9780356b1 --- /dev/null +++ b/spec/controllers/uploads_controller_spec.rb @@ -0,0 +1,296 @@ +require 'spec_helper' + +describe UploadsController do + let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + describe "GET show" do + context "when viewing a user avatar" do + context "when signed in" do + before do + sign_in(user) + end + + context "when the user is blocked" do + before do + user.block + end + + it "redirects to the sign in page" do + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when viewing a project avatar" do + let!(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 200" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context "when not signed in" do + it "redirects to the sign in page" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + before do + user.block + project.team << [user, :master] + end + + it "redirects to the sign in page" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + + expect(response.status).to eq(404) + end + end + end + end + end + + context "when viewing a group avatar" do + let!(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let!(:project) { create(:project, namespace: group) } + + context "when the group has public projects" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 200" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the project doesn't have public projects" do + context "when not signed in" do + it "redirects to the sign in page" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + before do + user.block + project.team << [user, :master] + end + + it "redirects to the sign in page" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + + expect(response.status).to eq(404) + end + end + end + end + end + + context "when viewing a note attachment" do + let!(:note) { create(:note, :with_attachment) } + let(:project) { note.project } + + context "when the project is public" do + before do + project.update_attribute(:visibility_level, Project::PUBLIC) + end + + context "when not signed in" do + it "responds with status 200" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + it "responds with status 200" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the project is private" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + context "when not signed in" do + it "redirects to the sign in page" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the user has access to the project" do + before do + project.team << [user, :master] + end + + context "when the user is blocked" do + before do + user.block + project.team << [user, :master] + end + + it "redirects to the sign in page" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when the user isn't blocked" do + it "responds with status 200" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(200) + end + end + end + + context "when the user doesn't have access to the project" do + it "responds with status 404" do + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + + expect(response.status).to eq(404) + end + end + end + end + end + end +end -- GitLab From cadf76562a7fc7d4f341c440ce3d5e301f4c87d1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 10:30:07 -0700 Subject: [PATCH 1268/1609] Fix highlight of selected lines --- CHANGELOG | 1 + app/assets/stylesheets/highlight/dark.scss | 4 ++-- app/assets/stylesheets/highlight/monokai.scss | 2 +- app/assets/stylesheets/highlight/solarized_dark.scss | 4 ++-- app/assets/stylesheets/highlight/solarized_light.scss | 4 ++-- app/assets/stylesheets/highlight/white.scss | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9b9e5832558..635546e2452 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,7 @@ v 7.9.0 (unreleased) - Wrap commit message in EmailsOnPush email. - Send EmailsOnPush emails when deleting commits using force push. - Fix EmailsOnPush email comparison link to include first commit. + - Fix highliht of selected lines in file v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index fcd4d47bace..df95625716a 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -13,8 +13,8 @@ } // highlight line via anchor - pre.hll { - background-color: #fff !important; + pre .hll { + background-color: #557 !important; } .hll { background-color: #373b41 } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index bcd2e716657..8f6b9edc7f5 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -13,7 +13,7 @@ } // highlight line via anchor - pre.hll { + pre .hll { background-color: #49483e !important; } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 4a6b759bd2c..97926f906e5 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -13,8 +13,8 @@ } // highlight line via anchor - pre.hll { - background-color: #073642 !important; + pre .hll { + background-color: #174652 !important; } /* Solarized Dark diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 7254f4d7ac1..9398067781f 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -13,8 +13,8 @@ } // highlight line via anchor - pre.hll { - background-color: #eee8d5 !important; + pre .hll { + background-color: #ddd8c5 !important; } /* Solarized Light diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 4d6f5dfd91e..c8217d3e6bf 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -13,7 +13,7 @@ } // highlight line via anchor - pre.hll { + pre .hll { background-color: #f8eec7 !important; } -- GitLab From 1178dacdb60e80d25700f984099dc6ce8dbf7efb Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 10:50:53 -0700 Subject: [PATCH 1269/1609] Fix line highlight being hidden by header --- app/assets/javascripts/blob/blob.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/blob/blob.js.coffee b/app/assets/javascripts/blob/blob.js.coffee index a5f15f80c5c..37a175fdbc7 100644 --- a/app/assets/javascripts/blob/blob.js.coffee +++ b/app/assets/javascripts/blob/blob.js.coffee @@ -26,7 +26,7 @@ class @BlobView unless isNaN first_line $("#tree-content-holder .highlight .line").removeClass("hll") $("#LC#{line}").addClass("hll") for line in [first_line..last_line] - $.scrollTo("#L#{first_line}") unless e? + $.scrollTo("#L#{first_line}", offset: -50) unless e? # parse selected lines from hash # always return first and last line (initialized to NaN) -- GitLab From 02ed61c4db6fd6c20e3a23e525ccc3aef174a874 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 6 Mar 2015 21:09:14 +0200 Subject: [PATCH 1270/1609] remove duplication --- ...0150306023106_fix_namespace_duplication.rb | 21 +++++++++++++++++++ ...306023112_add_unique_index_to_namespace.rb | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20150306023106_fix_namespace_duplication.rb diff --git a/db/migrate/20150306023106_fix_namespace_duplication.rb b/db/migrate/20150306023106_fix_namespace_duplication.rb new file mode 100644 index 00000000000..334e5574559 --- /dev/null +++ b/db/migrate/20150306023106_fix_namespace_duplication.rb @@ -0,0 +1,21 @@ +class FixNamespaceDuplication < ActiveRecord::Migration + def up + #fixes path duplication + select_all('SELECT MAX(id) max, COUNT(id) cnt, path FROM namespaces GROUP BY path HAVING COUNT(id) > 1').each do |nms| + bad_nms_ids = select_all("SELECT id FROM namespaces WHERE path = '#{nms['path']}' AND id <> #{nms['max']}").map{|x| x["id"]} + execute("UPDATE projects SET namespace_id = #{nms["max"]} WHERE namespace_id IN(#{bad_nms_ids.join(', ')})") + execute("DELETE FROM namespaces WHERE id IN(#{bad_nms_ids.join(', ')})") + end + + #fixes name duplication + select_all('SELECT MAX(id) max, COUNT(id) cnt, name FROM namespaces GROUP BY name HAVING COUNT(id) > 1').each do |nms| + bad_nms_ids = select_all("SELECT id FROM namespaces WHERE name = '#{nms['name']}' AND id <> #{nms['max']}").map{|x| x["id"]} + execute("UPDATE projects SET namespace_id = #{nms["max"]} WHERE namespace_id IN(#{bad_nms_ids.join(', ')})") + execute("DELETE FROM namespaces WHERE id IN(#{bad_nms_ids.join(', ')})") + end + end + + def down + # not implemented + end +end diff --git a/db/migrate/20150306023112_add_unique_index_to_namespace.rb b/db/migrate/20150306023112_add_unique_index_to_namespace.rb index b1f7822b4dc..6472138e3ef 100644 --- a/db/migrate/20150306023112_add_unique_index_to_namespace.rb +++ b/db/migrate/20150306023112_add_unique_index_to_namespace.rb @@ -1,7 +1,7 @@ class AddUniqueIndexToNamespace < ActiveRecord::Migration def change - remove_index :namespaces, :name - remove_index :namespaces, :path + remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) + remove_index :namespaces, column: :path if index_exists?(:namespaces, :path) add_index :namespaces, :name, unique: true add_index :namespaces, :path, unique: true -- GitLab From ae7e3806324fbe1ab63e68da823472fcbe31d652 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 12:03:04 -0700 Subject: [PATCH 1271/1609] Add active users to gitlab:check --- lib/tasks/gitlab/check.rake | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 43115915de1..976c4b5f22f 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -29,6 +29,7 @@ namespace :gitlab do check_redis_version check_ruby_version check_git_version + check_active_users finished_checking "GitLab" end @@ -781,6 +782,10 @@ namespace :gitlab do end end + def check_active_users + puts "Active users: #{User.active.count}" + end + def omnibus_gitlab? Dir.pwd == '/opt/gitlab/embedded/service/gitlab-rails' end -- GitLab From 0d4328fd00617bd23cd91f212e964b86c70792f7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 12:03:35 -0700 Subject: [PATCH 1272/1609] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 9b9e5832558..8428739a17b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,7 @@ v 7.9.0 (unreleased) - Wrap commit message in EmailsOnPush email. - Send EmailsOnPush emails when deleting commits using force push. - Fix EmailsOnPush email comparison link to include first commit. + - Add GitLab active users count to rake gitlab:check v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 -- GitLab From c572bdb587bada094af867839a60a26a1fc1423e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 12:10:26 -0700 Subject: [PATCH 1273/1609] Add CHANGELOG item about db migration --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index a7b274eb1dd..b1152a40383 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -46,6 +46,7 @@ v 7.9.0 (unreleased) - Fix EmailsOnPush email comparison link to include first commit. - Fix highliht of selected lines in file - Reject access to group/project avatar if the user doesn't have access. + - Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update) v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 -- GitLab From ea7478d6b5a464d888a1aa7743a0076470f8bd3d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 12:59:39 -0700 Subject: [PATCH 1274/1609] Fix features checkboxes at admin settings page --- .../application_settings/_form.html.haml | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index ac64d26f9aa..520f327f4e7 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -8,22 +8,30 @@ %fieldset %legend Features .form-group - = f.label :signup_enabled, class: 'control-label col-sm-2' - .col-sm-10 - = f.check_box :signup_enabled, class: 'checkbox form-control' - .form-group - = f.label :signin_enabled, class: 'control-label col-sm-2' - .col-sm-10 - = f.check_box :signin_enabled, class: 'checkbox form-control' - .form-group - = f.label :gravatar_enabled, class: 'control-label col-sm-2' - .col-sm-10 - = f.check_box :gravatar_enabled, class: 'checkbox form-control' - .form-group - = f.label :twitter_sharing_enabled, "Twitter enabled", class: 'control-label col-sm-2' - .col-sm-10 - = f.check_box :twitter_sharing_enabled, class: 'checkbox form-control', :'aria-describedby' => 'twitter_help_block' - %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :signup_enabled do + = f.check_box :signup_enabled + Signin enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :signin_enabled do + = f.check_box :signin_enabled + Signup enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :gravatar_enabled do + = f.check_box :gravatar_enabled + Gravatar enabled + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :twitter_sharing_enabled do + = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' + %strong Twitter enabled + %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter %fieldset %legend Misc .form-group -- GitLab From 7fd4dc1e11e772095123008c79796202b5b7c80d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 13:17:16 -0700 Subject: [PATCH 1275/1609] Remove group rendering from dashboard page --- app/controllers/dashboard_controller.rb | 6 ------ app/views/dashboard/_groups.html.haml | 21 ------------------- app/views/dashboard/_sidebar.html.haml | 17 +-------------- .../_zero_authorized_projects.html.haml | 5 +++-- app/views/dashboard/groups/index.html.haml | 1 + app/views/dashboard/show.html.haml | 2 +- 6 files changed, 6 insertions(+), 46 deletions(-) delete mode 100644 app/views/dashboard/_groups.html.haml diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 8f06a673584..2822d510e50 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -5,15 +5,9 @@ class DashboardController < ApplicationController before_filter :event_filter, only: :show def show - @projects_limit = 20 - @groups = current_user.authorized_groups.order_name_asc - @has_authorized_projects = @projects.count > 0 - @projects_count = @projects.count @projects = @projects.includes(:namespace) @last_push = current_user.recent_push - @publicish_project_count = Project.publicish(current_user).count - respond_to do |format| format.html diff --git a/app/views/dashboard/_groups.html.haml b/app/views/dashboard/_groups.html.haml deleted file mode 100644 index e3df43d8892..00000000000 --- a/app/views/dashboard/_groups.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -.panel.panel-default - .panel-heading.clearfix - .input-group - = search_field_tag :filter_group, nil, placeholder: 'Filter by name', class: 'dash-filter form-control' - - if current_user.can_create_group? - .input-group-addon.dash-new-group - = link_to new_group_path, class: "" do - %strong New group - %ul.well-list.dash-list - - groups.each do |group| - %li.group-row - = link_to group_path(id: group.path), class: dom_class(group) do - .dash-project-avatar - = image_tag group_icon(group.path), class: "avatar s40" - %span.group-name.filter-title - = truncate(group.name, length: 35) - %span.arrow - %i.fa.fa-angle-right - - if groups.blank? - %li - .nothing-here-block You have no groups yet. diff --git a/app/views/dashboard/_sidebar.html.haml b/app/views/dashboard/_sidebar.html.haml index 983da4aba04..78f695be916 100644 --- a/app/views/dashboard/_sidebar.html.haml +++ b/app/views/dashboard/_sidebar.html.haml @@ -1,18 +1,3 @@ -%ul.nav.nav-tabs.dash-sidebar-tabs - %li.active - = link_to '#projects', 'data-toggle' => 'tab', id: 'sidebar-projects-tab' do - Projects - %span.badge= @projects_count - %li - = link_to '#groups', 'data-toggle' => 'tab', id: 'sidebar-groups-tab' do - Groups - %span.badge= @groups.count - -.tab-content - .tab-pane.active#projects - = render "dashboard/projects", projects: @projects - .tab-pane#groups - = render "dashboard/groups", groups: @groups - += render "dashboard/projects", projects: @projects .prepend-top-20 = render 'shared/promo' diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/_zero_authorized_projects.html.haml index 6e76f95b34e..4e7d6639727 100644 --- a/app/views/dashboard/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/_zero_authorized_projects.html.haml @@ -1,3 +1,4 @@ +- publicish_project_count = Project.publicish(current_user).count %h3.page-title Welcome to GitLab! %p.light Self hosted Git management application. %hr @@ -35,7 +36,7 @@ %i.fa.fa-plus New Group --if @publicish_project_count > 0 +-if publicish_project_count > 0 %hr %div .dashboard-intro-icon @@ -43,7 +44,7 @@ .dashboard-intro-text %p.slead There are - %strong= @publicish_project_count + %strong= publicish_project_count public projects on this server. %br Public projects are an easy way to allow everyone to have read-only access. diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index fd7bbb5500c..50e90b1c170 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -27,6 +27,7 @@ %i.fa.fa-sign-out Leave + = image_tag group_icon(group.path), class: "avatar s40 avatar-tile" = link_to group, class: 'group-name' do %strong= group.name diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml index f973f4829a0..fa8946011b7 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/show.html.haml @@ -1,4 +1,4 @@ -- if @has_authorized_projects +- if @projects.any? .dashboard.row %section.activities.col-md-8 = render 'activities' -- GitLab From 04c674e6792ad638dfb505f06f4cf9e980bb4a74 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 13:27:58 -0700 Subject: [PATCH 1276/1609] Cleanup after removing group tab from dashboard aside --- app/assets/javascripts/dashboard.js.coffee | 12 --------- app/assets/stylesheets/pages/dashboard.scss | 28 --------------------- 2 files changed, 40 deletions(-) diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee index 3bdb9469d06..00ee503ff16 100644 --- a/app/assets/javascripts/dashboard.js.coffee +++ b/app/assets/javascripts/dashboard.js.coffee @@ -1,15 +1,3 @@ class @Dashboard constructor: -> - @initSidebarTab() new ProjectsList() - - initSidebarTab: -> - key = "dashboard_sidebar_filter" - - # store selection in cookie - $('.dash-sidebar-tabs a').on 'click', (e) -> - $.cookie(key, $(e.target).attr('id')) - - # show tab from cookie - sidebar_filter = $.cookie(key) - $("#" + sidebar_filter).tab('show') if sidebar_filter diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 96f84b7122b..5a543a852c2 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -23,25 +23,6 @@ } } -.dash-sidebar-tabs { - margin-bottom: 2px; - border: none; - margin: 0 !important; - - li { - &.active { - a { - background-color: whitesmoke !important; - border-bottom: 1px solid whitesmoke !important; - } - } - - a { - border-color: #DDD !important; - } - } -} - .project-row, .group-row { padding: 0 !important; font-size: 14px; @@ -116,15 +97,6 @@ } } -.dash-new-group { - background: $gl-success; - border: 1px solid $gl-success; - - a { - color: #FFF; - } -} - .dash-list .str-truncated { max-width: 72%; } -- GitLab From 8527e8d5996bb5543cb5a13e857b864b466a31f2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 15:19:28 -0700 Subject: [PATCH 1277/1609] Fix test for creating group from dashboard --- features/dashboard/group.feature | 8 ++++++++ features/groups.feature | 8 -------- features/steps/dashboard/group.rb | 19 +++++++++++++++++++ features/steps/groups.rb | 19 ------------------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature index 92c1379ba77..cf4b8d7283b 100644 --- a/features/dashboard/group.feature +++ b/features/dashboard/group.feature @@ -46,3 +46,11 @@ Feature: Dashboard Group When I visit dashboard groups page Then I should see group "Owned" in group list Then I should not see group "Guest" in group list + + Scenario: Create a group from dasboard + And I visit dashboard groups page + And I click new group link + And submit form with new group "Samurai" info + Then I should be redirected to group "Samurai" page + And I should see newly created group "Samurai" + diff --git a/features/groups.feature b/features/groups.feature index b5ff03db844..05546e0d6ef 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -10,14 +10,6 @@ Feature: Groups Then I should see group "Owned" projects list And I should see projects activity feed - Scenario: Create a group from dasboard - When I visit group "Owned" page - And I visit dashboard page - And I click new group link - And submit form with new group "Samurai" info - Then I should be redirected to group "Samurai" page - And I should see newly created group "Samurai" - Scenario: I should see group "Owned" issues list Given project from group "Owned" has issues assigned to me When I visit group "Owned" issues page diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb index 09d7717b67b..8384df2fb59 100644 --- a/features/steps/dashboard/group.rb +++ b/features/steps/dashboard/group.rb @@ -41,4 +41,23 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps step 'I should not see group "Guest" in group list' do page.should_not have_content("Guest") end + + step 'I click new group link' do + click_link "New Group" + end + + step 'submit form with new group "Samurai" info' do + fill_in 'group_path', with: 'Samurai' + fill_in 'group_description', with: 'Tokugawa Shogunate' + click_button "Create group" + end + + step 'I should be redirected to group "Samurai" page' do + current_path.should == group_path(Group.find_by(name: 'Samurai')) + end + + step 'I should see newly created group "Samurai"' do + page.should have_content "Samurai" + page.should have_content "Tokugawa Shogunate" + end end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index c3c34070e2e..91921f5e21c 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -72,25 +72,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps author: current_user end - When 'I click new group link' do - click_link "New group" - end - - step 'submit form with new group "Samurai" info' do - fill_in 'group_path', with: 'Samurai' - fill_in 'group_description', with: 'Tokugawa Shogunate' - click_button "Create group" - end - - step 'I should be redirected to group "Samurai" page' do - current_path.should == group_path(Group.find_by(name: 'Samurai')) - end - - step 'I should see newly created group "Samurai"' do - page.should have_content "Samurai" - page.should have_content "Tokugawa Shogunate" - end - step 'I change group "Owned" name to "new-name"' do fill_in 'group_name', with: 'new-name' fill_in 'group_path', with: 'new-name' -- GitLab From 83f7e98d9a672158d5c754307ab471fd50c5b2a3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 15:59:14 -0700 Subject: [PATCH 1278/1609] Add project filter by visibility and tag to explore page --- app/controllers/dashboard_controller.rb | 1 - .../explore/projects_controller.rb | 3 + app/helpers/dashboard_helper.rb | 16 --- app/helpers/explore_helper.rb | 17 +++ .../dashboard/_projects_filter.html.haml | 100 ------------------ app/views/explore/projects/_filter.html.haml | 67 ++++++++++++ app/views/explore/projects/index.html.haml | 27 +---- app/views/layouts/nav/_dashboard.html.haml | 1 - 8 files changed, 88 insertions(+), 144 deletions(-) create mode 100644 app/helpers/explore_helper.rb delete mode 100644 app/views/dashboard/_projects_filter.html.haml create mode 100644 app/views/explore/projects/_filter.html.haml diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 2822d510e50..b6e300e84b6 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -36,7 +36,6 @@ class DashboardController < ApplicationController end @projects = @projects.where(namespace_id: Group.find_by(name: params[:group])) if params[:group].present? - @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.includes(:namespace, :forked_from_project, :tags) @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = @projects.sort(@sort = params[:sort]) diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 0e5891ae807..d664624fa69 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -6,6 +6,9 @@ class Explore::ProjectsController < ApplicationController def index @projects = ProjectsFinder.new.execute(current_user) + @tags = @projects.tags_on(:tags) + @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? + @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.search(params[:search]) if params[:search].present? @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).page(params[:page]).per(20) diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index 4dae96644c8..c25b54eadc6 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -1,20 +1,4 @@ module DashboardHelper - def projects_dashboard_filter_path(options={}) - exist_opts = { - sort: params[:sort], - scope: params[:scope], - group: params[:group], - tag: params[:tag], - visibility_level: params[:visibility_level], - } - - options = exist_opts.merge(options) - - path = request.path - path << "?#{options.to_param}" - path - end - def assigned_issues_dashboard_path issues_dashboard_path(assignee_id: current_user.id) end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb new file mode 100644 index 00000000000..7616fe6bad8 --- /dev/null +++ b/app/helpers/explore_helper.rb @@ -0,0 +1,17 @@ +module ExploreHelper + def explore_projects_filter_path(options={}) + exist_opts = { + sort: params[:sort], + scope: params[:scope], + group: params[:group], + tag: params[:tag], + visibility_level: params[:visibility_level], + } + + options = exist_opts.merge(options) + + path = request.path + path << "?#{options.to_param}" + path + end +end diff --git a/app/views/dashboard/_projects_filter.html.haml b/app/views/dashboard/_projects_filter.html.haml deleted file mode 100644 index d87ca861aed..00000000000 --- a/app/views/dashboard/_projects_filter.html.haml +++ /dev/null @@ -1,100 +0,0 @@ -.dash-projects-filters.append-bottom-20 - .append-right-20 - %ul.nav.nav-tabs - = nav_tab :scope, nil do - = link_to projects_dashboard_filter_path(scope: nil) do - All - = nav_tab :scope, 'personal' do - = link_to projects_dashboard_filter_path(scope: 'personal') do - Personal - = nav_tab :scope, 'joined' do - = link_to projects_dashboard_filter_path(scope: 'joined') do - Joined - = nav_tab :scope, 'owned' do - = link_to projects_dashboard_filter_path(scope: 'owned') do - Owned - - .dropdown.inline.append-right-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-globe - %span.light Visibility: - - if params[:visibility_level].present? - = visibility_level_label(params[:visibility_level].to_i) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to projects_dashboard_filter_path(visibility_level: nil) do - Any - - Gitlab::VisibilityLevel.values.each do |level| - %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } - = link_to projects_dashboard_filter_path(visibility_level: level) do - = visibility_level_icon(level) - = visibility_level_label(level) - - - if @groups.present? - .dropdown.inline.append-right-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-group - %span.light Group: - - if params[:group].present? - = Group.find_by(name: params[:group]).name - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to projects_dashboard_filter_path(group: nil) do - Any - - @groups.each do |group| - %li{ class: (group.name == params[:group]) ? 'active' : 'light' } - = link_to projects_dashboard_filter_path(group: group.name) do - = group.name - %small.pull-right - = group.projects.count - - - - - if @tags.present? - .dropdown.inline.append-right-10 - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %i.fa.fa-tags - %span.light Tags: - - if params[:tag].present? - = params[:tag] - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to projects_dashboard_filter_path(tag: nil) do - Any - - - @tags.each do |tag| - %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } - = link_to projects_dashboard_filter_path(tag: tag.name) do - %i.fa.fa-tag - = tag.name - - .pull-right - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light sort: - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to projects_dashboard_filter_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to projects_dashboard_filter_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to projects_dashboard_filter_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to projects_dashboard_filter_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - = link_to projects_dashboard_filter_path(sort: sort_value_name) do - = sort_title_name diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml new file mode 100644 index 00000000000..b3963a9d901 --- /dev/null +++ b/app/views/explore/projects/_filter.html.haml @@ -0,0 +1,67 @@ +.pull-left + = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f| + .form-group + = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search" + .form-group + = button_tag 'Search', class: "btn btn-primary wide" + +.pull-right.hidden-sm.hidden-xs + - if current_user + .dropdown.inline.append-right-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-globe + %span.light Visibility: + - if params[:visibility_level].present? + = visibility_level_label(params[:visibility_level].to_i) + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to explore_projects_filter_path(visibility_level: nil) do + Any + - Gitlab::VisibilityLevel.values.each do |level| + %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } + = link_to explore_projects_filter_path(visibility_level: level) do + = visibility_level_icon(level) + = visibility_level_label(level) + + - if @tags.present? + .dropdown.inline.append-right-10 + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %i.fa.fa-tags + %span.light Tags: + - if params[:tag].present? + = params[:tag] + - else + Any + %b.caret + %ul.dropdown-menu + %li + = link_to explore_projects_filter_path(tag: nil) do + Any + + - @tags.each do |tag| + %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } + = link_to explore_projects_filter_path(tag: tag.name) do + %i.fa.fa-tag + = tag.name + + .dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light sort: + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu + %li + = link_to explore_projects_filter_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index cb93b300d6a..5086b58cd03 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -1,30 +1,5 @@ .clearfix - .pull-left - = form_tag explore_projects_path, method: :get, class: 'form-inline form-tiny' do |f| - .form-group - = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search" - .form-group - = button_tag 'Search', class: "btn btn-primary wide" - - .pull-right - .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to explore_projects_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to explore_projects_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to explore_projects_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to explore_projects_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated + = render 'filter' %hr .public-projects diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index b21f25e87cf..c24dd4efc32 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -41,4 +41,3 @@ %i.fa.fa-question-circle %span Help - -- GitLab From 0414b2ae98180f1a462aae5300ba0fde94614cb4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 16:03:37 -0700 Subject: [PATCH 1279/1609] Remove projects page from dashboard --- app/controllers/dashboard_controller.rb | 22 -------- app/views/dashboard/projects.html.haml | 60 ---------------------- app/views/layouts/nav/_dashboard.html.haml | 5 -- config/routes.rb | 1 - features/dashboard/projects.feature | 9 ---- features/steps/dashboard/projects.rb | 11 ---- 6 files changed, 108 deletions(-) delete mode 100644 app/views/dashboard/projects.html.haml delete mode 100644 features/dashboard/projects.feature delete mode 100644 features/steps/dashboard/projects.rb diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index b6e300e84b6..05006199091 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -23,28 +23,6 @@ class DashboardController < ApplicationController end end - def projects - @projects = case params[:scope] - when 'personal' then - current_user.namespace.projects - when 'joined' then - current_user.authorized_projects.joined(current_user) - when 'owned' then - current_user.owned_projects - else - current_user.authorized_projects - end - - @projects = @projects.where(namespace_id: Group.find_by(name: params[:group])) if params[:group].present? - @projects = @projects.includes(:namespace, :forked_from_project, :tags) - @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? - @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.page(params[:page]).per(30) - - @tags = current_user.authorized_projects.tags_on(:tags) - @groups = current_user.authorized_groups - end - def merge_requests @merge_requests = get_merge_requests_collection @merge_requests = @merge_requests.page(params[:page]).per(20) diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml deleted file mode 100644 index 03d4b3d8bbb..00000000000 --- a/app/views/dashboard/projects.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -%h3.page-title - My Projects - - = link_to new_project_path, class: "btn btn-new pull-right" do - %i.fa.fa-plus - New Project - -%p.light - All projects you have access to are listed here. Public projects are not included here unless you are a member -%hr -.side-filters - = render "projects_filter" -.dash-projects - %ul.bordered-list.my-projects.top-list - - @projects.each do |project| - %li.my-project-row - %h4.project-title - .pull-left - = project_icon(project, alt: '', class: 'avatar project-avatar s60') - .project-access-icon - = visibility_level_icon(project.visibility_level) - = link_to project_path(project), class: dom_class(project) do - %strong= project.name_with_namespace - - - if project.forked_from_project -   - %small - %i.fa.fa-code-fork - Forked from: - = link_to project.forked_from_project.name_with_namespace, namespace_project_path(project.namespace, project.forked_from_project) - - - if current_user.can_leave_project?(project) - .pull-right - = link_to leave_namespace_project_team_members_path(project.namespace, project), data: { confirm: "Leave project?"}, method: :delete, remote: true, class: "btn-tiny btn remove-row", title: 'Leave project' do - %i.fa.fa-sign-out - Leave - - .project-info - .pull-right - - if project.archived? - %span.label - %i.fa.fa-archive - Archived - - project.tags.each do |tag| - %span.label.label-info - %i.fa.fa-tag - = tag.name - - if project.description.present? - %p= truncate project.description, length: 100 - .last-activity - %span.light Last activity: - %span.date= project_last_activity(project) - - - - if @projects.blank? - %li - .nothing-here-block There are no projects here. - .bottom - = paginate @projects, theme: "gitlab" - diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index c24dd4efc32..73b68d08e9b 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -2,11 +2,6 @@ = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do = link_to root_path, title: 'Home', class: 'shortcuts-activity' do %i.fa.fa-dashboard - %span - Activity - = nav_link(path: 'dashboard#projects') do - = link_to projects_dashboard_path, title: 'Projects', class: 'shortcuts-projects' do - %i.fa.fa-cube %span Projects = nav_link(path: 'projects#starred') do diff --git a/config/routes.rb b/config/routes.rb index 637b855e661..889995e92a6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -208,7 +208,6 @@ Gitlab::Application.routes.draw do # resource :dashboard, controller: 'dashboard', only: [:show] do member do - get :projects get :issues get :merge_requests end diff --git a/features/dashboard/projects.feature b/features/dashboard/projects.feature deleted file mode 100644 index bb4e84f0159..00000000000 --- a/features/dashboard/projects.feature +++ /dev/null @@ -1,9 +0,0 @@ -@dashboard -Feature: Dashboard Projects - Background: - Given I sign in as a user - And I own project "Shop" - And I visit dashboard projects page - - Scenario: I should see projects list - Then I should see projects list diff --git a/features/steps/dashboard/projects.rb b/features/steps/dashboard/projects.rb deleted file mode 100644 index 2a348163060..00000000000 --- a/features/steps/dashboard/projects.rb +++ /dev/null @@ -1,11 +0,0 @@ -class Spinach::Features::DashboardProjects < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - step 'I should see projects list' do - @user.authorized_projects.all.each do |project| - page.should have_link project.name_with_namespace - end - end -end -- GitLab From 9839d106c46a2c035c19337cfce0a905e2e1fea3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 16:06:30 -0700 Subject: [PATCH 1280/1609] Rename dashboard landing page to Your projects --- app/views/layouts/nav/_dashboard.html.haml | 2 +- features/steps/shared/active_tab.rb | 2 +- spec/features/security/dashboard_access_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 73b68d08e9b..e4f630c6a18 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -3,7 +3,7 @@ = link_to root_path, title: 'Home', class: 'shortcuts-activity' do %i.fa.fa-dashboard %span - Projects + Your Projects = nav_link(path: 'projects#starred') do = link_to starred_dashboard_projects_path, title: 'Starred Projects' do %i.fa.fa-star diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index c229864bc83..9beb688bd16 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -26,7 +26,7 @@ module SharedActiveTab end step 'the active main tab should be Home' do - ensure_active_main_tab('Activity') + ensure_active_main_tab('Your Projects') end step 'the active main tab should be Projects' do diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index 3d2d8a3502c..67238e3ab76 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -25,8 +25,8 @@ describe "Dashboard access", feature: true do it { is_expected.to be_denied_for :visitor } end - describe "GET /dashboard/projects" do - subject { projects_dashboard_path } + describe "GET /dashboard/projects/starred" do + subject { starred_dashboard_projects_path } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :user } -- GitLab From 9623b71a3975bb442b85aa57146b788f96de6320 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Tue, 10 Mar 2015 18:21:09 -0600 Subject: [PATCH 1281/1609] More restricted visibility changes Bug fixes and new tests for the restricted visibility changes. --- app/helpers/application_settings_helper.rb | 3 ++- lib/api/project_snippets.rb | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 2b0d8860f9b..241d6075c9f 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -29,7 +29,8 @@ module ApplicationSettingsHelper checkbox_name = 'application_setting[restricted_visibility_levels][]' label_tag(checkbox_name, class: css_class) do - check_box_tag(checkbox_name, level, checked, autocomplete: 'off', + check_box_tag(checkbox_name, level, checked, + autocomplete: 'off', 'aria-describedby' => help_block_id) + name end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 25f34a3dab5..54f2555903f 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -51,13 +51,13 @@ module API attrs = attributes_for_keys [:title, :file_name, :visibility_level] attrs[:content] = params[:code] if params[:code].present? - @snippet = CreateSnippetservice.new(user_project, current_user, + @snippet = CreateSnippetService.new(user_project, current_user, attrs).execute - if @snippet.saved? - present @snippet, with: Entities::ProjectSnippet - else + if @snippet.errors.any? render_validation_error!(@snippet) + else + present @snippet, with: Entities::ProjectSnippet end end -- GitLab From 6d0ff0ade8c136e1093a7f3336a58b361abdfd51 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 10 Mar 2015 18:09:13 -0700 Subject: [PATCH 1282/1609] Remove placeholder methods to prevent calling methods rather than attributes. --- app/models/project_services/issue_tracker_service.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 0c734a544d2..8e90c44d103 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -30,18 +30,6 @@ class IssueTrackerService < Service false end - def project_url - # implement inside child - end - - def issues_url - # implement inside child - end - - def new_issue_url - # implement inside child - end - def issue_url(iid) self.issues_url.gsub(':id', iid.to_s) end -- GitLab From 61ed518781cfc78bf1710286d84d4e74521f48d4 Mon Sep 17 00:00:00 2001 From: Vyacheslav Slinko Date: Fri, 2 Jan 2015 15:47:22 +0300 Subject: [PATCH 1283/1609] Make email display name configurable --- CHANGELOG | 3 ++- app/mailers/notify.rb | 2 +- config/gitlab.yml.example | 1 + config/initializers/1_settings.rb | 1 + spec/mailers/notify_spec.rb | 3 ++- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b210a6b0155..52997ee8c95 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,7 +33,8 @@ v 7.9.0 (unreleased) - Send notifications and leave system comments when bulk updating issues. - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) - Move groups page from profile to dashboard - - Starred projects page at dashboard + - Starred projects page at dashboard + - Make email display name configurable v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 65925b61e94..ee27879cf40 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -53,7 +53,7 @@ class Notify < ActionMailer::Base # The default email address to send emails from def default_sender_address address = Mail::Address.new(Gitlab.config.gitlab.email_from) - address.display_name = "GitLab" + address.display_name = Gitlab.config.gitlab.email_display_name address end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 6dff07cf9df..75d9e65aefe 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -43,6 +43,7 @@ production: &base # email_enabled: true # Email address used in the "From" field in mails sent by GitLab email_from: example@example.com + email_display_name: GitLab # Email server smtp settings are in config/initializers/smtp_settings.rb.sample diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 6a8bbb80b9c..70af7a829c4 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -102,6 +102,7 @@ Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || '' Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http" Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil? Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}" +Settings.gitlab['email_display_name'] ||= "GitLab" Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' Settings.gitlab['user_home'] ||= begin diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 4090fa46205..b3c507ccbe2 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -5,6 +5,7 @@ describe Notify do include EmailSpec::Matchers include RepoHelpers + let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } let(:gitlab_sender) { Gitlab.config.gitlab.email_from } let(:recipient) { create(:user, email: 'recipient@example.com') } let(:project) { create(:project) } @@ -23,7 +24,7 @@ describe Notify do shared_examples 'an email sent from GitLab' do it 'is sent from GitLab' do sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq('GitLab') + expect(sender.display_name).to eq(gitlab_sender_display_name) expect(sender.address).to eq(gitlab_sender) end end -- GitLab From 4b1bb42bf77328e2f80e40933d53037a1144bf0a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 21:56:19 -0700 Subject: [PATCH 1284/1609] Fix tests for project removing --- app/controllers/projects_controller.rb | 2 +- features/dashboard/archived_projects.feature | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 82b8a1cc13a..fad692c7a34 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -105,7 +105,7 @@ class ProjectsController < ApplicationController if request.referer.include?('/admin') redirect_to admin_namespaces_projects_path else - redirect_to projects_dashboard_path + redirect_to dashboard_path end end end diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature index 3af93bc373c..69b3a776441 100644 --- a/features/dashboard/archived_projects.feature +++ b/features/dashboard/archived_projects.feature @@ -10,8 +10,3 @@ Feature: Dashboard Archived Projects Scenario: I should see non-archived projects on dashboard Then I should see "Shop" project link And I should not see "Forum" project link - - Scenario: I should see all projects on projects page - And I visit dashboard projects page - Then I should see "Shop" project link - And I should see "Forum" project link -- GitLab From 9ed71f77fee46fc489af8a6150cf12c1c6c468c1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 10 Mar 2015 23:34:18 -0700 Subject: [PATCH 1285/1609] Remove tests for un-existing page --- features/project/archived.feature | 9 --------- 1 file changed, 9 deletions(-) diff --git a/features/project/archived.feature b/features/project/archived.feature index 9aac29384ba..ad466f4f307 100644 --- a/features/project/archived.feature +++ b/features/project/archived.feature @@ -14,15 +14,6 @@ Feature: Project Archived And I visit project "Forum" page Then I should see "Archived" - Scenario: I should not see archived on projects page with no archived projects - And I visit dashboard projects page - Then I should not see "Archived" - - Scenario: I should see archived on projects page with archived projects - And project "Forum" is archived - And I visit dashboard projects page - Then I should see "Archived" - Scenario: I archive project When project "Shop" has push event And I visit project "Shop" page -- GitLab From 536d4373737cf5c4710d949fb4819d99faf2cdd7 Mon Sep 17 00:00:00 2001 From: Job van der Voort Date: Wed, 11 Mar 2015 17:31:39 +0100 Subject: [PATCH 1286/1609] add AMI update step --- doc/release/monthly.md | 4 ++++ doc/release/patch.md | 1 + 2 files changed, 5 insertions(+) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index dd44c1eb860..a7e5faf2a6e 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -200,3 +200,7 @@ Consider creating a post on Hacker News. ## Update GitLab.com with the stable version - Deploy the package (should not need downtime because of the small difference with RC1) + +## Release new AMIs + +[Follow this guide](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) diff --git a/doc/release/patch.md b/doc/release/patch.md index 80afa19b6c5..5397343e715 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -54,3 +54,4 @@ CE=false be rake release['x.x.x'] 1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md) 1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post 1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only) +1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) -- GitLab From 11e966d7a93ec0a745cde65021fa79a6a6b24667 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 11 Mar 2015 17:43:40 +0100 Subject: [PATCH 1287/1609] Add changelog item. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 81468d4013e..7a5f115c678 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,7 @@ v 7.9.0 (unreleased) - Move groups page from profile to dashboard - Starred projects page at dashboard - Blocking user does not remove him/her from project/groups but show blocked label + - Allow smb:// links in Markdown text. v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 -- GitLab From 88e4aed9df812eee9af2f27a975eeac894580042 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 11 Mar 2015 13:02:49 -0700 Subject: [PATCH 1288/1609] Update changelog for 7.8.4. --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 03e3fb303e8..90ed6864481 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -52,6 +52,12 @@ v 7.9.0 (unreleased) - Starred projects page at dashboard - Make email display name configurable - Improve json validation in hook data +v 7.8.4 + - Fix issue_tracker_id substitution in custom issue trackers + - Fix path and name duplication in namespaces + +v 7.8.3 + - Bump version of gitlab_git fixing annotated tags without message v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 -- GitLab From 5c4f567ea021a48fa53ef6c1b235775ec511fe09 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 11 Mar 2015 13:52:55 -0700 Subject: [PATCH 1289/1609] Bump gitlab_git to v7.1.0. It should fix a lot of encoding issues --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 462c932584d..77480262487 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '7.0.1' +gem "gitlab_git", '7.1.0' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' diff --git a/Gemfile.lock b/Gemfile.lock index cca8f59ac28..1332e096478 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -213,7 +213,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.0.1.1) emoji (~> 1.0.1) - gitlab_git (7.0.1) + gitlab_git (7.1.0) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -607,7 +607,7 @@ GEM eventmachine (>= 1.0.0) rack (>= 1.0.0) thor (0.19.1) - thread_safe (0.3.4) + thread_safe (0.3.5) tilt (1.4.1) timers (4.0.1) hitimes @@ -708,7 +708,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.0.1.1) - gitlab_git (= 7.0.1) + gitlab_git (= 7.1.0) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.0) gollum-lib (~> 4.0.0) -- GitLab From d2d709a252ec4c26894b269a03df871fe51e8b82 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 11 Mar 2015 16:05:01 -0700 Subject: [PATCH 1290/1609] Update html-pipeline and emoji --- Gemfile | 4 ++-- Gemfile.lock | 17 +++++++++-------- app/controllers/projects_controller.rb | 8 ++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index 462c932584d..506dedddb85 100644 --- a/Gemfile +++ b/Gemfile @@ -88,7 +88,7 @@ gem "six" gem "seed-fu" # Markup pipeline for GitLab -gem 'html-pipeline-gitlab', '~> 0.1.0' +gem 'html-pipeline-gitlab', '~> 0.1' # Markdown to HTML gem "github-markup" @@ -194,7 +194,7 @@ gem "jquery-scrollto-rails" gem "raphael-rails", "~> 2.1.2" gem 'bootstrap-sass', '~> 3.0' gem "font-awesome-rails", '~> 4.2' -gem "gitlab_emoji", "~> 0.0.1.1" +gem "gitlab_emoji", "~> 0.1" gem "gon", '~> 5.0.0' gem 'nprogress-rails' gem 'request_store' diff --git a/Gemfile.lock b/Gemfile.lock index cca8f59ac28..32bbfbdc2de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -144,8 +144,6 @@ GEM email_spec (1.5.0) launchy (~> 2.1) mail (~> 2.2) - emoji (1.0.1) - json enumerize (0.7.0) activesupport (>= 3.2) equalizer (0.0.8) @@ -193,6 +191,8 @@ GEM formatador (0.2.4) gemnasium-gitlab-service (0.2.5) rugged (~> 0.21) + gemojione (2.0.0) + json gherkin-ruby (0.3.1) racc github-markup (1.3.1) @@ -211,8 +211,8 @@ GEM charlock_holmes (~> 0.6.6) escape_utils (~> 0.2.4) mime-types (~> 1.19) - gitlab_emoji (0.0.1.1) - emoji (~> 1.0.1) + gitlab_emoji (0.1.0) + gemojione (~> 2.0) gitlab_git (7.0.1) activesupport (~> 4.0) charlock_holmes (~> 0.6) @@ -278,10 +278,11 @@ GEM html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) - html-pipeline-gitlab (0.1.5) + html-pipeline-gitlab (0.2.0) actionpack (~> 4) - gitlab_emoji (~> 0.0.1) + gitlab_emoji (~> 0.1) html-pipeline (~> 1.11.0) + mime-types sanitize (~> 2.1) http_parser.rb (0.5.3) httparty (0.13.0) @@ -707,7 +708,7 @@ DEPENDENCIES gitlab-flowdock-git-hook (~> 0.4.2) gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) - gitlab_emoji (~> 0.0.1.1) + gitlab_emoji (~> 0.1) gitlab_git (= 7.0.1) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.0) @@ -720,7 +721,7 @@ DEPENDENCIES guard-spinach haml-rails hipchat (~> 1.4.0) - html-pipeline-gitlab (~> 0.1.0) + html-pipeline-gitlab (~> 0.1) httparty jasmine (= 2.0.2) jquery-atwho-rails (~> 0.3.3) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fad692c7a34..0f28794b736 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -176,11 +176,11 @@ class ProjectsController < ApplicationController end def autocomplete_emojis - Rails.cache.fetch("autocomplete-emoji-#{Emoji::VERSION}") do - Emoji.names.map do |e| + Rails.cache.fetch("autocomplete-emoji-#{Gemojione::VERSION}") do + Emoji.emojis.map do |name, emoji| { - name: e, - path: view_context.image_url("emoji/#{e}.png") + name: name, + path: view_context.image_url("emoji/#{emoji["unicode"]}.png") } end end -- GitLab From 5f40253f7696e920614328246120805fbf79d97c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 11 Mar 2015 16:07:33 -0700 Subject: [PATCH 1291/1609] Remove annoying notice messages when create/update merge request --- app/controllers/projects/merge_requests_controller.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 848cf367493..57c017e799f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -78,10 +78,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute if @merge_request.valid? - redirect_to( - merge_request_path(@merge_request), - notice: 'Merge request was successfully created.' - ) + redirect_to(merge_request_path(@merge_request)) else @source_project = @merge_request.source_project @target_project = @merge_request.target_project @@ -97,8 +94,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController format.js format.html do redirect_to([@merge_request.target_project.namespace.becomes(Namespace), - @merge_request.target_project, @merge_request], - notice: 'Merge request was successfully updated.') + @merge_request.target_project, @merge_request]) end format.json do render json: { -- GitLab From 66384d7d4434f9185dbe8bfe22cb7db974763ed3 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 11 Mar 2015 17:50:02 -0700 Subject: [PATCH 1292/1609] Add a note about building AMI to security doc --- doc/release/security.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/release/security.md b/doc/release/security.md index 1575fcf2708..6ed25264498 100644 --- a/doc/release/security.md +++ b/doc/release/security.md @@ -18,6 +18,7 @@ Please report suspected security vulnerabilities in private to Date: Wed, 11 Mar 2015 17:52:02 -0700 Subject: [PATCH 1293/1609] Fix tests for emojione --- CHANGELOG | 2 ++ spec/helpers/gitlab_markdown_helper_spec.rb | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 90ed6864481..fef266d2eec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -52,6 +52,8 @@ v 7.9.0 (unreleased) - Starred projects page at dashboard - Make email display name configurable - Improve json validation in hook data + - Use Emoji One + v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers - Fix path and name duplication in namespaces diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 74a42932fe8..fd80c615221 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -664,19 +664,19 @@ describe GitlabMarkdownHelper do it "should generate absolute urls for emoji" do expect(markdown(':smile:')).to( - include(%(src="#{Gitlab.config.gitlab.url}/assets/emoji/smile.png)) + include(%(src="#{Gitlab.config.gitlab.url}/assets/emoji/#{Emoji.emoji_filename('smile')}.png)) ) end it "should generate absolute urls for emoji if relative url is present" do allow(Gitlab.config.gitlab).to receive(:url).and_return('http://localhost/gitlab/root') - expect(markdown(":smile:")).to include("src=\"http://localhost/gitlab/root/assets/emoji/smile.png") + expect(markdown(":smile:")).to include("src=\"http://localhost/gitlab/root/assets/emoji/#{Emoji.emoji_filename('smile')}.png") end it "should generate absolute urls for emoji if asset_host is present" do allow(Gitlab::Application.config).to receive(:asset_host).and_return("https://cdn.example.com") ActionView::Base.any_instance.stub_chain(:config, :asset_host).and_return("https://cdn.example.com") - expect(markdown(":smile:")).to include("src=\"https://cdn.example.com/assets/emoji/smile.png") + expect(markdown(":smile:")).to include("src=\"https://cdn.example.com/assets/emoji/#{Emoji.emoji_filename('smile')}.png") end -- GitLab From ccc2c6e762cba3b25ce8fe842b35aef837d7ffd0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 11 Mar 2015 18:03:21 -0700 Subject: [PATCH 1294/1609] Fix Gemfile.lock --- Gemfile.lock | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 65c5b2e3a03..c847424a7c4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -708,13 +708,8 @@ DEPENDENCIES gitlab-flowdock-git-hook (~> 0.4.2) gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) -<<<<<<< HEAD gitlab_emoji (~> 0.1) - gitlab_git (= 7.0.1) -======= - gitlab_emoji (~> 0.0.1.1) gitlab_git (= 7.1.0) ->>>>>>> master gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.0) gollum-lib (~> 4.0.0) -- GitLab From 3f823068e1f6e3e88d6631de60d9aaf9ecd5e6f9 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 11 Mar 2015 21:11:32 -0700 Subject: [PATCH 1295/1609] Add deploy to ci.gitlab.com to release documents. --- doc/release/monthly.md | 2 ++ doc/release/patch.md | 1 + doc/release/security.md | 1 + 3 files changed, 4 insertions(+) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index a7e5faf2a6e..ec96be27f3f 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -51,6 +51,7 @@ Xth: (5 working days before the 22nd) Xth: (4 working days before the 22nd) - [ ] Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) +- [ ] Update ci.gitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package) - [ ] Create regression issues (CE, CI) (#LINK) - [ ] Tweet about rc1 (#LINK) @@ -68,6 +69,7 @@ Xth: (1 working day before the 22nd) - [ ] Create CE, EE, CI stable versions (#LINK) - [ ] Create Omnibus tags and build packages - [ ] Update GitLab.com with the stable version (#LINK) +- [ ] Update ci.gitLab.com with the stable version (#LINK) 22nd: diff --git a/doc/release/patch.md b/doc/release/patch.md index 5397343e715..68156ae9c0e 100644 --- a/doc/release/patch.md +++ b/doc/release/patch.md @@ -51,6 +51,7 @@ CE=false be rake release['x.x.x'] 1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md) 1. Apply the patch to GitLab.com and the private GitLab development server +1. Apply the patch to ci.gitLab.com and the private GitLab CI development server 1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md) 1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post 1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only) diff --git a/doc/release/security.md b/doc/release/security.md index 6ed25264498..60bcfbb6da5 100644 --- a/doc/release/security.md +++ b/doc/release/security.md @@ -18,6 +18,7 @@ Please report suspected security vulnerabilities in private to Date: Wed, 11 Mar 2015 21:29:11 -0700 Subject: [PATCH 1296/1609] Add blue theme to GitLab --- app/assets/stylesheets/pages/profile.scss | 4 ++++ app/assets/stylesheets/themes/ui_blue.scss | 6 ++++++ app/views/profiles/design.html.haml | 5 +++++ lib/gitlab/theme.rb | 4 +++- 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 app/assets/stylesheets/themes/ui_blue.scss diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 0ab62b7ae49..81afe05162f 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -80,6 +80,10 @@ &.violet { background: #548; } + + &.blue { + background: #2980b9; + } } } } diff --git a/app/assets/stylesheets/themes/ui_blue.scss b/app/assets/stylesheets/themes/ui_blue.scss new file mode 100644 index 00000000000..cb7980b5a07 --- /dev/null +++ b/app/assets/stylesheets/themes/ui_blue.scss @@ -0,0 +1,6 @@ +/** + * Modern GitLab UI theme + */ +.ui_blue { + @include dark-theme(#BECDE9, #2980b9, #1970a9, #096099); +} diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/design.html.haml index 8d09595fd4f..cc00d08d03b 100644 --- a/app/views/profiles/design.html.haml +++ b/app/views/profiles/design.html.haml @@ -33,6 +33,11 @@ .prev.violet = f.radio_button :theme_id, 5 Violet + + = label_tag do + .prev.blue + = f.radio_button :theme_id, 6 + Blue %br .clearfix diff --git a/lib/gitlab/theme.rb b/lib/gitlab/theme.rb index a7c83a880f6..9799e54de5d 100644 --- a/lib/gitlab/theme.rb +++ b/lib/gitlab/theme.rb @@ -5,6 +5,7 @@ module Gitlab MODERN = 3 unless const_defined?(:MODERN) GRAY = 4 unless const_defined?(:GRAY) COLOR = 5 unless const_defined?(:COLOR) + BLUE = 6 unless const_defined?(:BLUE) def self.css_class_by_id(id) themes = { @@ -12,7 +13,8 @@ module Gitlab MARS => "ui_mars", MODERN => "ui_modern", GRAY => "ui_gray", - COLOR => "ui_color" + COLOR => "ui_color", + BLUE => "ui_blue" } id ||= Gitlab.config.gitlab.default_theme -- GitLab From 3c7e0f45c2d9c242bf45d153bb73e96ce7525a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Wed, 4 Mar 2015 12:58:40 +0100 Subject: [PATCH 1297/1609] replace images in emails with inline images This adds the functionality of replacing all images that were uploaded to gitlab with inline images(base64) in emails. This change fixes the broken images in emails that 7.8 introduced --- CHANGELOG | 1 + app/helpers/emails_helper.rb | 25 ++++++ app/views/notify/_note_message.html.haml | 2 +- app/views/notify/new_issue_email.html.haml | 2 +- .../notify/new_merge_request_email.html.haml | 2 +- spec/mailers/notify_spec.rb | 82 ++++++++++++++++++- 6 files changed, 108 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3b263a7d4fa..e1e22102ed5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) + - Fix broken email images (Hannes Rosenögger) - Fix mass SQL statements on initial push (Hannes Rosenögger) - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) - Add comment notification events to HipChat and Slack services (Stan Hu) diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 92cc9c426b8..08476f8516e 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -1,3 +1,6 @@ +require 'html/pipeline' +require 'html/pipeline/gitlab' + module EmailsHelper # Google Actions @@ -39,4 +42,26 @@ module EmailsHelper lexer = Rugments::Lexers::Diff.new raw formatter.format(lexer.lex(diffcontent)) end + + def replace_image_links_with_base64(text, project) + # Used pipelines in GitLab: + # GitlabEmailImageFilter - replaces images that have been uploaded as attachments with inline images in emails. + # + # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters + filters = [ + HTML::Pipeline::Gitlab::GitlabEmailImageFilter + ] + + context = { + base_url: File.join(Gitlab.config.gitlab.url, project.path_with_namespace, 'uploads'), + upload_path: File.join(Rails.root, 'public', 'uploads', project.path_with_namespace), + } + + pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline + + result = pipeline.call(text, context) + text = result[:output].to_html(save_with: 0) + + text.html_safe + end end diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 5272dfa0ede..778a78acf56 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,2 +1,2 @@ %div - = markdown(@note.note) + = replace_image_links_with_base64(markdown(@note.note), @note.project) diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index f2f8eee18c4..03cbee94608 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,5 +1,5 @@ -if @issue.description - = markdown(@issue.description) + = replace_image_links_with_base64(markdown(@issue.description), @issue.project) - if @issue.assignee_id.present? %p diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index f02d5111b22..729a7bb505d 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -6,4 +6,4 @@ Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} -if @merge_request.description - = markdown(@merge_request.description) + = replace_image_links_with_base64(markdown(@merge_request.description), @merge_request.project) diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index b3c507ccbe2..e3a3b542358 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -183,6 +183,13 @@ describe Notify do context 'for issues' do let(:issue) { create(:issue, author: current_user, assignee: assignee, project: project) } let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: Faker::Lorem.sentence) } + let(:issue_with_image) do + create(:issue, + author: current_user, + assignee: assignee, + project: project, + description: "![test](#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/12345/test.jpg)") + end describe 'that are new' do subject { Notify.new_issue_email(issue.assignee_id, issue.id) } @@ -207,6 +214,22 @@ describe Notify do end end + describe 'that contain images' do + let(:png) { File.read("#{Rails.root}/spec/fixtures/dk.png") } + let(:png_encoded) { Base64::encode64(png) } + + before :each do + file_path = File.join(Rails.root, 'public', 'uploads', issue_with_image.project.path_with_namespace, '12345/test.jpg') + allow(File).to receive(:file?).with(file_path).and_return(true) + allow(File).to receive(:read).with(file_path).and_return(png) + end + + subject { Notify.new_issue_email(issue_with_image.assignee_id, issue_with_image.id) } + it 'replaces attached images with inline images' do + is_expected.to have_body_text URI.encode(png_encoded) + end + end + describe 'that have been reassigned' do subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user) } @@ -271,6 +294,14 @@ describe Notify do let(:merge_author) { create(:user) } let(:merge_request) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project) } let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: Faker::Lorem.sentence) } + let(:merge_request_with_image) do + create(:merge_request, + author: current_user, + assignee: assignee, + source_project: project, + target_project: project, + description: "![test](#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/12345/test.jpg)") + end describe 'that are new' do subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } @@ -307,6 +338,22 @@ describe Notify do end end + describe 'that are new and contain contain images in the description' do + let(:png) {File.read("#{Rails.root}/spec/fixtures/dk.png")} + let(:png_encoded) { Base64::encode64(png) } + + before :each do + file_path = File.join(Rails.root, 'public', 'uploads', merge_request_with_image.project.path_with_namespace, '/12345/test.jpg') + allow(File).to receive(:file?).with(file_path).and_return(true) + allow(File).to receive(:read).with(file_path).and_return(png) + end + + subject { Notify.new_merge_request_email(merge_request_with_image.assignee_id, merge_request_with_image.id) } + it 'replaces attached images with inline images' do + is_expected.to have_body_text URI.encode(png_encoded) + end + end + describe 'that are reassigned' do subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) } @@ -415,9 +462,12 @@ describe Notify do describe 'project access changed' do let(:project) { create(:project) } let(:user) { create(:user) } - let(:project_member) { create(:project_member, - project: project, - user: user) } + let(:project_member) do + create(:project_member, + project: project, + user: user) + end + subject { Notify.project_access_granted_email(project_member.id) } it_behaves_like 'an email sent from GitLab' @@ -457,6 +507,32 @@ describe Notify do end end + describe 'on a commit that contains an image' do + let(:commit) { project.repository.commit } + let(:note_with_image) do + create(:note, + project: project, + author: note_author, + note: "![test](#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/12345/test.jpg)") + end + + let(:png) {File.read("#{Rails.root}/spec/fixtures/dk.png")} + let(:png_encoded) { Base64::encode64(png) } + + before :each do + file_path = File.join(Rails.root, 'public', 'uploads', note_with_image.project.path_with_namespace, '12345/test.jpg') + allow(File).to receive(:file?).with(file_path).and_return(true) + allow(File).to receive(:read).with(file_path).and_return(png) + allow(Note).to receive(:find).with(note_with_image.id).and_return(note_with_image) + allow(note_with_image).to receive(:noteable).and_return(commit) + end + + subject { Notify.note_commit_email(recipient.id, note_with_image.id) } + it 'replaces attached images with inline images' do + is_expected.to have_body_text URI.encode(png_encoded) + end + end + describe 'on a commit' do let(:commit) { project.repository.commit } -- GitLab From 3175438f02ca4bc0469aca097e02b2671865ef43 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 12 Mar 2015 13:47:15 +0100 Subject: [PATCH 1298/1609] Fix missing GitHub organisation repositories on import page. --- CHANGELOG | 1 + app/controllers/import/github_controller.rb | 2 +- spec/controllers/import/github_controller_spec.rb | 7 +++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9b9e5832558..5a5fb4f18a7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,7 @@ v 7.9.0 (unreleased) - Wrap commit message in EmailsOnPush email. - Send EmailsOnPush emails when deleting commits using force push. - Fix EmailsOnPush email comparison link to include first commit. + - Fix missing GitHub organisation repositories on import page. v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index dc7668ee6fd..8650b6464dc 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -14,7 +14,7 @@ class Import::GithubController < Import::BaseController def status @repos = client.repos client.orgs.each do |org| - @repos += client.repos(org.login) + @repos += client.org_repos(org.login) end @already_added_projects = current_user.created_projects.where(import_type: "github") diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index b8820413406..5b967bfcc0c 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -27,17 +27,20 @@ describe Import::GithubController do describe "GET status" do before do @repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim') + @org = OpenStruct.new(login: 'company') + @org_repo = OpenStruct.new(login: 'company', full_name: 'company/repo') end it "assigns variables" do @project = create(:project, import_type: 'github', creator_id: user.id) controller.stub_chain(:client, :repos).and_return([@repo]) - controller.stub_chain(:client, :orgs).and_return([]) + controller.stub_chain(:client, :orgs).and_return([@org]) + controller.stub_chain(:client, :org_repos).with(@org.login).and_return([@org_repo]) get :status expect(assigns(:already_added_projects)).to eq([@project]) - expect(assigns(:repos)).to eq([@repo]) + expect(assigns(:repos)).to eq([@repo, @org_repo]) end it "does not show already added project" do -- GitLab From 0b38c3e04138984123592da78ad78c79fdeaec3d Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 12 Mar 2015 17:08:48 +0200 Subject: [PATCH 1299/1609] group controller refactoring --- app/controllers/groups/application_controller.rb | 10 ++++++++++ app/controllers/groups/group_members_controller.rb | 8 +------- app/controllers/groups_controller.rb | 8 +------- 3 files changed, 12 insertions(+), 14 deletions(-) create mode 100644 app/controllers/groups/application_controller.rb diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb new file mode 100644 index 00000000000..7f27f2bb734 --- /dev/null +++ b/app/controllers/groups/application_controller.rb @@ -0,0 +1,10 @@ +class Groups::ApplicationController < ApplicationController + + private + + def authorize_admin_group! + unless can?(current_user, :manage_group, group) + return render_404 + end + end +end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index ca88d033878..b083cf5d8c5 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,4 +1,4 @@ -class Groups::GroupMembersController < ApplicationController +class Groups::GroupMembersController < Groups::ApplicationController before_filter :group # Authorize @@ -37,12 +37,6 @@ class Groups::GroupMembersController < ApplicationController @group ||= Group.find_by(path: params[:group_id]) end - def authorize_admin_group! - unless can?(current_user, :manage_group, group) - return render_404 - end - end - def member_params params.require(:group_member).permit(:access_level, :user_id) end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index d011523c94f..89f94fa0d45 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,4 +1,4 @@ -class GroupsController < ApplicationController +class GroupsController < Groups::ApplicationController skip_before_filter :authenticate_user!, only: [:show, :issues, :members, :merge_requests] respond_to :html before_filter :group, except: [:new, :create] @@ -132,12 +132,6 @@ class GroupsController < ApplicationController end end - def authorize_admin_group! - unless can?(current_user, :manage_group, group) - return render_404 - end - end - def set_title @title = 'New Group' end -- GitLab From fff34a7f4d58ff0add8c2cb043a4fd53d004bd71 Mon Sep 17 00:00:00 2001 From: Cameron Banga Date: Thu, 12 Mar 2015 10:49:46 -0500 Subject: [PATCH 1300/1609] Updated help documentation to properly reference EmojiOne. [ci skip] --- CHANGELOG | 1 + doc/markdown/markdown.md | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 90ed6864481..fec90f1e220 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -52,6 +52,7 @@ v 7.9.0 (unreleased) - Starred projects page at dashboard - Make email display name configurable - Improve json validation in hook data + - Updated emoji help documentation to properly reference EmojiOne. v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers - Fix path and name duplication in namespaces diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 1096ea9656c..52779103775 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -140,25 +140,25 @@ But let's throw in a tag. ## Emoji - Sometimes you want to be a :ninja: and add some :glowing_star: to your :speech_balloon:. Well we have a gift for you: + Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: - :high_voltage_sign: You can use emoji anywhere GFM is supported. :victory_hand: + :zap: You can use emoji anywhere GFM is supported. :v: - You can use it to point out a :bug: or warn about :speak_no_evil_monkey: patches. And if someone improves your really :snail: code, send them some :cake:. People will :heart: you for that. + You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. - If you are new to this, don't be :fearful_face:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. + If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. - Consult the [Emoji Cheat Sheet](https://s3.amazonaws.com/emoji-cheatsheet/cheat_sheet.pdf) for a list of all supported emoji codes. :thumbsup: + Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup: -Sometimes you want to be a :ninja: and add some :glowing_star: to your :speech_balloon:. Well we have a gift for you: +Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: -:high_voltage_sign: You can use emoji anywhere GFM is supported. :victory_hand: +:zap: You can use emoji anywhere GFM is supported. :v: -You can use it to point out a :bug: or warn about :speak_no_evil_monkey: patches. And if someone improves your really :snail: code, send them some :cake:. People will :heart: you for that. +You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. -If you are new to this, don't be :fearful_face:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. +If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. -Consult the [Emoji Cheat Sheet](https://s3.amazonaws.com/emoji-cheatsheet/cheat_sheet.pdf) for a list of all supported emoji codes. :thumbsup: +Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup: ## Special GitLab References -- GitLab From dd78cd1ce4699afb76ee430e7d82b852333805d8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 12 Mar 2015 10:04:09 -0700 Subject: [PATCH 1301/1609] Style improvements to import page --- .../javascripts/importer_status.js.coffee | 10 ++-- app/assets/stylesheets/base/gl_bootstrap.scss | 47 +++++++++++++++++++ app/views/import/bitbucket/status.html.haml | 2 +- app/views/import/github/status.html.haml | 2 +- app/views/import/gitlab/status.html.haml | 2 +- app/views/import/gitorious/status.html.haml | 2 +- 6 files changed, 56 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee index e0e7771ab20..be8d225e73b 100644 --- a/app/assets/javascripts/importer_status.js.coffee +++ b/app/assets/javascripts/importer_status.js.coffee @@ -16,20 +16,20 @@ class @ImporterStatus $(".js-import-all").click (event) => $(".js-add-to-import").each -> $(this).click() - + setAutoUpdate: -> setInterval (=> $.get @jobs_url, (data) => $.each data, (i, job) => job_item = $("#project_" + job.id) status_field = job_item.find(".job-status") - + if job.import_status == 'finished' job_item.removeClass("active").addClass("success") - status_field.html(' done') + status_field.html(' done') else if job.import_status == 'started' status_field.html(" started") else status_field.html(job.import_status) - - ), 4000 \ No newline at end of file + + ), 4000 diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index 0775c171817..16581e9ebf2 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -197,3 +197,50 @@ text-decoration: underline; } } + +// Typography ================================================================= + +.text-primary, +.text-primary:hover { + color: $brand-primary; +} + +.text-success, +.text-success:hover { + color: $brand-success; +} + +.text-danger, +.text-danger:hover { + color: $brand-danger; +} + +.text-warning, +.text-warning:hover { + color: $brand-warning; +} + +.text-info, +.text-info:hover { + color: $brand-info; +} + +// Tables ===================================================================== + +table.table { + .dropdown-menu a { + text-decoration: none; + } + + .success, + .warning, + .danger, + .info { + color: #fff; + + a:not(.btn) { + text-decoration: underline; + color: #fff; + } + } +} diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index bcbbaadf3e0..9da3c920c62 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -23,7 +23,7 @@ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' - %span.cgreen + %span %i.fa.fa-check done - elsif project.import_status == 'started' diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 883090a3026..9c4d91013ec 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -23,7 +23,7 @@ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' - %span.cgreen + %span %i.fa.fa-check done - elsif project.import_status == 'started' diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index 41ac073eae1..e809643d8d4 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -23,7 +23,7 @@ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' - %span.cgreen + %span %i.fa.fa-check done - elsif project.import_status == 'started' diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index ebe24747a05..645241a6c69 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -23,7 +23,7 @@ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status - if project.import_status == 'finished' - %span.cgreen + %span %i.fa.fa-check done - elsif project.import_status == 'started' -- GitLab From a1f5ae98e2afde84b028d17b489d7461f64a03d9 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 12 Mar 2015 10:10:19 -0700 Subject: [PATCH 1302/1609] Show asterisks instead of password in service edit form. --- app/helpers/projects_helper.rb | 10 ++++++++++ app/views/projects/services/_form.html.haml | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a5d7372bbe5..2225b110651 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -265,4 +265,14 @@ module ProjectsHelper "success" end end + + def service_field_value(type, value) + return value unless type == 'password' + + if value.present? + "***********" + else + nil + end + end end diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index eda59e6708b..3492dd5babd 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -75,7 +75,7 @@ - @service.fields.each do |field| - name = field[:name] - title = field[:title] || name.humanize - - value = @service.send(name) unless field[:type] == 'password' + - value = service_field_value(field[:type], @service.send(name)) - type = field[:type] - placeholder = field[:placeholder] - choices = field[:choices] @@ -94,7 +94,7 @@ - elsif type == 'select' = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - elsif type == 'password' - = f.password_field name, class: 'form-control' + = f.password_field name, placeholder: value, class: 'form-control' - if help %span.help-block= help -- GitLab From cdb64a81a8ca96961033b8ab06d5191ef5449634 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 12 Mar 2015 10:20:24 -0700 Subject: [PATCH 1303/1609] Add items into changelog --- CHANGELOG | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index abb47191f89..0ec9f3177e2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -53,16 +53,18 @@ v 7.9.0 (unreleased) - Make email display name configurable - Improve json validation in hook data - Use Emoji One - - Updated emoji help documentation to properly reference EmojiOne. + - Fix missing GitHub organisation repositories on import page. + - Added blue thmeme + - Remove annoying notice messages when create/update merge request + - Allow smb:// links in Markdown text. + v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers - Fix path and name duplication in namespaces v 7.8.3 - Bump version of gitlab_git fixing annotated tags without message - - Allow smb:// links in Markdown text. - - Fix missing GitHub organisation repositories on import page. v 7.8.2 - Fix service migration issue when upgrading from versions prior to 7.3 -- GitLab From 7ff9c8229d8df09be5927086222aad0cd5d71477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Thu, 8 Jan 2015 18:15:50 +0100 Subject: [PATCH 1304/1609] Add a service to support external wikis --- CHANGELOG | 1 + .../projects/services_controller.rb | 2 +- app/helpers/external_wiki_helper.rb | 11 +++++ app/models/project.rb | 1 + .../project_services/external_wiki_service.rb | 48 +++++++++++++++++++ app/views/layouts/nav/_project.html.haml | 2 +- spec/models/external_wiki_service_spec.rb | 39 +++++++++++++++ 7 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 app/helpers/external_wiki_helper.rb create mode 100644 app/models/project_services/external_wiki_service.rb create mode 100644 spec/models/external_wiki_service_spec.rb diff --git a/CHANGELOG b/CHANGELOG index b0adaeb101b..1d334edc904 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 7.9.0 (unreleased) - Fix merge request URL passed to Webhooks. (Stan Hu) - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) - Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu) + - Add a service to support external wikis (Hannes Rosenögger) - Move labels/milestones tabs to sidebar - Upgrade Rails gem to version 4.1.9. - Improve error messages for file edit failures diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 570447c746c..9a484c109ba 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -53,7 +53,7 @@ class Projects::ServicesController < Projects::ApplicationController :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :colorize_messages, :channels, :push_events, :issues_events, :merge_requests_events, :tag_push_events, - :note_events, :send_from_committer_email, :disable_diffs + :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url ) end end diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb new file mode 100644 index 00000000000..838b85afdfe --- /dev/null +++ b/app/helpers/external_wiki_helper.rb @@ -0,0 +1,11 @@ +module ExternalWikiHelper + def get_project_wiki_path(project) + external_wiki_service = project.services. + select { |service| service.to_param == 'external_wiki' }.first + if external_wiki_service.present? && external_wiki_service.active? + external_wiki_service.properties['external_wiki_url'] + else + namespace_project_wiki_path(project.namespace, project, :home) + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index c45338bf4eb..5fbda1ae86c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -93,6 +93,7 @@ class Project < ActiveRecord::Base has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link + has_one :external_wiki_service, dependent: :destroy # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' # Merge requests from source project should be kept when source project was removed diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb new file mode 100644 index 00000000000..e521186798c --- /dev/null +++ b/app/models/project_services/external_wiki_service.rb @@ -0,0 +1,48 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + +class ExternalWikiService < Service + include HTTParty + + prop_accessor :external_wiki_url + validates :external_wiki_url, + presence: true, + format: { with: URI::regexp }, + if: :activated? + + def title + 'External Wiki' + end + + def description + 'Replaces the link to the internal wiki with a link to an external wiki.' + end + + def to_param + 'external_wiki' + end + + def fields + [ + { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki' }, + ] + end + + def execute(_data) + @response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil + if @response !=200 + nil + end + end +end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index d340ab1796a..91cae2b572c 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -75,7 +75,7 @@ - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = link_to namespace_project_wiki_path(@project.namespace, @project, :home), title: 'Wiki', class: 'shortcuts-wiki' do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do %i.fa.fa-book %span Wiki diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb new file mode 100644 index 00000000000..78ef687d29c --- /dev/null +++ b/spec/models/external_wiki_service_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe ExternalWikiService do + include ExternalWikiHelper + describe "Associations" do + it { should belong_to :project } + it { should have_one :service_hook } + end + + describe "Validations" do + context "active" do + before do + subject.active = true + end + + it { should validate_presence_of :external_wiki_url } + end + end + + describe 'External wiki' do + let(:project) { create(:project) } + + context 'when it is active' do + before do + properties = { 'external_wiki_url' => 'https://gitlab.com' } + @service = project.create_external_wiki_service(active: true, properties: properties) + end + + after do + @service.destroy! + end + + it 'should replace the wiki url' do + wiki_path = get_project_wiki_path(project) + wiki_path.should match('https://gitlab.com') + end + end + end +end -- GitLab From e7f4f0ae1db4b0d940d0c4f1e4b32bebf9e6c299 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 12 Mar 2015 11:53:21 -0700 Subject: [PATCH 1305/1609] Block user if he/she was blocked in Active Directory --- CHANGELOG | 1 + lib/gitlab/ldap/access.rb | 9 ++++++++- spec/lib/gitlab/ldap/access_spec.rb | 7 ++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b0adaeb101b..3e0bf6e700a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,6 +59,7 @@ v 7.9.0 (unreleased) - Added blue thmeme - Remove annoying notice messages when create/update merge request - Allow smb:// links in Markdown text. + - Block user if he/she was blocked in Active Directory v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 0c85acf7e69..6e30724e1f7 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -34,7 +34,14 @@ module Gitlab def allowed? if Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) return true unless ldap_config.active_directory - !Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) + + # Block user in GitLab if he/she was blocked in AD + if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) + user.block unless user.blocked? + false + else + true + end else false end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index a2b05249147..39d46efcbc3 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -20,6 +20,11 @@ describe Gitlab::LDAP::Access do before { Gitlab::LDAP::Person.stub(disabled_via_active_directory?: true) } it { is_expected.to be_falsey } + + it "should block user in GitLab" do + access.allowed? + user.should be_blocked + end end context 'and has no disabled flag in active diretory' do @@ -38,4 +43,4 @@ describe Gitlab::LDAP::Access do end end end -end \ No newline at end of file +end -- GitLab From 7ac62388a5ba030dd60e6aef971eb32adf39ed0d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 12 Mar 2015 13:56:53 -0700 Subject: [PATCH 1306/1609] Prevent database query each time we render group avatar --- app/helpers/application_helper.rb | 9 --------- app/helpers/groups_helper.rb | 12 ++++++++++++ app/helpers/namespaces_helper.rb | 2 +- app/views/admin/groups/show.html.haml | 2 +- app/views/dashboard/groups/index.html.haml | 2 +- app/views/groups/edit.html.haml | 2 +- app/views/groups/show.html.haml | 2 +- app/views/users/_groups.html.haml | 2 +- 8 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a81e41819b7..8ed6d59c20d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -86,15 +86,6 @@ module ApplicationHelper end end - def group_icon(group_path) - group = Group.find_by(path: group_path) - if group && group.avatar.present? - group.avatar.url - else - image_path('no_group_avatar.png') - end - end - def avatar_icon(user_email = '', size = nil) user = User.find_by(email: user_email) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 03fd461a462..2d0d0b494f6 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -40,4 +40,16 @@ module GroupsHelper false end end + + def group_icon(group) + if group.is_a?(String) + group = Group.find_by(path: group) + end + + if group && group.avatar.present? + group.avatar.url + else + image_path('no_group_avatar.png') + end + end end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 2bcfde62830..b3132a1f3ba 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -28,7 +28,7 @@ module NamespacesHelper def namespace_icon(namespace, size = 40) if namespace.kind_of?(Group) - group_icon(namespace.path) + group_icon(namespace) else avatar_icon(namespace.owner.email, size) end diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index bb7f1972925..3040faa722b 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -12,7 +12,7 @@ Group info: %ul.well-list %li - = image_tag group_icon(@group.path), class: "avatar s60" + = image_tag group_icon(@group), class: "avatar s60" %li %span.light Name: %strong= @group.name diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 50e90b1c170..f7df5352512 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -27,7 +27,7 @@ %i.fa.fa-sign-out Leave - = image_tag group_icon(group.path), class: "avatar s40 avatar-tile" + = image_tag group_icon(group), class: "avatar s40 avatar-tile" = link_to group, class: 'group-name' do %strong= group.name diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index c4eb00e8925..838290e4aca 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -12,7 +12,7 @@ .form-group .col-sm-2 .col-sm-10 - = image_tag group_icon(@group.to_param), alt: '', class: 'avatar group-avatar s160' + = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' %p.light - if @group.avatar? You can change your group avatar here diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index a453889f744..25efe973d4f 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,6 +1,6 @@ .dashboard %div - = image_tag group_icon(@group.path), class: "avatar group-avatar s90" + = image_tag group_icon(@group), class: "avatar group-avatar s90" .clearfix %h2 = @group.name diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml index cb84570a6d5..f360fbb3d5d 100644 --- a/app/views/users/_groups.html.haml +++ b/app/views/users/_groups.html.haml @@ -1,4 +1,4 @@ .clearfix - groups.each do |group| = link_to group, class: 'profile-groups-avatars inline', title: group.name do - = image_tag group_icon(group.path), class: 'avatar group-avatar s40' + = image_tag group_icon(group), class: 'avatar group-avatar s40' -- GitLab From bf0c04e5ff61cd68177e52c15128aea9b1a41427 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 12 Mar 2015 14:51:41 -0700 Subject: [PATCH 1307/1609] Fix specs --- spec/helpers/application_helper_spec.rb | 18 ------------------ spec/helpers/groups_helper.rb | 21 +++++++++++++++++++++ 2 files changed, 21 insertions(+), 18 deletions(-) create mode 100644 spec/helpers/groups_helper.rb diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 99ff8a32ea5..4c11709ed6e 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -39,24 +39,6 @@ describe ApplicationHelper do end end - describe 'group_icon' do - avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') - - it 'should return an url for the avatar' do - group = create(:group) - group.avatar = File.open(avatar_file_path) - group.save! - expect(group_icon(group.path).to_s). - to match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png") - end - - it 'should give default avatar_icon when no avatar is present' do - group = create(:group) - group.save! - expect(group_icon(group.path)).to match('group_avatar.png') - end - end - describe 'project_icon' do avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') diff --git a/spec/helpers/groups_helper.rb b/spec/helpers/groups_helper.rb new file mode 100644 index 00000000000..3e99ab84ec9 --- /dev/null +++ b/spec/helpers/groups_helper.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe GroupsHelper do + describe 'group_icon' do + avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') + + it 'should return an url for the avatar' do + group = create(:group) + group.avatar = File.open(avatar_file_path) + group.save! + expect(group_icon(group.path).to_s). + to match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png") + end + + it 'should give default avatar_icon when no avatar is present' do + group = create(:group) + group.save! + expect(group_icon(group.path)).to match('group_avatar.png') + end + end +end -- GitLab From f0cbbd70bba7c44e9b09a3472e6c2f6f58623150 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 12 Mar 2015 15:37:00 -0700 Subject: [PATCH 1308/1609] Use same constant for amount of items per page --- app/controllers/admin/groups_controller.rb | 6 +++--- app/controllers/admin/projects_controller.rb | 6 +++--- app/controllers/application_controller.rb | 2 ++ app/controllers/dashboard/groups_controller.rb | 2 +- app/controllers/dashboard/milestones_controller.rb | 2 +- app/controllers/dashboard_controller.rb | 4 ++-- app/controllers/explore/groups_controller.rb | 2 +- app/controllers/explore/projects_controller.rb | 6 +++--- app/controllers/groups/milestones_controller.rb | 2 +- app/controllers/groups_controller.rb | 4 ++-- app/controllers/profiles_controller.rb | 2 +- app/controllers/projects/branches_controller.rb | 2 +- app/controllers/projects/issues_controller.rb | 2 +- app/controllers/projects/labels_controller.rb | 2 +- app/controllers/projects/merge_requests_controller.rb | 2 +- app/controllers/projects/milestones_controller.rb | 2 +- app/controllers/projects/tags_controller.rb | 2 +- app/controllers/projects/wikis_controller.rb | 2 +- app/controllers/snippets_controller.rb | 4 ++-- 19 files changed, 29 insertions(+), 27 deletions(-) diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 65dc027c8eb..e338abeac4c 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -5,12 +5,12 @@ class Admin::GroupsController < Admin::ApplicationController @groups = Group.all @groups = @groups.sort(@sort = params[:sort]) @groups = @groups.search(params[:name]) if params[:name].present? - @groups = @groups.page(params[:page]).per(20) + @groups = @groups.page(params[:page]).per(PER_PAGE) end def show - @members = @group.members.order("access_level DESC").page(params[:members_page]).per(30) - @projects = @group.projects.page(params[:projects_page]).per(30) + @members = @group.members.order("access_level DESC").page(params[:members_page]).per(PER_PAGE) + @projects = @group.projects.page(params[:projects_page]).per(PER_PAGE) end def new diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 2b1fc862b7f..5176a8399ae 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -11,15 +11,15 @@ class Admin::ProjectsController < Admin::ApplicationController @projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(20) + @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(PER_PAGE) end def show if @group - @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(30) + @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(PER_PAGE) end - @project_members = @project.project_members.page(params[:project_members_page]).per(30) + @project_members = @project.project_members.page(params[:project_members_page]).per(PER_PAGE) end def transfer diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index df1a588313e..e284f31f7ee 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,8 @@ class ApplicationController < ActionController::Base include Gitlab::CurrentSettings include GitlabRoutingHelper + PER_PAGE = 20 + before_filter :authenticate_user_from_token! before_filter :authenticate_user! before_filter :reject_blocked! diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index 61d691e6368..b827639978c 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,6 +1,6 @@ class Dashboard::GroupsController < ApplicationController def index - @user_groups = current_user.group_members.page(params[:page]).per(20) + @user_groups = current_user.group_members.page(params[:page]).per(PER_PAGE) end def leave diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 386e283f3a0..cb51792df16 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -8,7 +8,7 @@ class Dashboard::MilestonesController < ApplicationController else state('active') end @dashboard_milestones = Milestones::GroupService.new(project_milestones).execute - @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(30) + @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE) end def show diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 05006199091..9bd853ed5c7 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -25,13 +25,13 @@ class DashboardController < ApplicationController def merge_requests @merge_requests = get_merge_requests_collection - @merge_requests = @merge_requests.page(params[:page]).per(20) + @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) @merge_requests = @merge_requests.preload(:author, :target_project) end def issues @issues = get_issues_collection - @issues = @issues.page(params[:page]).per(20) + @issues = @issues.page(params[:page]).per(PER_PAGE) @issues = @issues.preload(:author, :project) respond_to do |format| diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index ada7031fea4..c51a4a211a6 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -8,6 +8,6 @@ class Explore::GroupsController < ApplicationController @groups = GroupsFinder.new.execute(current_user) @groups = @groups.search(params[:search]) if params[:search].present? @groups = @groups.sort(@sort = params[:sort]) - @groups = @groups.page(params[:page]).per(20) + @groups = @groups.page(params[:page]).per(PER_PAGE) end end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index d664624fa69..b295f295bb1 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -11,17 +11,17 @@ class Explore::ProjectsController < ApplicationController @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.search(params[:search]) if params[:search].present? @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.includes(:namespace).page(params[:page]).per(20) + @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) end def trending @trending_projects = TrendingProjectsFinder.new.execute(current_user) - @trending_projects = @trending_projects.page(params[:page]).per(10) + @trending_projects = @trending_projects.page(params[:page]).per(PER_PAGE) end def starred @starred_projects = ProjectsFinder.new.execute(current_user) @starred_projects = @starred_projects.reorder('star_count DESC') - @starred_projects = @starred_projects.page(params[:page]).per(10) + @starred_projects = @starred_projects.page(params[:page]).per(PER_PAGE) end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 6802e529b54..c46b8fff88f 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -10,7 +10,7 @@ class Groups::MilestonesController < ApplicationController else state('active') end @group_milestones = Milestones::GroupService.new(project_milestones).execute - @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(30) + @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE) end def show diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 89f94fa0d45..7e336803fbb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -52,13 +52,13 @@ class GroupsController < Groups::ApplicationController def merge_requests @merge_requests = get_merge_requests_collection - @merge_requests = @merge_requests.page(params[:page]).per(20) + @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) @merge_requests = @merge_requests.preload(:author, :target_project) end def issues @issues = get_issues_collection - @issues = @issues.page(params[:page]).per(20) + @issues = @issues.page(params[:page]).per(PER_PAGE) @issues = @issues.preload(:author, :project) respond_to do |format| diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index a7863aba756..1b9a86ee42c 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -43,7 +43,7 @@ class ProfilesController < ApplicationController end def history - @events = current_user.recent_events.page(params[:page]).per(20) + @events = current_user.recent_events.page(params[:page]).per(PER_PAGE) end def update_username diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 690501f3060..f049e96e61d 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -8,7 +8,7 @@ class Projects::BranchesController < Projects::ApplicationController def index @sort = params[:sort] || 'name' @branches = @repository.branches_sorted_by(@sort) - @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(30) + @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE) end def recent diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 1f1a9b4d43a..4266bcaef16 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -20,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController terms = params['issue_search'] @issues = get_issues_collection @issues = @issues.full_search(terms) if terms.present? - @issues = @issues.page(params[:page]).per(20) + @issues = @issues.page(params[:page]).per(PER_PAGE) respond_to do |format| format.html diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 5e31fce4b0e..207a01ed3b0 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -7,7 +7,7 @@ class Projects::LabelsController < Projects::ApplicationController respond_to :js, :html def index - @labels = @project.labels.page(params[:page]).per(20) + @labels = @project.labels.page(params[:page]).per(PER_PAGE) end def new diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 57c017e799f..10c34584c8a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -18,7 +18,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def index @merge_requests = get_merge_requests_collection - @merge_requests = @merge_requests.page(params[:page]).per(20) + @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE) end def show diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index afdb560e73c..b49b549547a 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -18,7 +18,7 @@ class Projects::MilestonesController < Projects::ApplicationController end @milestones = @milestones.includes(:project) - @milestones = @milestones.page(params[:page]).per(20) + @milestones = @milestones.page(params[:page]).per(PER_PAGE) end def new diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 03fface2d2a..c4f27a6d989 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -7,7 +7,7 @@ class Projects::TagsController < Projects::ApplicationController def index sorted = VersionSorter.rsort(@repository.tag_names) - @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(30) + @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE) end def create diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 3392fbca91e..643167947b9 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -7,7 +7,7 @@ class Projects::WikisController < Projects::ApplicationController before_filter :load_project_wiki def pages - @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(30) + @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE) end def show diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 6ac048e4b83..ae501362dc2 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -16,7 +16,7 @@ class SnippetsController < ApplicationController layout :determine_layout def index - @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(20) + @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(PER_PAGE) end def user_index @@ -28,7 +28,7 @@ class SnippetsController < ApplicationController filter: :by_user, user: @user, scope: params[:scope] }). - page(params[:page]).per(20) + page(params[:page]).per(PER_PAGE) if @user == current_user render 'current_user_index' -- GitLab From 80b2f3fb86d6e6b16565b9e9de82dda169926bcb Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 12 Mar 2015 16:20:58 -0700 Subject: [PATCH 1309/1609] Implement merge requests search It is same search like we have at issues page. It allows to quickly filter merge requests based on title or desription. I copy-pasted some js code from Issues.js. In future search (filtering) logic should be refactoed into one class for merge requests and issues --- CHANGELOG | 1 + app/assets/javascripts/dispatcher.js.coffee | 1 + app/assets/javascripts/issues.js.coffee | 2 +- .../javascripts/merge_requests.js.coffee | 37 ++++++++++++++++--- .../projects/merge_requests_controller.rb | 11 ++++++ app/views/projects/issues/index.html.haml | 9 +---- .../merge_requests/_merge_requests.html.haml | 13 +++++++ .../projects/merge_requests/index.html.haml | 31 +++++----------- .../shared/_issuable_search_form.html.haml | 9 +++++ features/project/merge_requests.feature | 7 ++++ features/steps/project/merge_requests.rb | 4 ++ 11 files changed, 90 insertions(+), 35 deletions(-) create mode 100644 app/views/projects/merge_requests/_merge_requests.html.haml create mode 100644 app/views/shared/_issuable_search_form.html.haml diff --git a/CHANGELOG b/CHANGELOG index b0adaeb101b..d352656eba8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,6 +59,7 @@ v 7.9.0 (unreleased) - Added blue thmeme - Remove annoying notice messages when create/update merge request - Allow smb:// links in Markdown text. + - Filter merge request by title or description at Merge Requests page v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 928232e95bd..e1015a63d52 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -52,6 +52,7 @@ class Dispatcher new ZenMode() when 'projects:merge_requests:index' shortcut_handler = new ShortcutsNavigation() + MergeRequests.init() when 'dashboard:show' new Dashboard() new Activities() diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 6513f4bcefc..40bb9e9cb0c 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -47,7 +47,7 @@ initSearch: -> @timer = null $("#issue_search").keyup -> - clearTimeout(@timer); + clearTimeout(@timer) @timer = setTimeout(Issues.filterResults, 500) filterResults: => diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee index 9201c84c5ed..83434c1b9ba 100644 --- a/app/assets/javascripts/merge_requests.js.coffee +++ b/app/assets/javascripts/merge_requests.js.coffee @@ -1,8 +1,35 @@ # # * Filter merge requests # -@merge_requestsPage = -> - $('#assignee_id').select2() - $('#milestone_id').select2() - $('#milestone_id, #assignee_id').on 'change', -> - $(this).closest('form').submit() +@MergeRequests = + init: -> + MergeRequests.initSearch() + + # Make sure we trigger ajax request only after user stop typing + initSearch: -> + @timer = null + $("#issue_search").keyup -> + clearTimeout(@timer) + @timer = setTimeout(MergeRequests.filterResults, 500) + + filterResults: => + form = $("#issue_search_form") + search = $("#issue_search").val() + $('.merge-requests-holder').css("opacity", '0.5') + issues_url = form.attr('action') + '? '+ form.serialize() + + $.ajax + type: "GET" + url: form.attr('action') + data: form.serialize() + complete: -> + $('.merge-requests-holder').css("opacity", '1.0') + success: (data) -> + $('.merge-requests-holder').html(data.html) + # Change url so if user reload a page - search results are saved + History.replaceState {page: issues_url}, document.title, issues_url + MergeRequests.reload() + dataType: "json" + + reload: -> + $('#filter_issue_search').val($('#issue_search').val()) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 57c017e799f..a8bc544789d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -17,8 +17,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_filter :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort] def index + terms = params['issue_search'] @merge_requests = get_merge_requests_collection + @merge_requests = @merge_requests.full_search(terms) if terms.present? @merge_requests = @merge_requests.page(params[:page]).per(20) + + respond_to do |format| + format.html + format.json do + render json: { + html: view_to_html_string("projects/merge_requests/_merge_requests") + } + end + end end def show diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index cbbcb1d06c0..2cb94d10b6f 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,14 +6,7 @@ = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do %i.fa.fa-rss - = form_tag namespace_project_issues_path(@project.namespace, @project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do - .append-right-10.hidden-xs.hidden-sm - = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } - = hidden_field_tag :state, params['state'] - = hidden_field_tag :scope, params['scope'] - = hidden_field_tag :assignee_id, params['assignee_id'] - = hidden_field_tag :milestone_id, params['milestone_id'] - = hidden_field_tag :label_id, params['label_id'] + = render 'shared/issuable_search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :write_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml new file mode 100644 index 00000000000..b8a0ca9a42f --- /dev/null +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -0,0 +1,13 @@ +.panel.panel-default + %ul.well-list.mr-list + = render @merge_requests + - if @merge_requests.blank? + %li + .nothing-here-block No merge requests to show + +- if @merge_requests.present? + .pull-right + %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter + + = paginate @merge_requests, theme: "gitlab" + diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index e3b9a28033b..d7992bdd19e 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,22 +1,11 @@ -.merge-requests-holder - .append-bottom-10 - .pull-right - - if can? current_user, :write_merge_request, @project - = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new pull-left", title: "New Merge Request" do - %i.fa.fa-plus - New Merge Request - = render 'shared/issuable_filter' - .panel.panel-default - %ul.well-list.mr-list - = render @merge_requests - - if @merge_requests.blank? - %li - .nothing-here-block No merge requests to show - - if @merge_requests.present? - .pull-right - %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter - - = paginate @merge_requests, theme: "gitlab" +.append-bottom-10 + .pull-right + = render 'shared/issuable_search_form', path: namespace_project_merge_requests_path(@project.namespace, @project) -:javascript - $(merge_requestsPage); + - if can? current_user, :write_merge_request, @project + = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new pull-left", title: "New Merge Request" do + %i.fa.fa-plus + New Merge Request + = render 'shared/issuable_filter' +.merge-requests-holder + = render 'merge_requests' diff --git a/app/views/shared/_issuable_search_form.html.haml b/app/views/shared/_issuable_search_form.html.haml new file mode 100644 index 00000000000..639d203dcd6 --- /dev/null +++ b/app/views/shared/_issuable_search_form.html.haml @@ -0,0 +1,9 @@ += form_tag(path, method: :get, id: "issue_search_form", class: 'pull-left issue-search-form') do + .append-right-10.hidden-xs.hidden-sm + = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' } + = hidden_field_tag :state, params['state'] + = hidden_field_tag :scope, params['scope'] + = hidden_field_tag :assignee_id, params['assignee_id'] + = hidden_field_tag :author_id, params['author_id'] + = hidden_field_tag :milestone_id, params['milestone_id'] + = hidden_field_tag :label_id, params['label_id'] diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 7c029f05d75..adad100e56c 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -218,3 +218,10 @@ Feature: Project Merge Requests And I click link "Edit" for the merge request And I preview a description text like "Bug fixed :smile:" Then I should see the Markdown write tab + + @javascript + Scenario: I search merge request + Given I click link "All" + When I fill in merge request search with "Fe" + Then I should see "Feature NS-03" in merge requests + And I should not see "Bug NS-04" in merge requests diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 263f2ef2438..b67b2e58caf 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -276,6 +276,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end end + step 'I fill in merge request search with "Fe"' do + fill_in 'issue_search', with: "Fe" + end + def merge_request @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") end -- GitLab From 2718955441587618933a632008b85762247081a2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 13:47:26 +0100 Subject: [PATCH 1310/1609] Fix import pages not working after first load. --- CHANGELOG | 1 + lib/gitlab/bitbucket_import/client.rb | 2 +- lib/gitlab/github_import/client.rb | 2 +- lib/gitlab/gitlab_import/client.rb | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 97376c85ece..7c7cbb366eb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ v 7.9.0 (unreleased) - Allow smb:// links in Markdown text. - Filter merge request by title or description at Merge Requests page - Block user if he/she was blocked in Active Directory + - Fix import pages not working after first load. v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index c907bebaef6..1e4906c9e31 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -92,7 +92,7 @@ module Gitlab end def bitbucket_options - OmniAuth::Strategies::Bitbucket.default_options[:client_options] + OmniAuth::Strategies::Bitbucket.default_options[:client_options].dup end end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 676d226bddd..7fe076b333b 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -46,7 +46,7 @@ module Gitlab end def github_options - OmniAuth::Strategies::GitHub.default_options[:client_options] + OmniAuth::Strategies::GitHub.default_options[:client_options].dup end end end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index ecf4ff94e39..2236439c6ce 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -71,7 +71,7 @@ module Gitlab end def gitlab_options - OmniAuth::Strategies::GitLab.default_options[:client_options] + OmniAuth::Strategies::GitLab.default_options[:client_options].dup end end end -- GitLab From f96dc6295aeb6f7d731c635c1a8a8ff609b4510f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 12:25:31 +0100 Subject: [PATCH 1311/1609] Everything from gitlab_git is already UTF-8. --- app/controllers/projects/graphs_controller.rb | 4 ++-- app/models/repository.rb | 4 ++-- app/views/projects/blame/show.html.haml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 752474b4a4c..6e54af356e0 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -28,8 +28,8 @@ class Projects::GraphsController < Projects::ApplicationController @commits.each do |commit| @log << { - author_name: commit.author_name.force_encoding('UTF-8'), - author_email: commit.author_email.force_encoding('UTF-8'), + author_name: commit.author_name, + author_email: commit.author_email, date: commit.committed_date.strftime("%Y-%m-%d") } end diff --git a/app/models/repository.rb b/app/models/repository.rb index 6117db418a7..47758b8ad68 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -136,8 +136,8 @@ class Repository commit = Gitlab::Git::Commit.new(rugged_commit) { - author_name: commit.author_name.force_encoding('UTF-8'), - author_email: commit.author_email.force_encoding('UTF-8'), + author_name: commit.author_name, + author_email: commit.author_email, additions: commit.stats.additions, deletions: commit.stats.deletions, } diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 5a33d18e631..4cc1fedb1ce 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -30,5 +30,5 @@ %code :erb <% lines.each do |line| %> - <%= highlight(@blob.name, line.force_encoding("utf-8"), true).html_safe %> + <%= highlight(@blob.name, line, true).html_safe %> <% end %> -- GitLab From 7f4cffd88b393a55f6462ce870807c7afd7ca98a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 6 Mar 2015 12:25:42 +0100 Subject: [PATCH 1312/1609] Reuse blob object fetched by Gitlab::Git::Blame. --- app/controllers/projects/blame_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 489a6ae5666..a87b8270a22 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -7,7 +7,7 @@ class Projects::BlameController < Projects::ApplicationController before_filter :authorize_download_code! def show - @blob = @repository.blob_at(@commit.id, @path) - @blame = Gitlab::Git::Blame.new(project.repository, @commit.id, @path) + @blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path) + @blob = @blame.blob end end -- GitLab From 4e49f21b141e8cbbf581c119c7524f6e9553f136 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 14:51:48 +0100 Subject: [PATCH 1313/1609] Set push data object kind in PushDataBuilder. --- app/services/create_tag_service.rb | 4 +--- app/services/git_tag_push_service.rb | 4 +--- lib/gitlab/push_data_builder.rb | 3 ++- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index dfc5677c9d4..755202310ab 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -40,9 +40,7 @@ class CreateTagService < BaseService end def create_push_data(project, user, tag) - data = Gitlab::PushDataBuilder. + Gitlab::PushDataBuilder. build(project, user, Gitlab::Git::BLANK_SHA, tag.target, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) - data[:object_kind] = "tag_push" - data end end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index cd92f50b02a..666bc482f88 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -16,8 +16,6 @@ class GitTagPushService private def create_push_data(oldrev, newrev, ref) - data = Gitlab::PushDataBuilder.build(project, user, oldrev, newrev, ref, []) - data[:object_kind] = "tag_push" - data + Gitlab::PushDataBuilder.build(project, user, oldrev, newrev, ref, []) end end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 0cc6b0ac694..ea9012b8844 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -28,9 +28,10 @@ module Gitlab # Get latest 20 commits ASC commits_limited = commits.last(20) + type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" # Hash to be passed as post_receive_data data = { - object_kind: "push", + object_kind: type, before: oldrev, after: newrev, ref: ref, -- GitLab From 09791774a594972d410fa72f5e0a256f19f0915a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 15:42:31 +0100 Subject: [PATCH 1314/1609] Use custom LDAP label in LDAP signin form. --- CHANGELOG | 1 + app/views/devise/sessions/_new_ldap.html.haml | 6 +++--- app/views/devise/shared/_signin_box.html.haml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 97376c85ece..6342e68805a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ v 7.9.0 (unreleased) - Allow smb:// links in Markdown text. - Filter merge request by title or description at Merge Requests page - Block user if he/she was blocked in Active Directory + - Use custom LDAP label in LDAP signin form. v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index e986989a728..812e22373a7 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,4 +1,4 @@ -= form_tag(user_omniauth_callback_path(provider), id: 'new_ldap_user' ) do - = text_field_tag :username, nil, {class: "form-control top", placeholder: "LDAP Login", autofocus: "autofocus"} += form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do + = text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} - = button_tag "LDAP Sign in", class: "btn-save btn" + = button_tag "#{server['label']} Sign in", class: "btn-save btn" diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 8faa6398a60..c76574db457 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -17,7 +17,7 @@ .tab-content - @ldap_servers.each_with_index do |server, i| %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero?)} - = render 'devise/sessions/new_ldap', provider: server['provider_name'] + = render 'devise/sessions/new_ldap', server: server - if signin_enabled? %div#tab-signin.tab-pane = render 'devise/sessions/new_base' -- GitLab From affb5b3ff48f9037349eac77f86adb7c946663ce Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 13 Mar 2015 07:48:35 -0700 Subject: [PATCH 1315/1609] Update documentation for object_kind field in Webhook push and tag push Webhooks --- CHANGELOG | 1 + doc/web_hooks/web_hooks.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 97376c85ece..a2e139eca33 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) + - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) - Fix broken email images (Hannes Rosenögger) - Fix mass SQL statements on initial push (Hannes Rosenögger) - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 3cccd84b063..851f50f5e9a 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -16,6 +16,7 @@ Triggered when you push to the repository except when pushing tags. ```json { + "object_kind": "push", "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", "ref": "refs/heads/master", @@ -66,6 +67,7 @@ Triggered when you create (or delete) tags to the repository. ```json { + "object_kind": "tag_push", "ref": "refs/tags/v1.0.0", "before": "0000000000000000000000000000000000000000", "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", -- GitLab From 84d28209b6f8a63f35ad082bc8851e28550643e1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 14:55:17 +0100 Subject: [PATCH 1316/1609] Use PushDataBuilder where applicable. --- app/services/create_branch_service.rb | 14 +++++++++++--- app/services/create_tag_service.rb | 4 ++-- app/services/delete_branch_service.rb | 11 +++++++++-- app/services/event_create_service.rb | 20 -------------------- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 5e971c7891c..f835f06e72b 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -17,10 +17,13 @@ class CreateBranchService < BaseService new_branch = repository.find_branch(branch_name) if new_branch - EventCreateService.new.push_ref(project, current_user, new_branch, 'add') - return success(new_branch) + push_data = build_push_data(project, current_user, new_branch) + + EventCreateService.new.push(project, current_user, push_data) + + success(new_branch) else - return error('Invalid reference name') + error('Invalid reference name') end end @@ -29,4 +32,9 @@ class CreateBranchService < BaseService out[:branch] = branch out end + + def build_push_data(project, user, branch) + Gitlab::PushDataBuilder. + build(project, user, Gitlab::Git::BLANK_SHA, branch.target, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) + end end diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 755202310ab..af4b537cb93 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -21,9 +21,9 @@ class CreateTagService < BaseService new_tag = repository.find_tag(tag_name) if new_tag - EventCreateService.new.push_ref(project, current_user, new_tag, 'add', Gitlab::Git::TAG_REF_PREFIX) - push_data = create_push_data(project, current_user, new_tag) + + EventCreateService.new.push(project, current_user, push_data) project.execute_hooks(push_data.dup, :tag_push_hooks) project.execute_services(push_data.dup, :tag_push_hooks) diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index c26aee2b0aa..f2d5ed818c1 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -25,10 +25,12 @@ class DeleteBranchService < BaseService end if repository.rm_branch(branch_name) - EventCreateService.new.push_ref(project, current_user, branch, 'rm') + push_data = build_push_data(branch) + + EventCreateService.new.push(project, current_user, push_data) success('Branch was removed') else - return error('Failed to remove branch') + error('Failed to remove branch') end end @@ -43,4 +45,9 @@ class DeleteBranchService < BaseService out[:message] = message out end + + def build_push_data(branch) + Gitlab::PushDataBuilder + .build(project, current_user, branch.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) + end end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index dc52d6d89df..103d6b0a08b 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -62,26 +62,6 @@ class EventCreateService create_event(project, current_user, Event::CREATED) end - def push_ref(project, current_user, ref, action = 'add', prefix = Gitlab::Git::BRANCH_REF_PREFIX) - commit = project.repository.commit(ref.target) - - if action.to_s == 'add' - before = Gitlab::Git::BLANK_SHA - after = commit.id - else - before = commit.id - after = Gitlab::Git::BLANK_SHA - end - - data = { - ref: "#{prefix}#{ref.name}", - before: before, - after: after - } - - push(project, current_user, data) - end - def push(project, current_user, push_data) create_event(project, current_user, Event::PUSHED, data: push_data) end -- GitLab From f2024b1e06587c2b274d4982a48d80d052bba088 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 14:55:38 +0100 Subject: [PATCH 1317/1609] More consistent method naming. --- app/services/git_push_service.rb | 5 +++-- app/services/git_tag_push_service.rb | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index bfabfd7ade3..4885e1b2fc5 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -53,7 +53,8 @@ class GitPushService process_commit_messages(ref) end - @push_data = post_receive_data(oldrev, newrev, ref) + @push_data = build_push_data(oldrev, newrev, ref) + EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup, :push_hooks) @@ -101,7 +102,7 @@ class GitPushService end end - def post_receive_data(oldrev, newrev, ref) + def build_push_data(oldrev, newrev, ref) Gitlab::PushDataBuilder. build(project, user, oldrev, newrev, ref, push_commits) end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 666bc482f88..0d8e6e85e47 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -3,19 +3,21 @@ class GitTagPushService def execute(project, user, oldrev, newrev, ref) @project, @user = project, user - @push_data = create_push_data(oldrev, newrev, ref) + + @push_data = build_push_data(oldrev, newrev, ref) EventCreateService.new.push(project, user, @push_data) - project.repository.expire_cache project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) + project.repository.expire_cache + true end private - def create_push_data(oldrev, newrev, ref) + def build_push_data(oldrev, newrev, ref) Gitlab::PushDataBuilder.build(project, user, oldrev, newrev, ref, []) end end -- GitLab From 10421674afdc8a18cdab52288e736d06e3015096 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 14:56:08 +0100 Subject: [PATCH 1318/1609] Ecevute hooks and services when branches are created/deleted through web. --- app/services/create_branch_service.rb | 2 ++ app/services/delete_branch_service.rb | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index f835f06e72b..cf7ae4345f3 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -20,6 +20,8 @@ class CreateBranchService < BaseService push_data = build_push_data(project, current_user, new_branch) EventCreateService.new.push(project, current_user, push_data) + project.execute_hooks(push_data.dup, :push_hooks) + project.execute_services(push_data.dup, :push_hooks) success(new_branch) else diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index f2d5ed818c1..b19b112a0c4 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -28,6 +28,9 @@ class DeleteBranchService < BaseService push_data = build_push_data(branch) EventCreateService.new.push(project, current_user, push_data) + project.execute_hooks(push_data.dup, :push_hooks) + project.execute_services(push_data.dup, :push_hooks) + success('Branch was removed') else error('Failed to remove branch') -- GitLab From 12b779e70b54692f4f00cb386440833bd1426a93 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 14:56:53 +0100 Subject: [PATCH 1319/1609] Move tag deletion to service and execute hooks and services. --- app/controllers/projects/tags_controller.rb | 11 +++--- app/services/delete_tag_service.rb | 42 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 app/services/delete_tag_service.rb diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index c4f27a6d989..83f4937bce3 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -24,14 +24,13 @@ class Projects::TagsController < Projects::ApplicationController end def destroy - tag = @repository.find_tag(params[:id]) - - if tag && @repository.rm_tag(tag.name) - EventCreateService.new.push_ref(@project, current_user, tag, 'rm', Gitlab::Git::TAG_REF_PREFIX) - end + DeleteTagService.new(project, current_user).execute(params[:id]) respond_to do |format| - format.html { redirect_to namespace_project_tags_path } + format.html do + redirect_to namespace_project_tags_path(@project.namespace, + @project) + end format.js end end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb new file mode 100644 index 00000000000..0c836401136 --- /dev/null +++ b/app/services/delete_tag_service.rb @@ -0,0 +1,42 @@ +require_relative 'base_service' + +class DeleteTagService < BaseService + def execute(tag_name) + repository = project.repository + tag = repository.find_tag(tag_name) + + # No such tag + unless tag + return error('No such tag', 404) + end + + if repository.rm_tag(tag_name) + push_data = build_push_data(tag) + + EventCreateService.new.push(project, current_user, push_data) + project.execute_hooks(push_data.dup, :tag_push_hooks) + project.execute_services(push_data.dup, :tag_push_hooks) + + success('Tag was removed') + else + error('Failed to remove tag') + end + end + + def error(message, return_code = 400) + out = super(message) + out[:return_code] = return_code + out + end + + def success(message) + out = super() + out[:message] = message + out + end + + def build_push_data(tag) + Gitlab::PushDataBuilder + .build(project, current_user, tag.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) + end +end -- GitLab From b160db1482255146e58317be4b16b1e9aebc3748 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 14:57:27 +0100 Subject: [PATCH 1320/1609] Add changelog item. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 97376c85ece..0905cc01be6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ v 7.9.0 (unreleased) - Allow smb:// links in Markdown text. - Filter merge request by title or description at Merge Requests page - Block user if he/she was blocked in Active Directory + - Execute hooks and services when branch or tag is created or deleted through web interface. v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers -- GitLab From 9bb5986d4afff761ddc344413fdb34babb4c9d70 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 12 Mar 2015 13:35:51 -0700 Subject: [PATCH 1321/1609] Remove unnecessary fetch of commit messages for initial push. This will reduce the memory usage significantly. --- CHANGELOG | 1 + app/services/git_push_service.rb | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b0adaeb101b..896e6a6531b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) + - Remove unnecessary fetch of commit messages on initial push (Perforce Software) - Fix broken email images (Hannes Rosenögger) - Fix mass SQL statements on initial push (Hannes Rosenögger) - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index bfabfd7ade3..232028b1e0b 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -29,9 +29,6 @@ class GitPushService elsif push_to_new_branch?(ref, oldrev) # Re-find the pushed commits. if is_default_branch?(ref) - # Initial push to the default branch. Take the full history of that branch as "newly pushed". - @push_commits = project.repository.commits(newrev) - # Set protection on the default branch if configured if (current_application_settings.default_branch_protection != PROTECTION_NONE) developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false -- GitLab From b09e8c771c19953cbebbce5b546d0d92ba9d623e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 Mar 2015 09:47:50 -0700 Subject: [PATCH 1322/1609] Bump gitlab_git to 7.1.1 Verify found object is actually a commit in Commit.find --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 064ed0958a7..44f024a4b8e 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '7.1.0' +gem "gitlab_git", '~> 7.1.0' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' diff --git a/Gemfile.lock b/Gemfile.lock index c847424a7c4..b331c29f7ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -213,7 +213,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.1.0) + gitlab_git (7.1.1) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -709,7 +709,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (= 7.1.0) + gitlab_git (~> 7.1.0) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.0) gollum-lib (~> 4.0.0) -- GitLab From a1b3e239ee0fdf43d511ea96ed30770336bb9c3f Mon Sep 17 00:00:00 2001 From: Andrea Ruggiero Date: Fri, 13 Mar 2015 21:27:05 +0100 Subject: [PATCH 1323/1609] Fix typo in CHANGELOG.md --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5ccedcbc8c5..21dc1bb2678 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -57,7 +57,7 @@ v 7.9.0 (unreleased) - Use Emoji One - Updated emoji help documentation to properly reference EmojiOne. - Fix missing GitHub organisation repositories on import page. - - Added blue thmeme + - Added blue theme - Remove annoying notice messages when create/update merge request - Allow smb:// links in Markdown text. - Filter merge request by title or description at Merge Requests page -- GitLab From 2172d7ff9e6369d963199348291046f6e06a4215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Fri, 13 Mar 2015 22:17:51 +0100 Subject: [PATCH 1324/1609] Revert "Merge branch 'follow-on-mr376' into 'master'" This reverts commit 07f9a3f928d39accf876d052b265844f74130099, reversing changes made to 4803675190833cdf7e83558a32b2f2ea3283dce0. Reverted this because the data is used in hooks as well. --- CHANGELOG | 1 - app/services/git_push_service.rb | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index faf74ff2555..5ccedcbc8c5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,6 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) - - Remove unnecessary fetch of commit messages on initial push (Perforce Software) - Fix broken email images (Hannes Rosenögger) - Fix mass SQL statements on initial push (Hannes Rosenögger) - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index e4c30dd6dfc..4885e1b2fc5 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -29,6 +29,9 @@ class GitPushService elsif push_to_new_branch?(ref, oldrev) # Re-find the pushed commits. if is_default_branch?(ref) + # Initial push to the default branch. Take the full history of that branch as "newly pushed". + @push_commits = project.repository.commits(newrev) + # Set protection on the default branch if configured if (current_application_settings.default_branch_protection != PROTECTION_NONE) developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false -- GitLab From 8fed435208fed3115c740eb630c263a97b5a631d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 16:40:15 +0100 Subject: [PATCH 1325/1609] Unblock user if they were unblocked in AD. --- CHANGELOG | 2 +- lib/gitlab/ldap/access.rb | 1 + spec/lib/gitlab/ldap/access_spec.rb | 11 ++++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 97376c85ece..3511a94ba06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -60,7 +60,7 @@ v 7.9.0 (unreleased) - Remove annoying notice messages when create/update merge request - Allow smb:// links in Markdown text. - Filter merge request by title or description at Merge Requests page - - Block user if he/she was blocked in Active Directory + - Block and unblock user if he/she was blocked/unblocked in Active Directory v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index 6e30724e1f7..960fb3849b4 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -40,6 +40,7 @@ module Gitlab user.block unless user.blocked? false else + user.activate if user.blocked? true end else diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 39d46efcbc3..707a0521ab3 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -28,9 +28,18 @@ describe Gitlab::LDAP::Access do end context 'and has no disabled flag in active diretory' do - before { Gitlab::LDAP::Person.stub(disabled_via_active_directory?: false) } + before do + user.block + + Gitlab::LDAP::Person.stub(disabled_via_active_directory?: false) + end it { is_expected.to be_truthy } + + it "should unblock user in GitLab" do + access.allowed? + user.should_not be_blocked + end end context 'without ActiveDirectory enabled' do -- GitLab From 141168ad3cf0ad2f79f0a5c64c29e7f95c2064b5 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 13 Mar 2015 17:14:34 -0700 Subject: [PATCH 1326/1609] Change default number of unicorn workers to three. --- config/unicorn.rb.example | 2 +- doc/install/requirements.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index 29253b71f49..3aee718097f 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -16,7 +16,7 @@ # Read about unicorn workers here: # http://doc.gitlab.com/ee/install/requirements.html#unicorn-workers # -worker_processes 2 +worker_processes 3 # Since Unicorn is never exposed to outside clients, it does not need to # run on the standard HTTP port (80), there is no reason to start Unicorn diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 5bdb9caa2bf..65ddb3e3cfb 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -80,7 +80,7 @@ It's possible to increase the amount of unicorn workers and tis will usually hel For most instances we recommend using: CPU cores + 1 = unicorn workers. So for a machine with 2 cores, 3 unicorn workers is ideal. -For all machines that have 1GB and up we recommend a minimum of two unicorn workers. +For all machines that have 1GB and up we recommend a minimum of three unicorn workers. If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping. With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check). If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping. -- GitLab From 8c2655b5bf935830313db99fc83c34771ecbd609 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 13 Mar 2015 17:44:54 -0700 Subject: [PATCH 1327/1609] Remove rubyracer gem. --- Gemfile | 1 - Gemfile.lock | 6 ------ 2 files changed, 7 deletions(-) diff --git a/Gemfile b/Gemfile index 44f024a4b8e..439d0c313e6 100644 --- a/Gemfile +++ b/Gemfile @@ -268,7 +268,6 @@ end group :production do gem "gitlab_meta", '7.0' - gem "therubyracer" end gem "newrelic_rpm" diff --git a/Gemfile.lock b/Gemfile.lock index b331c29f7ef..397735d994a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -320,7 +320,6 @@ GEM addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - libv8 (3.16.14.7) listen (2.3.1) celluloid (>= 0.15.2) rb-fsevent (>= 0.9.3) @@ -479,7 +478,6 @@ GEM redis-store (~> 1.1.0) redis-store (1.1.4) redis (>= 2.2) - ref (1.0.5) request_store (1.0.5) rest-client (1.6.7) mime-types (>= 1.16) @@ -600,9 +598,6 @@ GEM tins (~> 0.8) terminal-table (1.4.5) test_after_commit (0.2.2) - therubyracer (0.12.0) - libv8 (~> 3.16.14.0) - ref thin (1.6.1) daemons (>= 1.0.9) eventmachine (>= 1.0.0) @@ -788,7 +783,6 @@ DEPENDENCIES stamp state_machine test_after_commit - therubyracer thin tinder (~> 1.9.2) turbolinks -- GitLab From 3aded9d5816b8605d5ccf486ab2e38247e2b654f Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 13 Mar 2015 17:47:30 -0700 Subject: [PATCH 1328/1609] Update changelog with change to unicorn workers number recommendation. --- CHANGELOG | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3a2cd3ed650..ba4f6e6de97 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -36,16 +36,16 @@ v 7.9.0 (unreleased) - Send notifications and leave system comments when bulk updating issues. - Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison) - Move groups page from profile to dashboard - - Starred projects page at dashboard + - Starred projects page at dashboard - Blocking user does not remove him/her from project/groups but show blocked label - Change subject of EmailsOnPush emails to include namespace, project and branch. - Change subject of EmailsOnPush emails to include first commit message when multiple were pushed. - Remove confusing footer from EmailsOnPush mail body. - - Add list of changed files to EmailsOnPush emails. - - Add option to send EmailsOnPush emails from committer email if domain matches. + - Add list of changed files to EmailsOnPush emails. + - Add option to send EmailsOnPush emails from committer email if domain matches. - Add option to disable code diffs in EmailOnPush emails. - Wrap commit message in EmailsOnPush email. - - Send EmailsOnPush emails when deleting commits using force push. + - Send EmailsOnPush emails when deleting commits using force push. - Fix EmailsOnPush email comparison link to include first commit. - Fix highliht of selected lines in file - Reject access to group/project avatar if the user doesn't have access. @@ -66,6 +66,7 @@ v 7.9.0 (unreleased) - Use custom LDAP label in LDAP signin form. - Execute hooks and services when branch or tag is created or deleted through web interface. - Block and unblock user if he/she was blocked/unblocked in Active Directory + - Raise recommended number of unicorn workers from 2 to 3 v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers -- GitLab From e8dbd692f6439c5e31b0b8879d4620d89eb2eb74 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 13 Mar 2015 18:22:26 -0700 Subject: [PATCH 1329/1609] Remove rubyracer --- Gemfile | 1 - Gemfile.lock | 6 ------ 2 files changed, 7 deletions(-) diff --git a/Gemfile b/Gemfile index 44f024a4b8e..439d0c313e6 100644 --- a/Gemfile +++ b/Gemfile @@ -268,7 +268,6 @@ end group :production do gem "gitlab_meta", '7.0' - gem "therubyracer" end gem "newrelic_rpm" diff --git a/Gemfile.lock b/Gemfile.lock index b331c29f7ef..397735d994a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -320,7 +320,6 @@ GEM addressable (~> 2.3) letter_opener (1.1.2) launchy (~> 2.2) - libv8 (3.16.14.7) listen (2.3.1) celluloid (>= 0.15.2) rb-fsevent (>= 0.9.3) @@ -479,7 +478,6 @@ GEM redis-store (~> 1.1.0) redis-store (1.1.4) redis (>= 2.2) - ref (1.0.5) request_store (1.0.5) rest-client (1.6.7) mime-types (>= 1.16) @@ -600,9 +598,6 @@ GEM tins (~> 0.8) terminal-table (1.4.5) test_after_commit (0.2.2) - therubyracer (0.12.0) - libv8 (~> 3.16.14.0) - ref thin (1.6.1) daemons (>= 1.0.9) eventmachine (>= 1.0.0) @@ -788,7 +783,6 @@ DEPENDENCIES stamp state_machine test_after_commit - therubyracer thin tinder (~> 1.9.2) turbolinks -- GitLab From 89d3028b20d769185b20d9af8e2e2f58d29667b8 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 13 Mar 2015 19:00:16 -0700 Subject: [PATCH 1330/1609] Update monthly doc to mention wip blogpost for next release. --- doc/release/monthly.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/release/monthly.md b/doc/release/monthly.md index ec96be27f3f..cfe01896d8f 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -178,6 +178,10 @@ Update [installation.md](/doc/install/installation.md) to the newest version in Follow the [release doc in the Omnibus repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md). This can happen before tagging because Omnibus uses tags in its own repo and SHA1's to refer to the GitLab codebase. +## Update GitLab.com with the stable version + +- Deploy the package (should not need downtime because of the small difference with RC1) +- Deploy the package for ci.gitlab.com ## Release CE, EE and CI @@ -199,10 +203,10 @@ Proposed tweet "Release of GitLab X.X & CI Y.Y! FEATURE, FEATURE and FEATURE < Consider creating a post on Hacker News. -## Update GitLab.com with the stable version - -- Deploy the package (should not need downtime because of the small difference with RC1) - ## Release new AMIs [Follow this guide](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md) + +## Create a WIP blogpost for the next release + +Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). -- GitLab From 2da2720584e162c53436a046380740bd64c3ad24 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 Mar 2015 19:20:25 -0700 Subject: [PATCH 1331/1609] Improve css for file actions --- app/assets/stylesheets/base/gl_bootstrap.scss | 5 +--- app/assets/stylesheets/generic/files.scss | 24 +++++++------------ app/views/help/ui.html.haml | 19 +++++++++++++++ app/views/projects/blame/show.html.haml | 7 +++--- app/views/projects/blob/_blob.html.haml | 10 ++++---- app/views/projects/snippets/show.html.haml | 4 ++-- .../search/results/_snippet_blob.html.haml | 6 ----- app/views/snippets/show.html.haml | 4 ++-- 8 files changed, 43 insertions(+), 36 deletions(-) diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index 16581e9ebf2..7012d31e318 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -152,12 +152,9 @@ */ .panel { .panel-heading { - font-size: 14px; - line-height: 18px; - .panel-head-actions { position: relative; - top: -6px; + top: -5px; float: right; } } diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index 1ed41272ac5..dca6b957d24 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -18,27 +18,21 @@ text-align: left; padding: 10px 15px; - .options { + .file-actions { float: right; - margin-top: -3px; + position: relative; + top: -5px; + + .btn { + padding: 0px 10px; + font-size: 13px; + line-height: 28px; + } } .left-options { margin-top: -3px; } - - .file_name { - font-weight: bold; - padding-left: 3px; - font-size: 14px; - - small { - color: #888; - font-size: 13px; - font-weight: normal; - padding-left: 10px; - } - } } .file-content { background: #fff; diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 58de5b7c869..ed03f885dcd 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -21,6 +21,8 @@ = link_to 'Alerts', '#alerts' %li = link_to 'Forms', '#forms' + %li + = link_to 'Files', '#file' %li = link_to 'Markdown', '#markdown' @@ -193,6 +195,23 @@ Remember me %button.btn.btn-default{:type => "submit"} Sign in + %h2#file File + %h3 + %code .file-holder + + - blob = Snippet.new(content: "Wow\nSuch\nFile") + .example + .file-holder + .file-title + Awesome file + .file-actions + .btn-group + %a.btn Edit + %a.btn Remove + .file-contenta.code + = render 'shared/file_highlight', blob: blob + + %h2#markdown Markdown %h3 %code .md or .wiki and others diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 4cc1fedb1ce..e6a859fea8f 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -4,10 +4,11 @@ .file-holder .file-title %i.fa.fa-file - %span.file_name + %strong = @path - %small= number_to_human_size @blob.size - %span.options= render "projects/blob/actions" + %small= number_to_human_size @blob.size + .file-actions + = render "projects/blob/actions" .file-content.blame.highlight %table - @blame.each do |commit, lines, since| diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 9ff61f3887f..ba60bd92869 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -21,12 +21,14 @@ %div#tree-content-holder.tree-content-holder %article.file-holder - .file-title.clearfix + .file-title %i.fa.fa-file - %span.file_name + %strong = blob.name - %small= number_to_human_size blob.size - %span.options.hidden-xs= render "actions" + %small + = number_to_human_size(blob.size) + .file-actions.hidden-xs + = render "actions" - if blob.text? = render "text", blob: blob - elsif blob.image? diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 345848fa6d1..408e3c0224a 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -23,9 +23,9 @@ .file-holder .file-title %i.fa.fa-file - %span.file_name + %strong = @snippet.file_name - .options + .file-actions .btn-group - if can?(current_user, :modify_project_snippet, @snippet) = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-small", title: 'Edit Snippet' diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 6fc2cdf6362..8af393777f0 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -13,12 +13,6 @@ .file-title %i.fa.fa-file %strong= snippet_blob[:snippet_object].file_name - %span.options - .btn-group.tree-btn-group.pull-right - - if snippet_blob[:snippet_object].author == current_user - = link_to "Edit", edit_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", title: 'Edit Snippet' - = link_to "Delete", snippet_path(snippet_blob[:snippet_object]), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-tiny", title: 'Delete Snippet' - = link_to "Raw", raw_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", target: "_blank" - if gitlab_markdown?(snippet_blob[:snippet_object].file_name) .file-content.wiki - snippet_blob[:snippet_chunks].each do |snippet| diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index f5bc543de10..d9436caaadb 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -31,9 +31,9 @@ .file-holder .file-title %i.fa.fa-file - %span.file_name + %strong = @snippet.file_name - .options + .file-actions .btn-group - if can?(current_user, :modify_personal_snippet, @snippet) = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-small", title: 'Edit Snippet' -- GitLab From dffa2fa9e92e37a664afa2807fd5b01fbbd87ef2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 13 Mar 2015 23:40:46 -0700 Subject: [PATCH 1332/1609] Use sass variabled for backgroung and border colors --- app/assets/stylesheets/base/gl_variables.scss | 4 ++-- app/assets/stylesheets/base/variables.scss | 3 ++- app/assets/stylesheets/generic/files.scss | 14 ++++++----- app/assets/stylesheets/generic/highlight.scss | 2 +- app/assets/stylesheets/generic/selects.scss | 2 +- app/assets/stylesheets/generic/tables.scss | 2 +- app/assets/stylesheets/pages/diff.scss | 23 +++++++++++-------- app/assets/stylesheets/pages/graph.scss | 4 ++-- .../stylesheets/pages/merge_requests.scss | 4 ++-- 9 files changed, 33 insertions(+), 25 deletions(-) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index ea230646a89..ce82ad80318 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -716,8 +716,8 @@ $panel-border-radius: 0; // $panel-footer-bg: #f5f5f5 $panel-default-text: $text-color; -// $panel-default-border: #ddd -// $panel-default-heading-bg: #f5f5f5 +$panel-default-border: $border-color; +$panel-default-heading-bg: $background-color; // $panel-primary-text: #fff // $panel-primary-border: $brand-primary diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 54af78ee082..4e2c64aa132 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -1,6 +1,5 @@ $style_color: #474D57; $hover: #FFF3EB; -$box_bg: #F9F9F9; $gl-link-color: #446e9b; $nprogress-color: #c0392b; $gl-font-size: 14px; @@ -9,6 +8,8 @@ $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; +$border-color: #dce4ec; +$background-color: #ECF0F1; /* * State colors: diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index dca6b957d24..91220a856ac 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -3,7 +3,7 @@ * */ .file-holder { - border: 1px solid #CCC; + border: 1px solid $border-color; margin-bottom: 1em; table { @@ -11,8 +11,9 @@ } .file-title { - background: #EEE; - border-bottom: 1px solid #CCC; + position: relative; + background: $background-color; + border-bottom: 1px solid $border-color; text-shadow: 0 1px 1px #fff; margin: 0; text-align: left; @@ -20,8 +21,9 @@ .file-actions { float: right; - position: relative; - top: -5px; + position: absolute; + top: 5px; + right: 15px; .btn { padding: 0px 10px; @@ -113,7 +115,7 @@ ol { margin-left: 40px; padding: 10px 0; - border-left: 1px solid #CCC; + border-left: 1px solid $border-color; margin-bottom: 0; background: white; li { diff --git a/app/assets/stylesheets/generic/highlight.scss b/app/assets/stylesheets/generic/highlight.scss index 0f8225d6823..2e13ee842e0 100644 --- a/app/assets/stylesheets/generic/highlight.scss +++ b/app/assets/stylesheets/generic/highlight.scss @@ -57,7 +57,7 @@ .note-text .code { border: none; box-shadow: none; - background: $box_bg; + background: $background-color; padding: 1em; overflow-x: auto; diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index af0ecb192d6..c13a685a528 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -2,7 +2,7 @@ .select2-container, .select2-container.select2-drop-above { .select2-choice { background: #FFF; - border-color: #BBB; + border-color: #CCC; padding: 6px 14px; line-height: 1.42857143; height: auto; diff --git a/app/assets/stylesheets/generic/tables.scss b/app/assets/stylesheets/generic/tables.scss index 71a7d4abaee..a66e45577de 100644 --- a/app/assets/stylesheets/generic/tables.scss +++ b/app/assets/stylesheets/generic/tables.scss @@ -9,7 +9,7 @@ table { th { font-weight: normal; font-size: 15px; - border-bottom: 1px solid #CCC !important; + border-bottom: 1px solid $border-color !important; } td { border-color: #F1F1F1 !important; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 54311a68852..5a9f93dc03d 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1,25 +1,30 @@ .diff-file { - border: 1px solid #CCC; + border: 1px solid $border-color; margin-bottom: 1em; .diff-header { - @extend .clearfix; - background: #EEE; - border-bottom: 1px solid #CCC; - padding: 5px 5px 5px 10px; + position: relative; + background: $background-color; + border-bottom: 1px solid $border-color; + padding: 10px 15px; color: #555; z-index: 10; > span { + @include str-truncated(65%); font-family: $monospace_font; - line-height: 2; } .diff-btn-group { float: right; + position: absolute; + top: 5px; + right: 15px; .btn { - background-color: #FFF; + padding: 0px 10px; + font-size: 13px; + line-height: 28px; } } @@ -87,7 +92,7 @@ background: #F5F5F5; color: rgba(0,0,0,0.3); padding: 0px 5px; - border-right: 1px solid #ccc; + border-right: 1px solid $border-color; text-align: right; min-width: 35px; max-width: 50px; @@ -136,7 +141,7 @@ background: #ffecec; } &.matched { - color: #ccc; + color: $border-color; background: #fafafa; } &.parallel { diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 3d878d1e528..c3b10d144e1 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -1,11 +1,11 @@ .project-network { - border: 1px solid #CCC; + border: 1px solid $border-color; .controls { color: #888; font-size: 14px; padding: 5px; - border-bottom: 1px solid #bbb; + border-bottom: 1px solid $border-color; background: #EEE; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 9bd34b7376f..6babb824f3c 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -123,10 +123,10 @@ } .mr-state-widget { - background: $box_bg; + background: $background-color; margin-bottom: 20px; color: #666; - border: 1px solid #EEE; + border: 1px solid $border-color; @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); .ci_widget { -- GitLab From abf90611cbfd54590444a2f0221d2cbd7d8a6747 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 14 Mar 2015 00:13:15 -0700 Subject: [PATCH 1333/1609] Improve import buttons on new project page --- app/assets/stylesheets/pages/projects.scss | 5 +++ app/views/projects/new.html.haml | 43 +++++++++------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index bfd05973d75..e359aa45025 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -301,3 +301,8 @@ table.table.protected-branches-list tr.no-border { border: 0; } } + +.project-import .btn { + float: left; + margin-right: 10px; +} diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 00b912742b2..264012506a4 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -40,52 +40,45 @@ The import will time out after 4 minutes. For big repositories, use a clone/push combination. For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"} + .project-import.form-group - .col-sm-2 + %label.control-label Import projects from .col-sm-10 - if github_import_enabled? - = link_to status_import_github_path do + = link_to status_import_github_path, class: 'btn' do %i.fa.fa-github - Import projects from GitHub + GitHub - else - = link_to '#', class: 'how_to_import_link light' do + = link_to '#', class: 'how_to_import_link light btn' do %i.fa.fa-github - Import projects from GitHub + GitHub = render 'github_import_modal' - .project-import.form-group - .col-sm-2 - .col-sm-10 + - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path do + = link_to status_import_bitbucket_path, class: 'btn' do %i.fa.fa-bitbucket - Import projects from Bitbucket + Bitbucket - else - = link_to '#', class: 'how_to_import_link light' do + = link_to '#', class: 'how_to_import_link light btn' do %i.fa.fa-bitbucket - Import projects from Bitbucket + Bitbucket = render 'bitbucket_import_modal' - - unless request.host == 'gitlab.com' - .project-import.form-group - .col-sm-2 - .col-sm-10 + - unless request.host == 'gitlab.com' - if gitlab_import_enabled? - = link_to status_import_gitlab_path do + = link_to status_import_gitlab_path, class: 'btn' do %i.fa.fa-heart - Import projects from GitLab.com + GitLab.com - else - = link_to '#', class: 'how_to_import_link light' do + = link_to '#', class: 'how_to_import_link light btn' do %i.fa.fa-heart - Import projects from GitLab.com + GitLab.com = render 'gitlab_import_modal' - .project-import.form-group - .col-sm-2 - .col-sm-10 - = link_to new_import_gitorious_path do + = link_to new_import_gitorious_path, class: 'btn' do %i.icon-gitorious.icon-gitorious-small - Import projects from Gitorious.org + Gitorious.org %hr.prepend-botton-10 -- GitLab From 30ca451fd4f926998868b9db524e8fa98cd9457d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 14 Mar 2015 00:29:32 -0700 Subject: [PATCH 1334/1609] Refactor buttons --- app/assets/stylesheets/generic/buttons.scss | 15 --------------- app/assets/stylesheets/pages/commit.scss | 3 ++- app/helpers/diff_helper.rb | 4 ++-- .../admin/applications/_delete_form.html.haml | 2 +- .../admin/broadcast_messages/index.html.haml | 2 +- app/views/admin/groups/index.html.haml | 4 ++-- app/views/admin/groups/show.html.haml | 2 +- app/views/admin/hooks/index.html.haml | 4 ++-- app/views/admin/projects/index.html.haml | 4 ++-- app/views/admin/projects/show.html.haml | 6 +++--- app/views/admin/users/index.html.haml | 8 ++++---- app/views/admin/users/show.html.haml | 6 +++--- app/views/dashboard/groups/index.html.haml | 4 ++-- .../applications/_delete_form.html.haml | 2 +- .../_delete_form.html.haml | 2 +- app/views/events/_event_last_push.html.haml | 2 +- app/views/groups/edit.html.haml | 2 +- .../groups/group_members/_group_member.html.haml | 8 ++++---- app/views/groups/milestones/index.html.haml | 4 ++-- app/views/groups/milestones/show.html.haml | 4 ++-- app/views/groups/projects.html.haml | 6 +++--- app/views/profiles/applications.html.haml | 2 +- app/views/profiles/emails/index.html.haml | 2 +- app/views/profiles/keys/_key.html.haml | 2 +- app/views/profiles/show.html.haml | 4 ++-- app/views/projects/blob/_actions.html.haml | 12 ++++++------ app/views/projects/branches/_branch.html.haml | 6 +++--- .../projects/deploy_keys/_deploy_key.html.haml | 6 +++--- app/views/projects/diffs/_file.html.haml | 2 +- app/views/projects/diffs/_stats.html.haml | 2 +- app/views/projects/diffs/_warning.html.haml | 10 +++++----- app/views/projects/edit.html.haml | 4 ++-- app/views/projects/hooks/index.html.haml | 4 ++-- app/views/projects/issues/_issue.html.haml | 6 +++--- .../show/_remove_source_branch.html.haml | 2 +- .../projects/milestones/_milestone.html.haml | 4 ++-- app/views/projects/new.html.haml | 2 +- .../protected_branches/_branches_list.html.haml | 2 +- app/views/projects/snippets/show.html.haml | 6 +++--- app/views/projects/tags/_tag.html.haml | 4 ++-- .../team_members/_group_members.html.haml | 2 +- .../projects/team_members/_team_member.html.haml | 2 +- app/views/projects/tree/show.html.haml | 2 +- app/views/projects/wikis/edit.html.haml | 2 +- app/views/search/_filter.html.haml | 4 ++-- .../shared/_choose_group_avatar_button.html.haml | 2 +- app/views/snippets/show.html.haml | 6 +++--- app/views/snippets/user_index.html.haml | 2 +- 48 files changed, 92 insertions(+), 106 deletions(-) diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index 0224484d82b..cd6bf64c0ae 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -21,18 +21,6 @@ float: right; } - &.btn-small { - padding: 2px 10px; - font-size: 12px; - } - - &.btn-tiny { - font-size: 11px; - padding: 2px 6px; - line-height: 16px; - margin: 2px; - } - &.btn-close { color: $gl-danger; border-color: $gl-danger; @@ -84,6 +72,3 @@ } } } - -.btn-group-small > .btn { @extend .btn.btn-small; } -.btn-group-tiny > .btn { @extend .btn.btn-tiny; } diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index f46d6542c03..e7125c03993 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -30,7 +30,8 @@ color: #666; font-size: 14px; font-weight: normal; - padding: 10px 0; + padding: 3px 0; + margin-bottom: 10px; } .commit-info-row { diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 8c921cba543..f81504991d3 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -122,7 +122,7 @@ module DiffHelper params_copy = params.dup params_copy[:view] = 'inline' - link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn active' : 'btn') do + link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do 'Inline' end end @@ -131,7 +131,7 @@ module DiffHelper params_copy = params.dup params_copy[:view] = 'parallel' - link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active' : 'btn') do + link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do 'Side-by-side' end end diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml index 371ac55209f..3147cbd659f 100644 --- a/app/views/admin/applications/_delete_form.html.haml +++ b/app/views/admin/applications/_delete_form.html.haml @@ -1,4 +1,4 @@ -- submit_btn_css ||= 'btn btn-link btn-remove btn-small' +- submit_btn_css ||= 'btn btn-link btn-remove btn-sm' = form_tag admin_application_path(application) do %input{:name => "_method", :type => "hidden", :value => "delete"}/ = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css \ No newline at end of file diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index 7b483ee6556..c0afaf16d8f 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -52,7 +52,7 @@ %strong #{broadcast_message.ends_at.to_s(:short)}   - = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-tiny' do + = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-xs' do %i.fa.fa-times.cred .message= broadcast_message.message diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 8ae9a1edea9..4c53ff55708 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -40,8 +40,8 @@ %li .clearfix .pull-right.prepend-top-10 - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-small" - = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-small btn-remove" + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-sm" + = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-sm btn-remove" %h4 = link_to [:admin, group] do diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 3040faa722b..a28eae38925 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -80,7 +80,7 @@ = link_to user.name, admin_user_path(user) %span.pull-right.light = member.human_access - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 0c5db0805f9..7a9dc113f2a 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -33,5 +33,5 @@ %strong= hook.url .pull-right - = link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-small" - = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-small" + = link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-sm" + = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 3a1e61d5d8d..3bbe10bc270 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -74,8 +74,8 @@ .pull-right %span.label.label-gray = repository_size(project) - = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-small btn-remove" + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" + = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove" - if @projects.blank? .nothing-here-block 0 projects matches = paginate @projects, theme: "gitlab" diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 1421c2ea909..ebb3b3a636e 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -97,7 +97,7 @@ %strong #{@group.name} group members (#{@group.group_members.count}) .pull-right - = link_to admin_group_path(@group), class: 'btn btn-small' do + = link_to admin_group_path(@group), class: 'btn btn-sm' do %i.fa.fa-pencil-square-o %ul.well-list - @group_members.each do |member| @@ -111,7 +111,7 @@ %small (#{@project.users.count}) .pull-right - = link_to namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-tiny" do + = link_to namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-xs" do %i.fa.fa-pencil-square-o Manage Access %ul.well-list.team_members @@ -126,7 +126,7 @@ %span.light Owner - else %span.light= project_member.human_access - = link_to namespace_project_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-small btn-remove" do + = link_to namespace_project_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do %i.fa.fa-times .panel-footer = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 35e9fd5154f..25c1730ef70 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -78,11 +78,11 @@ %i.fa.fa-envelope = mail_to user.email, user.email, class: 'light'   - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-small" + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-sm" - unless user == current_user - if user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-small success" + = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-sm success" - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-small btn-remove" - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-small btn-remove" + = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-remove" + = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-sm btn-remove" = paginate @users, theme: "gitlab" diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 90c9f8c2f9b..5cf423ead88 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -46,7 +46,7 @@ %li %span.light Secondary email: %strong= email.email - = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-tiny btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do + = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do %i.fa.fa-times %li @@ -182,7 +182,7 @@ .pull-right %span.light= user_group.human_access - unless user_group.owner? - = link_to group_group_member_path(group, user_group), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(group, user_group), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-times.fa-inverse - else .nothing-here-block This user has no groups. @@ -221,7 +221,7 @@ %span.light= tm.human_access - if tm.respond_to? :project - = link_to namespace_project_team_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from project' do + = link_to namespace_project_team_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do %i.fa.fa-times #ssh-keys.tab-pane = render 'profiles/keys/key_table', admin: true diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index f7df5352512..c232644b021 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -18,12 +18,12 @@ %li .pull-right - if can?(current_user, :manage_group, group) - = link_to edit_group_path(group), class: "btn-small btn btn-grouped" do + = link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do %i.fa.fa-cogs Settings - if can?(current_user, :destroy, user_group) - = link_to leave_dashboard_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-small btn btn-grouped", title: 'Remove user from group' do + = link_to leave_dashboard_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Remove user from group' do %i.fa.fa-sign-out Leave diff --git a/app/views/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml index bf8098f38d0..6a5c917049d 100644 --- a/app/views/doorkeeper/applications/_delete_form.html.haml +++ b/app/views/doorkeeper/applications/_delete_form.html.haml @@ -1,4 +1,4 @@ -- submit_btn_css ||= 'btn btn-link btn-remove btn-small' +- submit_btn_css ||= 'btn btn-link btn-remove btn-sm' = form_tag oauth_application_path(application) do %input{:name => "_method", :type => "hidden", :value => "delete"}/ = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css \ No newline at end of file diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml index 5cbb4a70c19..4bba72167e3 100644 --- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -1,4 +1,4 @@ - submit_btn_css ||= 'btn btn-link btn-remove' = form_tag oauth_authorized_application_path(application) do %input{:name => "_method", :type => "hidden", :value => "delete"}/ - = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-small' \ No newline at end of file + = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-sm' \ No newline at end of file diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index cb40aa9970b..d2f0005142a 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -9,6 +9,6 @@ #{time_ago_with_tooltip(event.created_at)} .pull-right - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-create btn-small" do + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-create btn-sm" do Create Merge Request %hr diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 838290e4aca..49e7180bf98 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -21,7 +21,7 @@ = render 'shared/choose_group_avatar_button', f: f - if @group.avatar? %hr - = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" + = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" .form-actions = f.submit 'Save group', class: "btn btn-save" diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 6267006f63f..5bef796c5a2 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -17,19 +17,19 @@ %strong= member.human_access - if show_controls - if can?(current_user, :modify, member) - = button_tag class: "btn-tiny btn js-toggle-button", + = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o - if can?(current_user, :destroy, member) - if current_user == member.user - = link_to leave_dashboard_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = link_to leave_dashboard_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse - else - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do + = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse .edit-member.hide.js-toggle-content = form_for [@group, member], remote: true do |f| .alert.prepend-top-20 = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level) - = f.submit 'Save', class: 'btn btn-save btn-small' + = f.submit 'Save', class: 'btn btn-save btn-sm' diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 9febaab04a7..57dc235f5bb 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -22,9 +22,9 @@ .pull-right - if can?(current_user, :manage_group, @group) - if milestone.closed? - = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-small btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" - else - = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-small btn-close" + = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" %h4 = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) %div diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index dd2d84499ba..fea70f5cbc3 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -8,9 +8,9 @@ .pull-right - if can?(current_user, :manage_group, @group) - if @group_milestone.active? - = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-small btn-close" + = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" - else - = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-small btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" %hr - if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active? diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index c95347b3a55..3b8c26ed391 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -16,9 +16,9 @@ %span.label.label-gray = repository_size(project) .pull-right - = link_to 'Members', namespace_project_team_index_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-small" - = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-small btn-remove" + = link_to 'Members', namespace_project_team_index_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" + = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove" - if @projects.blank? .nothing-here-block This group has no projects yet diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml index c8c522e9812..97e98948f36 100644 --- a/app/views/profiles/applications.html.haml +++ b/app/views/profiles/applications.html.haml @@ -23,7 +23,7 @@ - application.redirect_uri.split.each do |uri| %div= uri %td= application.access_tokens.count - %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-small' + %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm' %td= render 'doorkeeper/applications/delete_form', application: application %fieldset.oauth-authorized-applications.prepend-top-20 diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 3bbad6fdf7b..9d8f33cbbaa 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -25,7 +25,7 @@ %strong= email.email %span.cgray added #{time_ago_with_tooltip(email.created_at)} - = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-small btn-remove pull-right' + = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right' %h4 Add email address = form_for 'email', url: profile_emails_path, html: { class: 'form-horizontal' } do |f| diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 8892302e25d..fe5770f45c3 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -9,4 +9,4 @@ %span.cgray added #{time_ago_with_tooltip(key.created_at)} %td - = link_to 'Remove', path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-small btn-remove delete-key pull-right" + = link_to 'Remove', path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right" diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 1a7bc353bf3..e6b204451c4 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -77,7 +77,7 @@ %br or change it at #{link_to "gravatar.com", "http://gravatar.com"} %hr - %a.choose-btn.btn.btn-small.js-choose-user-avatar-button + %a.choose-btn.btn.btn-sm.js-choose-user-avatar-button %i.fa.fa-paperclip %span Choose File ...   @@ -86,7 +86,7 @@ .light The maximum file size allowed is 200KB. - if @user.avatar? %hr - = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" + = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - if @user.public_profile? .alert.alert-info diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index b5b29540bb6..13f8271b979 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,22 +1,22 @@ .btn-group.tree-btn-group = edit_blob_link(@project, @ref, @path) = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), - class: 'btn btn-small', target: '_blank' + class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files - if @blob.text? - if current_page? namespace_project_blame_path(@project.namespace, @project, @id) = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id), - class: 'btn btn-small' + class: 'btn btn-sm' - else = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id), - class: 'btn btn-small' unless @blob.empty? + class: 'btn btn-sm' unless @blob.empty? = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), - class: 'btn btn-small' + class: 'btn btn-sm' - if @ref != @commit.sha = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, - tree_join(@commit.sha, @path)), class: 'btn btn-small' + tree_join(@commit.sha, @path)), class: 'btn btn-sm' - if allowed_tree_edit? - = button_tag class: 'remove-blob btn btn-small btn-remove', + = button_tag class: 'remove-blob btn btn-sm btn-remove', 'data-toggle' => 'modal', 'data-target' => '#modal-remove-blob' do Remove diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 8de629b03e9..0de8c509f2b 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -11,14 +11,14 @@ protected .pull-right - if can?(current_user, :download_code, @project) - = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-small' + = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-sm' - if branch.name != @repository.root_ref - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-small', method: :post, title: "Compare" do + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-sm', method: :post, title: "Compare" do %i.fa.fa-files-o Compare - if can_remove_branch?(@project, branch.name) - = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-small btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do + = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-sm btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do %i.fa.fa-trash-o - if commit diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index 230e164f24c..a2faa9d5e25 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -1,16 +1,16 @@ %li .pull-right - if @available_keys.include?(deploy_key) - = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-small', method: :put do + = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do %i.fa.fa-plus Enable - else - if deploy_key.projects.count > 1 - = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-small', method: :put do + = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do %i.fa.fa-power-off Disable - else - = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :delete, class: "btn btn-remove delete-key btn-small pull-right" + = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :delete, class: "btn btn-remove delete-key btn-sm pull-right" - key_project = deploy_key.projects.include?(@project) ? @project : deploy_key.projects.first diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 2569e91ccfa..36d98b26712 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -27,7 +27,7 @@ = check_box_tag nil, 1, false, class: 'js-toggle-diff-line-wrap' Wrap text   - = link_to '#', class: 'js-toggle-diff-comments btn btn-small' do + = link_to '#', class: 'js-toggle-diff-comments btn btn-sm' do %i.fa.fa-chevron-down Show/Hide comments   diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 9b5eb84a86d..d387ec2f75c 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -9,7 +9,7 @@ and %strong.cred #{@commit.stats.deletions} deletions   - = link_to '#', class: 'btn btn-small js-toggle-button' do + = link_to '#', class: 'btn btn-sm js-toggle-button' do Show diff stats %i.fa.fa-chevron-down .file-stats.js-toggle-content.hide diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index af1f342afbd..47abbba2eb2 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -3,15 +3,15 @@ Too many changes. .pull-right - unless diff_hard_limit_enabled? - = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true)), class: "btn btn-small btn-warning" + = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true)), class: "btn btn-sm btn-warning" - if current_controller?(:commit) or current_controller?(:merge_requests) - if current_controller?(:commit) - = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-warning btn-small" - = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-small" + = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-warning btn-sm" + = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-sm" - elsif @merge_request && @merge_request.persisted? - = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-warning btn-small" - = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-small" + = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-warning btn-sm" + = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-sm" %p To preserve performance only %strong #{allowed_diff_size} of #{diffs.size} diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index b4c36beda88..e0d75113a5e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -87,7 +87,7 @@ You can change your project avatar here - else You can upload a project avatar here - %a.choose-btn.btn.btn-small.js-choose-project-avatar-button + %a.choose-btn.btn.btn-sm.js-choose-project-avatar-button %i.icon-paper-clip %span Choose File ...   @@ -96,7 +96,7 @@ .light The maximum file size allowed is 200KB. - if @project.avatar? %hr - = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" + = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" .form-actions = f.submit 'Save changes', class: "btn btn-save" diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index e70cf5c3884..bbaddba31b9 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -58,8 +58,8 @@ - @hooks.each do |hook| %li .pull-right - = link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-small btn-grouped" - = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-small btn-grouped" + = link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped" + = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" .clearfix %span.monospace= hook.url %p diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 01e2133e283..3b50ce01351 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -41,10 +41,10 @@ .issue-actions - if can? current_user, :modify_issue, issue - if issue.closed? - = link_to 'Reopen', issue_path(issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-small btn-grouped reopen_issue btn-reopen", remote: true + = link_to 'Reopen', issue_path(issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-sm btn-grouped reopen_issue btn-reopen", remote: true - else - = link_to 'Close', issue_path(issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-small btn-grouped close_issue btn-close", remote: true - = link_to edit_namespace_project_issue_path(issue.project.namespace, issue.project, issue), class: "btn btn-small edit-issue-link btn-grouped" do + = link_to 'Close', issue_path(issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-sm btn-grouped close_issue btn-close", remote: true + = link_to edit_namespace_project_issue_path(issue.project.namespace, issue.project, issue), class: "btn btn-sm edit-issue-link btn-grouped" do %i.fa.fa-pencil-square-o Edit diff --git a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml index 0a642b7e6d0..59cb85edfce 100644 --- a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml +++ b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml @@ -4,7 +4,7 @@ - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && @merge_request.merged? .remove_source_branch_widget %p Changes merged into #{@merge_request.target_branch}. You can remove source branch now - = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-small remove_source_branch" do + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do %i.fa.fa-times Remove Source Branch diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index dcf56541db8..7039c85bb2c 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -1,10 +1,10 @@ %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) } .pull-right - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? - = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-small edit-milestone-link btn-grouped" do + = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-sm edit-milestone-link btn-grouped" do %i.fa.fa-pencil-square-o Edit - = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-small btn-close" + = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close" %h4 = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) - if milestone.expired? and not milestone.closed? diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 00b912742b2..183343913c9 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -104,7 +104,7 @@ .pull-right .light Need a group for several dependent projects? - = link_to new_group_path, class: "btn btn-tiny" do + = link_to new_group_path, class: "btn btn-xs" do Create a group .save-project-loader.hide diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 5406b80dc16..bb49f4de873 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -31,4 +31,4 @@ %td .pull-right - if can? current_user, :admin_project, @project - = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-small" + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 408e3c0224a..d19689a1056 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -28,10 +28,10 @@ .file-actions .btn-group - if can?(current_user, :modify_project_snippet, @snippet) - = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-small", title: 'Edit Snippet' - = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-small", target: "_blank" + = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", title: 'Edit Snippet' + = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - if can?(current_user, :admin_project_snippet, @snippet) - = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-small btn-remove", title: 'Delete Snippet' + = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet' = render 'shared/snippets/blob' %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 8da07222cba..f22308e54b0 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -9,9 +9,9 @@ = strip_gpg_signature(tag.message) .pull-right - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-small' + = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-sm' - if can?(current_user, :admin_project, @project) - = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-small btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do + = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-sm btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do %i.fa.fa-trash-o - if commit diff --git a/app/views/projects/team_members/_group_members.html.haml b/app/views/projects/team_members/_group_members.html.haml index df3c914fdea..12bd828a5e7 100644 --- a/app/views/projects/team_members/_group_members.html.haml +++ b/app/views/projects/team_members/_group_members.html.haml @@ -4,7 +4,7 @@ %strong #{@group.name} group members (#{group_users_count}) .pull-right - = link_to members_group_path(@group), class: 'btn btn-small' do + = link_to members_group_path(@group), class: 'btn btn-sm' do %i.fa.fa-pencil-square-o %ul.well-list - @group.group_members.order('access_level DESC').limit(20).each do |member| diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml index eb815447407..1a755bbd560 100644 --- a/app/views/projects/team_members/_team_member.html.haml +++ b/app/views/projects/team_members/_team_member.html.haml @@ -7,7 +7,7 @@ = form_for(member, as: :project_member, url: namespace_project_team_member_path(@project.namespace, @project, member.user)) do |f| = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: "trigger-submit"   - = link_to namespace_project_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from team' do + = link_to namespace_project_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from team' do %i.fa.fa-minus.fa-inverse = image_tag avatar_icon(user.email, 32), class: "avatar s32" %p diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index fc4616da6ec..feca1453697 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -3,7 +3,7 @@ - if can? current_user, :download_code, @project .tree-download-holder - = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-small pull-right hidden-xs hidden-sm', split_button: true + = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-sm pull-right hidden-xs hidden-sm', split_button: true #tree-holder.tree-holder.clearfix = render "tree", tree: @tree diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 5567f1af22a..566850cb78d 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -9,5 +9,5 @@ .pull-right - if @page.persisted? && can?(current_user, :admin_wiki, @project) - = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-small btn-remove" do + = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-sm btn-remove" do Delete this page diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index c635c04fb8f..ffc145497ab 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -1,5 +1,5 @@ .dropdown.inline - %button.dropdown-toggle.btn.btn-small{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} %i.fa.fa-tags %span.light Group: - if @group.present? @@ -17,7 +17,7 @@ = group.name .dropdown.inline.prepend-left-10.project-filter - %button.dropdown-toggle.btn.btn-small{type: 'button', 'data-toggle' => 'dropdown'} + %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} %i.fa.fa-tags %span.light Project: - if @project.present? diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml index 299c0bd42a2..000532b1c9a 100644 --- a/app/views/shared/_choose_group_avatar_button.html.haml +++ b/app/views/shared/_choose_group_avatar_button.html.haml @@ -1,4 +1,4 @@ -%a.choose-btn.btn.btn-small.js-choose-group-avatar-button +%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button %i.fa.fa-paperclip %span Choose File ...   diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index d9436caaadb..edfa2092df9 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -36,8 +36,8 @@ .file-actions .btn-group - if can?(current_user, :modify_personal_snippet, @snippet) - = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-small", title: 'Edit Snippet' - = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-small", target: "_blank" + = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-sm", title: 'Edit Snippet' + = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" - if can?(current_user, :admin_personal_snippet, @snippet) - = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-small btn-remove", title: 'Delete Snippet' + = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet' = render 'shared/snippets/blob' diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml index 67f3a68aa22..df524cd18b0 100644 --- a/app/views/snippets/user_index.html.haml +++ b/app/views/snippets/user_index.html.haml @@ -5,7 +5,7 @@ \/ Snippets - if current_user - = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do + = link_to new_snippet_path, class: "btn btn-sm add_new pull-right", title: "New Snippet" do Add new snippet %hr -- GitLab From f311e189d5449f6118fc84746c62e33585e4c39c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 14 Mar 2015 00:33:26 -0700 Subject: [PATCH 1335/1609] Improve compare switch button --- app/assets/stylesheets/pages/commits.scss | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index e167d044e47..84361e15481 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,17 +1,11 @@ .commits-compare-switch{ + @extend .btn; background: image-url("switch_icon.png") no-repeat center center; - width: 32px; - height: 32px; text-indent: -9999px; float: left; margin-right: 9px; - border: 1px solid #DDD; - @include border-radius(4px); - padding: 4px; - background-color: #EEE; } - .lists-separator { margin: 10px 0; border-color: #DDD; -- GitLab From 9b445c683657a85a090af69b12545b657fe5f616 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 14 Mar 2015 01:47:06 -0700 Subject: [PATCH 1336/1609] Return some merge widget styles and make it more compact --- app/assets/stylesheets/base/gl_bootstrap.scss | 5 +++++ app/assets/stylesheets/pages/merge_requests.scss | 11 +++-------- .../projects/merge_requests/show/_mr_accept.html.haml | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index 7012d31e318..be1ee90c18f 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -187,6 +187,11 @@ } } +.panel-succes .panel-heading, +.panel-info .panel-heading, +.panel-danger .panel-heading, +.panel-warning .panel-heading, +.panel-primary .panel-heading, .alert { a { @extend .alert-link; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 6babb824f3c..d41e34caba1 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -12,14 +12,8 @@ } .accept-merge-holder { - margin-top: 5px; - .accept-action { display: inline-block; - - .accept_merge_request { - padding: 10px 20px; - } } .accept-control { @@ -123,10 +117,11 @@ } .mr-state-widget { - background: $background-color; + font-size: 13px; + background: #F9F9F9; margin-bottom: 20px; color: #666; - border: 1px solid $border-color; + border: 1px solid #EEE; @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); .ci_widget { diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index fb2c3220b8a..9f51f84d400 100644 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -30,7 +30,7 @@ text: @merge_request.merge_commit_message, rows: 14, hint: true - %hr + %br .light If you still want to merge this request manually - use %strong -- GitLab From 6235b027ec19f3ba0e668a6ee6e77d861c4f68bd Mon Sep 17 00:00:00 2001 From: Vasilij Schneidermann Date: Sat, 14 Mar 2015 10:22:06 +0100 Subject: [PATCH 1337/1609] Fix typo --- lib/support/deploy/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/support/deploy/deploy.sh b/lib/support/deploy/deploy.sh index 4684957233a..adea4c7a747 100755 --- a/lib/support/deploy/deploy.sh +++ b/lib/support/deploy/deploy.sh @@ -4,7 +4,7 @@ # If any command return non-zero status - stop deploy set -e -echo 'Deploy: Stoping sidekiq..' +echo 'Deploy: Stopping sidekiq..' cd /home/git/gitlab/ && sudo -u git -H bundle exec rake sidekiq:stop RAILS_ENV=production echo 'Deploy: Show deploy index page' -- GitLab From 5710c1aaf865d56013e272d2f32abe70d987eafc Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 14 Mar 2015 10:30:48 -0600 Subject: [PATCH 1338/1609] Update snippet authorization Allow authors and admins to update the visibility level of personal and project snippets. --- app/models/ability.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/ability.rb b/app/models/ability.rb index 890417e780d..652c6001e08 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -225,13 +225,15 @@ class Ability [:issue, :note, :project_snippet, :personal_snippet, :merge_request].each do |name| define_method "#{name}_abilities" do |user, subject| - if subject.author == user - [ + if subject.author == user || user.is_admin? + rules = [ :"read_#{name}", :"write_#{name}", :"modify_#{name}", :"admin_#{name}" ] + rules.push(:change_visibility_level) if subject.is_a?(Snippet) + rules elsif subject.respond_to?(:assignee) && subject.assignee == user [ :"read_#{name}", -- GitLab From 13e9f4f33420bf0bae0b61b98dd3c2301d6f6223 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 14 Mar 2015 10:33:02 -0600 Subject: [PATCH 1339/1609] Add tests for snippet services Add Rspec tests for the new UpdateSnippetService and CreateSnippetService classes. --- spec/services/create_snippet_service_spec.rb | 44 +++++++++++++++++ spec/services/update_snippet_service_spec.rb | 52 ++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 spec/services/create_snippet_service_spec.rb create mode 100644 spec/services/update_snippet_service_spec.rb diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb new file mode 100644 index 00000000000..08689c15ca8 --- /dev/null +++ b/spec/services/create_snippet_service_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe CreateSnippetService do + before do + @user = create :user + @admin = create :user, admin: true + @opts = { + title: 'Test snippet', + file_name: 'snippet.rb', + content: 'puts "hello world"', + visibility_level: Gitlab::VisibilityLevel::PRIVATE + } + end + + context 'When public visibility is restricted' do + before do + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return( + [Gitlab::VisibilityLevel::PUBLIC] + ) + ) + + @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + it 'non-admins should not be able to create a public snippet' do + snippet = create_snippet(nil, @user, @opts) + expect(snippet.errors.messages).to have_key(:visibility_level) + expect(snippet.errors.messages[:visibility_level].first).to( + match('Public visibility has been restricted') + ) + end + + it 'admins should be able to create a public snippet' do + snippet = create_snippet(nil, @admin, @opts) + expect(snippet.errors.any?).to be_falsey + expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + def create_snippet(project, user, opts) + CreateSnippetService.new(project, user, opts).execute + end +end diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb new file mode 100644 index 00000000000..841ef9bfed1 --- /dev/null +++ b/spec/services/update_snippet_service_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe UpdateSnippetService do + before do + @user = create :user + @admin = create :user, admin: true + @opts = { + title: 'Test snippet', + file_name: 'snippet.rb', + content: 'puts "hello world"', + visibility_level: Gitlab::VisibilityLevel::PRIVATE + } + end + + context 'When public visibility is restricted' do + before do + allow_any_instance_of(ApplicationSetting).to( + receive(:restricted_visibility_levels).and_return( + [Gitlab::VisibilityLevel::PUBLIC] + ) + ) + + @snippet = create_snippet(@project, @user, @opts) + @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + end + + it 'non-admins should not be able to update to public visibility' do + old_visibility = @snippet.visibility_level + update_snippet(@project, @user, @snippet, @opts) + expect(@snippet.errors.messages).to have_key(:visibility_level) + expect(@snippet.errors.messages[:visibility_level].first).to( + match('Public visibility has been restricted') + ) + expect(@snippet.visibility_level).to eq(old_visibility) + end + + it 'admins should be able to update to pubic visibility' do + old_visibility = @snippet.visibility_level + update_snippet(@project, @admin, @snippet, @opts) + expect(@snippet.visibility_level).not_to eq(old_visibility) + expect(@snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + def create_snippet(project, user, opts) + CreateSnippetService.new(project, user, opts).execute + end + + def update_snippet(project = nil, user, snippet, opts) + UpdateSnippetService.new(project, user, snippet, opts).execute + end +end -- GitLab From d7f357a386162f645ba5a8550dfa1d3b22d0ff2c Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 14 Mar 2015 12:20:40 -0600 Subject: [PATCH 1340/1609] Use pre-wrap for diff code in discussion view --- CHANGELOG | 1 + app/assets/stylesheets/pages/notes.scss | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ba4f6e6de97..7bc561b6e06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 7.9.0 (unreleased) - Fix checkbox alignment on the application settings page. - Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger) - Fix mass-unassignment of issues (Robert Speicher) + - Fix hidden diff comments in merge request discussion view - Allow user confirmation to be skipped for new users via API - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 384ff6d740c..70505dc4300 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -89,6 +89,11 @@ ul.notes { } } +// Diff code in discussion view +.discussion-body .diff-file .line_content { + white-space: pre-wrap; +} + .diff-file .notes_holder { font-size: 13px; line-height: 18px; -- GitLab From 99f995755ef4b445216dd7baae35f5a4846ef30c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 16:16:51 +0100 Subject: [PATCH 1341/1609] Use `group_member` instead of `users_group` or `membership`. --- .../groups/group_members_controller.rb | 6 ++--- .../profiles/notifications_controller.rb | 6 ++--- app/mailers/emails/groups.rb | 8 +++---- app/models/ability.rb | 10 ++++---- app/services/notification_service.rb | 10 ++++---- app/views/admin/users/show.html.haml | 24 +++++++++---------- app/views/dashboard/groups/index.html.haml | 12 +++++----- .../group_members/_group_member.html.haml | 4 ++-- .../group_access_granted_email.html.haml | 2 +- .../group_access_granted_email.text.erb | 2 +- .../profiles/notifications/show.html.haml | 6 ++--- lib/api/group_members.rb | 10 ++++---- spec/models/members/group_member_spec.rb | 12 +++++----- spec/services/notification_service_spec.rb | 6 ++--- 14 files changed, 59 insertions(+), 59 deletions(-) diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index b083cf5d8c5..132452d61c9 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -18,10 +18,10 @@ class Groups::GroupMembersController < Groups::ApplicationController end def destroy - @users_group = @group.group_members.find(params[:id]) + @group_member = @group.group_members.find(params[:id]) - if can?(current_user, :destroy, @users_group) # May fail if last owner. - @users_group.destroy + if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner. + @group_member.destroy respond_to do |format| format.html { redirect_to members_group_path(@group), notice: 'User was successfully removed from group.' } format.js { render nothing: true } diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 433c19189af..3fdcbbab61b 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -14,9 +14,9 @@ class Profiles::NotificationsController < ApplicationController @saved = if type == 'global' current_user.update_attributes(user_params) elsif type == 'group' - users_group = current_user.group_members.find(params[:notification_id]) - users_group.notification_level = params[:notification_level] - users_group.save + group_member = current_user.group_members.find(params[:notification_id]) + group_member.notification_level = params[:notification_level] + group_member.save else project_member = current_user.project_members.find(params[:notification_id]) project_member.notification_level = params[:notification_level] diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb index 8c09389985e..26f43bf955e 100644 --- a/app/mailers/emails/groups.rb +++ b/app/mailers/emails/groups.rb @@ -1,10 +1,10 @@ module Emails module Groups - def group_access_granted_email(user_group_id) - @membership = GroupMember.find(user_group_id) - @group = @membership.group + def group_access_granted_email(group_member_id) + @group_member = GroupMember.find(group_member_id) + @group = @group_member.group @target_url = group_url(@group) - mail(to: @membership.user.email, + mail(to: @group_member.user.email, subject: subject("Access to group was granted")) end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 890417e780d..773b51a7bcd 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -14,7 +14,7 @@ class Ability when "MergeRequest" then merge_request_abilities(user, subject) when "Group" then group_abilities(user, subject) when "Namespace" then namespace_abilities(user, subject) - when "GroupMember" then users_group_abilities(user, subject) + when "GroupMember" then group_member_abilities(user, subject) else [] end.concat(global_abilities(user)) end @@ -248,17 +248,17 @@ class Ability end end - def users_group_abilities(user, subject) + def group_member_abilities(user, subject) rules = [] target_user = subject.user group = subject.group can_manage = group_abilities(user, group).include?(:manage_group) if can_manage && (user != target_user) - rules << :modify - rules << :destroy + rules << :modify_group_member + rules << :destroy_group_member end if !group.last_owner?(user) && (can_manage || (user == target_user)) - rules << :destroy + rules << :destroy_group_member end rules end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 0063b7ce40c..843cb0c5ee0 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -194,11 +194,11 @@ class NotificationService project_members = project_member_notification(project) users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL) - users_with_group_level_global = users_group_notification(project, Notification::N_GLOBAL) + users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL) users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq) users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users) - users_with_group_setting = select_users_group_setting(project, project_members, users_with_group_level_global, users) + users_with_group_setting = select_group_member_setting(project, project_members, users_with_group_level_global, users) User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a end @@ -213,7 +213,7 @@ class NotificationService end end - def users_group_notification(project, notification_level) + def group_member_notification(project, notification_level) if project.group project.group.group_members.where(notification_level: notification_level).pluck(:user_id) else @@ -243,8 +243,8 @@ class NotificationService end # Build a list of users based on group notification settings - def select_users_group_setting(project, project_members, global_setting, users_global_level_watch) - uids = users_group_notification(project, Notification::N_WATCH) + def select_group_member_setting(project, project_members, global_setting, users_global_level_watch) + uids = group_member_notification(project, Notification::N_WATCH) # Group setting is watch, add to users list if user is not project member users = [] diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 5cf423ead88..0a2934d3bda 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -174,15 +174,15 @@ .panel.panel-default .panel-heading Groups: %ul.well-list - - @user.group_members.each do |user_group| - - group = user_group.group + - @user.group_members.each do |group_member| + - group = group_member.group %li.group_member - %span{class: ("list-item-name" unless user_group.owner?)} + %span{class: ("list-item-name" unless group_member.owner?)} %strong= link_to group.name, admin_group_path(group) .pull-right - %span.light= user_group.human_access - - unless user_group.owner? - = link_to group_group_member_path(group, user_group), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + %span.light= group_member.human_access + - unless group_member.owner? + = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-times.fa-inverse - else .nothing-here-block This user has no groups. @@ -207,21 +207,21 @@ .panel-heading Joined projects (#{@joined_projects.count}) %ul.well-list - @joined_projects.sort_by(&:name_with_namespace).each do |project| - - tm = project.team.find_tm(@user.id) + - member = project.team.find_member(@user.id) %li.project_member .list-item-name = link_to admin_namespace_project_path(project.namespace, project), class: dom_class(project) do = project.name_with_namespace - - if tm + - if member .pull-right - - if tm.owner? + - if member.owner? %span.light Owner - else - %span.light= tm.human_access + %span.light= member.human_access - - if tm.respond_to? :project - = link_to namespace_project_team_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do + - if member.respond_to? :project + = link_to namespace_project_project_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do %i.fa.fa-times #ssh-keys.tab-pane = render 'profiles/keys/key_table', admin: true diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index c232644b021..76f7d660f36 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -11,10 +11,10 @@ .panel.panel-default .panel-heading %strong Groups - (#{@user_groups.count}) + (#{@group_members.count}) %ul.well-list - - @user_groups.each do |user_group| - - group = user_group.group + - @group_members.each do |group_member| + - group = group_member.group %li .pull-right - if can?(current_user, :manage_group, group) @@ -22,7 +22,7 @@ %i.fa.fa-cogs Settings - - if can?(current_user, :destroy, user_group) + - if can?(current_user, :destroy_group_member, group_member) = link_to leave_dashboard_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Remove user from group' do %i.fa.fa-sign-out Leave @@ -32,9 +32,9 @@ %strong= group.name as - %strong #{user_group.human_access} + %strong #{group_member.human_access} %div.light #{pluralize(group.projects.count, "project")}, #{pluralize(group.users.count, "user")} -= paginate @user_groups += paginate @group_members diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 5bef796c5a2..2fc91df0931 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -16,11 +16,11 @@ %span.pull-right %strong= member.human_access - if show_controls - - if can?(current_user, :modify, member) + - if can?(current_user, :modify_group_member, member) = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o - - if can?(current_user, :destroy, member) + - if can?(current_user, :destroy_group_member, member) - if current_user == member.user = link_to leave_dashboard_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse diff --git a/app/views/notify/group_access_granted_email.html.haml b/app/views/notify/group_access_granted_email.html.haml index 823ebf77347..f1916d624b6 100644 --- a/app/views/notify/group_access_granted_email.html.haml +++ b/app/views/notify/group_access_granted_email.html.haml @@ -1,4 +1,4 @@ %p - = "You have been granted #{@membership.human_access} access to group" + = "You have been granted #{@group_member.human_access} access to group" = link_to group_url(@group) do = @group.name diff --git a/app/views/notify/group_access_granted_email.text.erb b/app/views/notify/group_access_granted_email.text.erb index 331bb98d5c9..ef9617bfc16 100644 --- a/app/views/notify/group_access_granted_email.text.erb +++ b/app/views/notify/group_access_granted_email.text.erb @@ -1,4 +1,4 @@ -You have been granted <%= @membership.human_access %> access to group <%= @group.name %> +You have been granted <%= @group_member.human_access %> access to group <%= @group.name %> <%= url_for(group_url(@group)) %> diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 6cf5c81c19e..273e72f8a4d 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -62,9 +62,9 @@ By default, all projects and groups will use the notification level set above. %h4 Groups: %ul.bordered-list - - @group_members.each do |users_group| - - notification = Notification.new(users_group) - = render 'settings', type: 'group', membership: users_group, notification: notification + - @group_members.each do |group_member| + - notification = Notification.new(group_member) + = render 'settings', type: 'group', membership: group_member, notification: notification .col-md-6 %p diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb index c9c9ccbcb2e..ed54c7f6ff0 100644 --- a/lib/api/group_members.rb +++ b/lib/api/group_members.rb @@ -53,14 +53,14 @@ module API authorize! :manage_group, group required_attributes! [:access_level] - team_member = group.group_members.find_by(user_id: params[:user_id]) - not_found!('User can not be found') if team_member.nil? + group_member = group.group_members.find_by(user_id: params[:user_id]) + not_found!('User can not be found') if group_member.nil? - if team_member.update_attributes(access_level: params[:access_level]) - @member = team_member.user + if group_member.update_attributes(access_level: params[:access_level]) + @member = group_member.user present @member, with: Entities::GroupMember, group: group else - handle_member_errors team_member.errors + handle_member_errors group_member.errors end end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index e04f1741b24..e206c11f33a 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -28,18 +28,18 @@ describe GroupMember do describe "#after_update" do before do - @membership = create :group_member - @membership.stub(notification_service: double('NotificationService').as_null_object) + @group_member = create :group_member + @group_member.stub(notification_service: double('NotificationService').as_null_object) end it "should send email to user" do - expect(@membership).to receive(:notification_service) - @membership.update_attribute(:access_level, GroupMember::MASTER) + expect(@group_member).to receive(:notification_service) + @group_member.update_attribute(:access_level, GroupMember::MASTER) end it "does not send an email when the access level has not changed" do - expect(@membership).not_to receive(:notification_service) - @membership.update_attribute(:access_level, GroupMember::OWNER) + expect(@group_member).not_to receive(:notification_service) + @group_member.update_attribute(:access_level, GroupMember::OWNER) end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 2074f8e7f78..34737348d41 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -69,9 +69,9 @@ describe NotificationService do user_project = note.project.project_members.find_by_user_id(@u_watcher.id) user_project.notification_level = Notification::N_PARTICIPATING user_project.save - user_group = note.project.group.group_members.find_by_user_id(@u_watcher.id) - user_group.notification_level = Notification::N_GLOBAL - user_group.save + group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id) + group_member.notification_level = Notification::N_GLOBAL + group_member.save end it do -- GitLab From 31fc73f0a9b9225ba3737b9525fcf7a1695a45f2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 16:22:03 +0100 Subject: [PATCH 1342/1609] Use `project_member` instead of `team_member`. --- app/assets/javascripts/dispatcher.js.coffee | 2 +- app/assets/stylesheets/generic/common.scss | 2 +- app/assets/stylesheets/pages/projects.scss | 2 +- app/controllers/admin/groups_controller.rb | 4 +- .../projects/project_members_controller.rb | 88 +++++++++++++++++++ .../projects/team_members_controller.rb | 73 --------------- app/helpers/search_helper.rb | 2 +- app/helpers/tab_helper.rb | 2 +- app/models/ability.rb | 6 +- app/models/members/project_member.rb | 4 +- app/models/project.rb | 4 +- app/models/user.rb | 5 +- app/services/notification_service.rb | 12 +-- app/services/projects/participants_service.rb | 4 +- app/views/admin/groups/show.html.haml | 2 +- app/views/admin/projects/show.html.haml | 4 +- app/views/groups/projects.html.haml | 2 +- app/views/projects/_dropdown.html.haml | 2 +- app/views/projects/_settings_nav.html.haml | 2 +- config/routes.rb | 4 +- features/steps/admin/groups.rb | 2 +- lib/api/project_members.rb | 28 +++--- lib/gitlab/markdown.rb | 2 +- spec/helpers/gitlab_markdown_helper_spec.rb | 2 +- spec/routing/project_routing_spec.rb | 17 ++-- 25 files changed, 143 insertions(+), 134 deletions(-) create mode 100644 app/controllers/projects/project_members_controller.rb delete mode 100644 app/controllers/projects/team_members_controller.rb diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index e1015a63d52..5da774cfb23 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -127,7 +127,7 @@ class Dispatcher new DropzoneInput($('.wiki-form')) when 'snippets', 'labels', 'graphs' shortcut_handler = new ShortcutsNavigation() - when 'team_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' + when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() new UsersSelect() diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index af8e90eb1a9..876eea72e8a 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -167,7 +167,7 @@ li.note { background-color: inherit; } -.team_member_show { +.project_member_show { td:first-child { color: #aaa; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index e359aa45025..74755951670 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -156,7 +156,7 @@ ul.nav.nav-projects-tabs { } } -.team_member_row form { +.project_member_row form { margin: 0px; } diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index e338abeac4c..9d9adaa467f 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,5 +1,5 @@ class Admin::GroupsController < Admin::ApplicationController - before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update] + before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update] def index @groups = Group.all @@ -40,7 +40,7 @@ class Admin::GroupsController < Admin::ApplicationController end end - def project_teams_update + def members_update @group.add_users(params[:user_ids].split(','), params[:access_level]) redirect_to [:admin, @group], notice: 'Users were successfully added.' diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb new file mode 100644 index 00000000000..4ab15db01f7 --- /dev/null +++ b/app/controllers/projects/project_members_controller.rb @@ -0,0 +1,88 @@ +class Projects::ProjectMembersController < Projects::ApplicationController + # Authorize + before_filter :authorize_admin_project!, except: :leave + + layout "project_settings" + + def index + @project_members = @project.project_members + + if params[:search].present? + users = @project.users.search(params[:search]).to_a + @project_members = @project_members.where(user_id: users) + end + + @project_members = @project_members.order('access_level DESC') + + @group = @project.group + if @group + @group_members = @group.group_members + + if params[:search].present? + users = @group.users.search(params[:search]).to_a + @group_members = @group_members.where(user_id: users) + end + + @group_members = @group_members.order('access_level DESC').limit(20) + end + + @project_member = @project.project_members.new + end + + def new + @project_member = @project.project_members.new + end + + def create + users = User.where(id: params[:user_ids].split(',')) + @project.team << [users, params[:access_level]] + + redirect_to namespace_project_project_members_path(@project.namespace, @project) + end + + def update + @project_member = @project.project_members.find_by(user_id: member) + @project_member.update_attributes(member_params) + end + + def destroy + @project_member = @project.project_members.find_by(user_id: member) + @project_member.destroy + + respond_to do |format| + format.html do + redirect_to namespace_project_project_members_path(@project.namespace, + @project) + end + format.js { render nothing: true } + end + end + + def leave + @project.project_members.find_by(user_id: current_user).destroy + + respond_to do |format| + format.html { redirect_to :back } + format.js { render nothing: true } + end + end + + def apply_import + giver = Project.find(params[:source_project_id]) + status = @project.team.import(giver) + notice = status ? "Successfully imported" : "Import failed" + + redirect_to(namespace_project_project_members_path(project.namespace, project), + notice: notice) + end + + protected + + def member + @member ||= User.find_by(username: params[:id]) + end + + def member_params + params.require(:project_member).permit(:user_id, :access_level) + end +end diff --git a/app/controllers/projects/team_members_controller.rb b/app/controllers/projects/team_members_controller.rb deleted file mode 100644 index f8a248ed729..00000000000 --- a/app/controllers/projects/team_members_controller.rb +++ /dev/null @@ -1,73 +0,0 @@ -class Projects::TeamMembersController < Projects::ApplicationController - # Authorize - before_filter :authorize_admin_project!, except: :leave - - layout "project_settings" - - def index - @group = @project.group - @project_members = @project.project_members.order('access_level DESC') - end - - def new - @user_project_relation = @project.project_members.new - end - - def create - users = User.where(id: params[:user_ids].split(',')) - @project.team << [users, params[:access_level]] - - redirect_to namespace_project_team_index_path(@project.namespace, @project) - end - - def update - @user_project_relation = @project.project_members.find_by(user_id: member) - @user_project_relation.update_attributes(member_params) - - unless @user_project_relation.valid? - flash[:alert] = "User should have at least one role" - end - redirect_to namespace_project_team_index_path(@project.namespace, @project) - end - - def destroy - @user_project_relation = @project.project_members.find_by(user_id: member) - @user_project_relation.destroy - - respond_to do |format| - format.html do - redirect_to namespace_project_team_index_path(@project.namespace, - @project) - end - format.js { render nothing: true } - end - end - - def leave - @project.project_members.find_by(user_id: current_user).destroy - - respond_to do |format| - format.html { redirect_to :back } - format.js { render nothing: true } - end - end - - def apply_import - giver = Project.find(params[:source_project_id]) - status = @project.team.import(giver) - notice = status ? "Successfully imported" : "Import failed" - - redirect_to(namespace_project_team_index_path(project.namespace, project), - notice: notice) - end - - protected - - def member - @member ||= User.find_by(username: params[:id]) - end - - def member_params - params.require(:project_member).permit(:user_id, :access_level) - end -end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index cb829037697..7d3fcfa7037 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -60,7 +60,7 @@ module SearchHelper { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { label: "#{prefix} - Team", url: namespace_project_team_index_path(@project.namespace, @project) }, + { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 7a401a274d3..a1d263d9d3a 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -89,7 +89,7 @@ module TabHelper def project_tab_class return "active" if current_page?(controller: "/projects", action: :edit, id: @project) - if ['services', 'hooks', 'deploy_keys', 'team_members', 'protected_branches'].include? controller.controller_name + if ['services', 'hooks', 'deploy_keys', 'project_members', 'protected_branches'].include? controller.controller_name "active" end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 773b51a7bcd..855134dd39b 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -37,7 +37,7 @@ class Ability :read_issue, :read_milestone, :read_project_snippet, - :read_team_member, + :read_project_member, :read_merge_request, :read_note, :download_code @@ -119,7 +119,7 @@ class Ability :read_issue, :read_milestone, :read_project_snippet, - :read_team_member, + :read_project_member, :read_merge_request, :read_note, :write_project, @@ -166,7 +166,7 @@ class Ability :admin_issue, :admin_milestone, :admin_project_snippet, - :admin_team_member, + :admin_project_member, :admin_merge_request, :admin_note, :admin_wiki, diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index e4791d0f0aa..6b13e0ff30b 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -116,14 +116,14 @@ class ProjectMember < Member def post_create_hook unless owner? event_service.join_project(self.project, self.user) - notification_service.new_team_member(self) + notification_service.new_project_member(self) end system_hook_service.execute_hooks_for(self, :create) end def post_update_hook - notification_service.update_team_member(self) if self.access_level_changed? + notification_service.update_project_member(self) if self.access_level_changed? end def post_destroy_hook diff --git a/app/models/project.rb b/app/models/project.rb index c45338bf4eb..f0b416c8f4a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -445,13 +445,13 @@ class Project < ActiveRecord::Base end end - def team_member_by_name_or_email(name = nil, email = nil) + def project_member_by_name_or_email(name = nil, email = nil) user = users.where('name like ? or email like ?', name, email).first project_members.where(user: user) if user end # Get Team Member record by user id - def team_member_by_id(user_id) + def project_member_by_id(user_id) project_members.find_by(user_id: user_id) end diff --git a/app/models/user.rb b/app/models/user.rb index 0d40ac8309e..ba325132df8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -169,11 +169,8 @@ class User < ActiveRecord::Base scope :admins, -> { where(admin: true) } scope :blocked, -> { with_state(:blocked) } scope :active, -> { with_state(:active) } - scope :in_team, ->(team){ where(id: team.member_ids) } - scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } - scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active } # # Class methods @@ -407,7 +404,7 @@ class User < ActiveRecord::Base end def tm_of(project) - project.team_member_by_id(self.id) + project.project_member_by_id(self.id) end def already_forked?(project) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 843cb0c5ee0..fb5baaf74b3 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -162,20 +162,20 @@ class NotificationService end end - def new_team_member(project_member) + def new_project_member(project_member) mailer.project_access_granted_email(project_member.id) end - def update_team_member(project_member) + def update_project_member(project_member) mailer.project_access_granted_email(project_member.id) end - def new_group_member(users_group) - mailer.group_access_granted_email(users_group.id) + def new_group_member(group_member) + mailer.group_access_granted_email(group_member.id) end - def update_group_member(users_group) - mailer.group_access_granted_email(users_group.id) + def update_group_member(group_member) + mailer.group_access_granted_email(group_member.id) end def project_was_moved(project) diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index f6f9aceef95..bcbacbff562 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -12,8 +12,8 @@ module Projects else [] end - team_members = sorted(@project.team.members) - participants = all_members + groups + team_members + participating + project_members = sorted(@project.team.members) + participants = all_members + groups + project_members + participating participants.uniq end diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index a28eae38925..7d292118075 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -58,7 +58,7 @@ Read more about project permissions %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" - = form_tag project_teams_update_admin_group_path(@group), id: "new_team_member", class: "bulk_import", method: :put do + = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div = users_select_tag(:user_ids, multiple: true) %div.prepend-top-10 diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index ebb3b3a636e..70ebc9561d4 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -114,7 +114,7 @@ = link_to namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-xs" do %i.fa.fa-pencil-square-o Manage Access - %ul.well-list.team_members + %ul.well-list.project_members - @project_members.each do |project_member| - user = project_member.user %li.project_member @@ -126,7 +126,7 @@ %span.light Owner - else %span.light= project_member.human_access - = link_to namespace_project_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do + = link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do %i.fa.fa-times .panel-footer = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 3b8c26ed391..dd1fa3840d5 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -16,7 +16,7 @@ %span.label.label-gray = repository_size(project) .pull-right - = link_to 'Members', namespace_project_team_index_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" + = link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove" - if @projects.blank? diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml index 2d5120f283b..3cdbbb7b043 100644 --- a/app/views/projects/_dropdown.html.haml +++ b/app/views/projects/_dropdown.html.haml @@ -15,7 +15,7 @@ %li = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do New snippet - - if can?(current_user, :admin_team_member, @project) + - if can?(current_user, :admin_project_member, @project) %li = link_to new_namespace_project_team_member_path(@project.namespace, @project), title: "New project member" do New project member diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml index 7fc3d44034f..6f19206b17f 100644 --- a/app/views/projects/_settings_nav.html.haml +++ b/app/views/projects/_settings_nav.html.haml @@ -4,8 +4,8 @@ %i.fa.fa-pencil-square-o %span Project - = nav_link(controller: [:team_members, :teams]) do = link_to namespace_project_team_index_path(@project.namespace, @project), title: 'Members', class: "team-tab tab" do + = nav_link(controller: [:project_members, :teams]) do %i.fa.fa-users %span Members diff --git a/config/routes.rb b/config/routes.rb index 889995e92a6..547d1aa8660 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -136,7 +136,7 @@ Gitlab::Application.routes.draw do resources :groups, constraints: { id: /[^\/]+/ } do member do - put :project_teams_update + put :members_update end end @@ -445,7 +445,7 @@ Gitlab::Application.routes.draw do end end - resources :team_members, except: [:index, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do + resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do collection do delete :leave diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index 6bcec48be88..721460b9371 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -38,7 +38,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps When 'I select user "John Doe" from user list as "Reporter"' do select2(user_john.id, from: "#user_ids", multiple: true) - within "#new_team_member" do + within "#new_project_member" do select "Reporter", from: "access_level" end click_button "Add users to group" diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb index 73cf062155b..c756bb479fc 100644 --- a/lib/api/project_members.rb +++ b/lib/api/project_members.rb @@ -46,19 +46,19 @@ module API required_attributes! [:user_id, :access_level] # either the user is already a team member or a new one - team_member = user_project.team_member_by_id(params[:user_id]) - if team_member.nil? - team_member = user_project.project_members.new( + project_member = user_project.project_member_by_id(params[:user_id]) + if project_member.nil? + project_member = user_project.project_members.new( user_id: params[:user_id], access_level: params[:access_level] ) end - if team_member.save - @member = team_member.user + if project_member.save + @member = project_member.user present @member, with: Entities::ProjectMember, project: user_project else - handle_member_errors team_member.errors + handle_member_errors project_member.errors end end @@ -74,14 +74,14 @@ module API authorize! :admin_project, user_project required_attributes! [:access_level] - team_member = user_project.project_members.find_by(user_id: params[:user_id]) - not_found!("User can not be found") if team_member.nil? + project_member = user_project.project_members.find_by(user_id: params[:user_id]) + not_found!("User can not be found") if project_member.nil? - if team_member.update_attributes(access_level: params[:access_level]) - @member = team_member.user + if project_member.update_attributes(access_level: params[:access_level]) + @member = project_member.user present @member, with: Entities::ProjectMember, project: user_project else - handle_member_errors team_member.errors + handle_member_errors project_member.errors end end @@ -94,9 +94,9 @@ module API # DELETE /projects/:id/members/:user_id delete ":id/members/:user_id" do authorize! :admin_project, user_project - team_member = user_project.project_members.find_by(user_id: params[:user_id]) - unless team_member.nil? - team_member.destroy + project_member = user_project.project_members.find_by(user_id: params[:user_id]) + unless project_member.nil? + project_member.destroy else { message: "Access revoked", id: params[:user_id].to_i } end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 2dfa18da482..c3a8d90ef54 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -200,7 +200,7 @@ module Gitlab def reference_user(identifier, project = @project, _ = nil) options = html_options.merge( - class: "gfm gfm-team_member #{html_options[:class]}" + class: "gfm gfm-project_member #{html_options[:class]}" ) if identifier == "all" diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index fd80c615221..6ba27b536e4 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -180,7 +180,7 @@ describe GitlabMarkdownHelper do end it "should include standard gfm classes" do - expect(gfm(actual)).to match(/class="\s?gfm gfm-team_member\s?"/) + expect(gfm(actual)).to match(/class="\s?gfm gfm-project_member\s?"/) end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 4308a765b56..d9bd91f5990 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -338,17 +338,14 @@ describe Projects::CommitsController, 'routing' do end end -# project_team_members GET /:project_id/team_members(.:format) team_members#index -# POST /:project_id/team_members(.:format) team_members#create -# new_project_team_member GET /:project_id/team_members/new(.:format) team_members#new -# edit_project_team_member GET /:project_id/team_members/:id/edit(.:format) team_members#edit -# project_team_member GET /:project_id/team_members/:id(.:format) team_members#show -# PUT /:project_id/team_members/:id(.:format) team_members#update -# DELETE /:project_id/team_members/:id(.:format) team_members#destroy -describe Projects::TeamMembersController, 'routing' do +# project_project_members GET /:project_id/project_members(.:format) project_members#index +# POST /:project_id/project_members(.:format) project_members#create +# PUT /:project_id/project_members/:id(.:format) project_members#update +# DELETE /:project_id/project_members/:id(.:format) project_members#destroy +describe Projects::ProjectMembersController, 'routing' do it_behaves_like 'RESTful project resources' do - let(:actions) { [:new, :create, :update, :destroy] } - let(:controller) { 'team_members' } + let(:actions) { [:index, :create, :update, :destroy] } + let(:controller) { 'project_members' } end end -- GitLab From bbdf23261c00de58b18948381b0cc206e7d3501f Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 16:23:45 +0100 Subject: [PATCH 1343/1609] Use `member` instead of `tm`. --- app/models/project_team.rb | 42 ++++++++++++++-------------- app/services/notification_service.rb | 24 ++++++++-------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/app/models/project_team.rb b/app/models/project_team.rb index bc9c3ce58f6..d4a07caf9ef 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -31,16 +31,16 @@ class ProjectTeam user end - def find_tm(user_id) - tm = project.project_members.find_by(user_id: user_id) + def find_member(user_id) + member = project.project_members.find_by(user_id: user_id) # If user is not in project members # we should check for group membership - if group && !tm - tm = group.group_members.find_by(user_id: user_id) + if group && !member + member = group.group_members.find_by(user_id: user_id) end - tm + member end def add_user(user, access) @@ -91,24 +91,24 @@ class ProjectTeam def import(source_project) target_project = project - source_team = source_project.project_members.to_a + source_members = source_project.project_members.to_a target_user_ids = target_project.project_members.pluck(:user_id) - source_team.reject! do |tm| + source_members.reject! do |member| # Skip if user already present in team - target_user_ids.include?(tm.user_id) + target_user_ids.include?(member.user_id) end - source_team.map! do |tm| - new_tm = tm.dup - new_tm.id = nil - new_tm.source = target_project - new_tm + source_members.map! do |member| + new_member = member.dup + new_member.id = nil + new_member.source = target_project + new_member end ProjectMember.transaction do - source_team.each do |tm| - tm.save + source_members.each do |member| + member.save end end @@ -118,26 +118,26 @@ class ProjectTeam end def guest?(user) - max_tm_access(user.id) == Gitlab::Access::GUEST + max_member_access(user.id) == Gitlab::Access::GUEST end def reporter?(user) - max_tm_access(user.id) == Gitlab::Access::REPORTER + max_member_access(user.id) == Gitlab::Access::REPORTER end def developer?(user) - max_tm_access(user.id) == Gitlab::Access::DEVELOPER + max_member_access(user.id) == Gitlab::Access::DEVELOPER end def master?(user) - max_tm_access(user.id) == Gitlab::Access::MASTER + max_member_access(user.id) == Gitlab::Access::MASTER end def member?(user_id) - !!find_tm(user_id) + !!find_member(user_id) end - def max_tm_access(user_id) + def max_member_access(user_id) access = [] access << project.project_members.find_by(user_id: user_id).try(:access_field) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index fb5baaf74b3..fb411c3e23d 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -273,20 +273,20 @@ class NotificationService users.reject do |user| next user.notification.disabled? unless project - tm = project.project_members.find_by(user_id: user.id) + member = project.project_members.find_by(user_id: user.id) - if !tm && project.group - tm = project.group.group_members.find_by(user_id: user.id) + if !member && project.group + member = project.group.group_members.find_by(user_id: user.id) end # reject users who globally disabled notification and has no membership - next user.notification.disabled? unless tm + next user.notification.disabled? unless member # reject users who disabled notification in project - next true if tm.notification.disabled? + next true if member.notification.disabled? # reject users who have N_GLOBAL in project and disabled in global settings - tm.notification.global? && user.notification.disabled? + member.notification.global? && user.notification.disabled? end end @@ -297,20 +297,20 @@ class NotificationService users.reject do |user| next user.notification.mention? unless project - tm = project.project_members.find_by(user_id: user.id) + member = project.project_members.find_by(user_id: user.id) - if !tm && project.group - tm = project.group.group_members.find_by(user_id: user.id) + if !member && project.group + member = project.group.group_members.find_by(user_id: user.id) end # reject users who globally set mention notification and has no membership - next user.notification.mention? unless tm + next user.notification.mention? unless member # reject users who set mention notification in project - next true if tm.notification.mention? + next true if member.notification.mention? # reject users who have N_MENTION in project and disabled in global settings - tm.notification.global? && user.notification.mention? + member.notification.global? && user.notification.mention? end end -- GitLab From e97cdb042d941c989b303137c42e4c22f535f36b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 16:23:59 +0100 Subject: [PATCH 1344/1609] Remove old team scopes. --- app/models/issue.rb | 1 - app/models/merge_request.rb | 1 - app/models/project.rb | 1 - 3 files changed, 3 deletions(-) diff --git a/app/models/issue.rb b/app/models/issue.rb index 19e43ebd788..6e102051387 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -32,7 +32,6 @@ class Issue < ActiveRecord::Base validates :project, presence: true scope :of_group, ->(group) { where(project_id: group.project_ids) } - scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) } scope :cared, ->(user) { where(assignee_id: user) } scope :open_for, ->(user) { opened.assigned_to(user) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f758126cfeb..4cbdc612297 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -115,7 +115,6 @@ class MergeRequest < ActiveRecord::Base validate :validate_fork scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) } - scope :of_user_team, ->(team) { where("(source_project_id in (:team_project_ids) OR target_project_id in (:team_project_ids) AND assignee_id in (:team_member_ids))", team_project_ids: team.project_ids, team_member_ids: team.member_ids) } scope :merged, -> { with_state(:merged) } scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) } scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) } diff --git a/app/models/project.rb b/app/models/project.rb index f0b416c8f4a..1d1ae569fc2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -157,7 +157,6 @@ class Project < ActiveRecord::Base scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) } scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped } scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) } - scope :in_team, ->(team) { where('projects.id IN (:ids)', ids: team.projects.map(&:id)) } scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } scope :in_group_namespace, -> { joins(:group) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } -- GitLab From 75aff0f79c73ccc430a8c92b2317d114a5c8b24d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 16:25:54 +0100 Subject: [PATCH 1345/1609] Move project members index from `/team` to `/project_members` --- app/views/admin/projects/show.html.haml | 2 +- app/views/projects/_settings_nav.html.haml | 2 +- config/routes.rb | 1 - features/steps/shared/paths.rb | 2 +- spec/features/security/project/internal_access_spec.rb | 4 ++-- spec/features/security/project/private_access_spec.rb | 4 ++-- spec/features/security/project/public_access_spec.rb | 4 ++-- 7 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 70ebc9561d4..077ee569085 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -111,7 +111,7 @@ %small (#{@project.users.count}) .pull-right - = link_to namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-xs" do + = link_to namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-xs" do %i.fa.fa-pencil-square-o Manage Access %ul.well-list.project_members diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml index 6f19206b17f..281a84a3d3c 100644 --- a/app/views/projects/_settings_nav.html.haml +++ b/app/views/projects/_settings_nav.html.haml @@ -4,8 +4,8 @@ %i.fa.fa-pencil-square-o %span Project - = link_to namespace_project_team_index_path(@project.namespace, @project), title: 'Members', class: "team-tab tab" do = nav_link(controller: [:project_members, :teams]) do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: "team-tab tab" do %i.fa.fa-users %span Members diff --git a/config/routes.rb b/config/routes.rb index 547d1aa8660..1b7ae09c773 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -425,7 +425,6 @@ Gitlab::Application.routes.draw do end end - resources :team, controller: 'team_members', only: [:index] resources :milestones, except: [:destroy], constraints: { id: /\d+/ } do member do put :sort_issues diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index bb6c336d7cd..77a90e80d14 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -386,7 +386,7 @@ module SharedPaths end step 'I visit project "Shop" team page' do - visit namespace_project_team_index_path(project.namespace, project) + visit namespace_project_project_members_path(project.namespace, project) end step 'I visit project wiki page' do diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 322697bced8..8d1bfd25223 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -79,8 +79,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_denied_for :visitor } end - describe "GET /:project_path/team" do - subject { namespace_project_team_index_path(project.namespace, project) } + describe "GET /:project_path/project_members" do + subject { namespace_project_project_members_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index ea146c3f0e4..9021ff33186 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -79,8 +79,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for :visitor } end - describe "GET /:project_path/team" do - subject { namespace_project_team_index_path(project.namespace, project) } + describe "GET /:project_path/project_members" do + subject { namespace_project_project_members_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 8ee9199ff29..6ec190ed777 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -84,8 +84,8 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :visitor } end - describe "GET /:project_path/team" do - subject { namespace_project_team_index_path(project.namespace, project) } + describe "GET /:project_path/project_members" do + subject { namespace_project_project_members_path(project.namespace, project) } it { is_expected.to be_allowed_for master } it { is_expected.to be_denied_for reporter } -- GitLab From 224187ffb96283cbf42953a30c116931c03562a2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 16:27:51 +0100 Subject: [PATCH 1346/1609] Move group members index from `/members` to `/group_members`. --- app/assets/javascripts/dispatcher.js.coffee | 2 +- .../groups/application_controller.rb | 18 +++++++++++++++ .../groups/group_members_controller.rb | 23 +++++++++++++++---- app/controllers/groups_controller.rb | 15 +----------- .../_new_group_member.html.haml | 4 ++-- .../index.html.haml} | 4 +++- app/views/layouts/nav/_group.html.haml | 4 ++-- config/routes.rb | 5 ++-- features/steps/explore/groups.rb | 2 +- features/steps/shared/paths.rb | 4 ++-- .../security/group/group_access_spec.rb | 4 ++-- .../group/internal_group_access_spec.rb | 4 ++-- .../security/group/mixed_group_access_spec.rb | 4 ++-- .../group/public_group_access_spec.rb | 4 ++-- 14 files changed, 60 insertions(+), 37 deletions(-) rename app/views/groups/{ => group_members}/_new_group_member.html.haml (71%) rename app/views/groups/{members.html.haml => group_members/index.html.haml} (92%) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 5da774cfb23..4dce6e66ce2 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -73,7 +73,7 @@ class Dispatcher new Activities() shortcut_handler = new ShortcutsNavigation() new ProjectsList() - when 'groups:members' + when 'groups:group_members:index' new GroupMembers() new UsersSelect() when 'groups:new', 'groups:edit', 'admin:groups:edit' diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 7f27f2bb734..a73b8fa212a 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -2,9 +2,27 @@ class Groups::ApplicationController < ApplicationController private + def authorize_read_group! + unless @group and can?(current_user, :read_group, @group) + if current_user.nil? + return authenticate_user! + else + return render_404 + end + end + end + def authorize_admin_group! unless can?(current_user, :manage_group, group) return render_404 end end + + def determine_layout + if current_user + 'group' + else + 'public_group' + end + end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 132452d61c9..d3d6ce1ca2c 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,15 +1,30 @@ class Groups::GroupMembersController < Groups::ApplicationController + skip_before_filter :authenticate_user!, only: [:index] before_filter :group # Authorize - before_filter :authorize_admin_group! + before_filter :authorize_read_group! + before_filter :authorize_admin_group!, except: [:index, :leave] - layout 'group' + layout :determine_layout + + def index + @project = @group.projects.find(params[:project_id]) if params[:project_id] + @members = @group.group_members + + if params[:search].present? + users = @group.users.search(params[:search]).to_a + @members = @members.where(user_id: users) + end + + @members = @members.order('access_level DESC').page(params[:page]).per(50) + @group_member = GroupMember.new + end def create @group.add_users(params[:user_ids].split(','), params[:access_level]) - redirect_to members_group_path(@group), notice: 'Users were successfully added.' + redirect_to group_group_members_path(@group), notice: 'Users were successfully added.' end def update @@ -23,7 +38,7 @@ class Groups::GroupMembersController < Groups::ApplicationController if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner. @group_member.destroy respond_to do |format| - format.html { redirect_to members_group_path(@group), notice: 'User was successfully removed from group.' } + format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.js { render nothing: true } end else diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7e336803fbb..7af3c077182 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,5 +1,5 @@ class GroupsController < Groups::ApplicationController - skip_before_filter :authenticate_user!, only: [:show, :issues, :members, :merge_requests] + skip_before_filter :authenticate_user!, only: [:show, :issues, :merge_requests] respond_to :html before_filter :group, except: [:new, :create] @@ -67,19 +67,6 @@ class GroupsController < Groups::ApplicationController end end - def members - @project = group.projects.find(params[:project_id]) if params[:project_id] - @members = group.group_members - - if params[:search].present? - users = group.users.search(params[:search]).to_a - @members = @members.where(user_id: users) - end - - @members = @members.order('access_level DESC').page(params[:page]).per(50) - @users_group = GroupMember.new - end - def edit end diff --git a/app/views/groups/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml similarity index 71% rename from app/views/groups/_new_group_member.html.haml rename to app/views/groups/group_members/_new_group_member.html.haml index 345c0555a36..c4c29bb2e8d 100644 --- a/app/views/groups/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -1,4 +1,4 @@ -= form_for @users_group, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| += form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| .form-group = f.label :user_ids, "People", class: 'control-label' .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large') @@ -6,7 +6,7 @@ .form-group = f.label :access_level, "Group Access", class: 'control-label' .col-sm-10 - = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @users_group.access_level), class: "project-access-select select2" + = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2" .help-block Read more about role permissions %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" diff --git a/app/views/groups/members.html.haml b/app/views/groups/group_members/index.html.haml similarity index 92% rename from app/views/groups/members.html.haml rename to app/views/groups/group_members/index.html.haml index 688c22e9624..0d501fe7bd3 100644 --- a/app/views/groups/members.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,4 +1,5 @@ - show_roles = should_user_see_group_roles?(current_user, @group) + %h3.page-title Group members - if show_roles @@ -10,7 +11,7 @@ %hr .clearfix.js-toggle-container - = form_tag members_group_path(@group), method: :get, class: 'form-inline member-search-form' do + = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' } = button_tag 'Search', class: 'btn' @@ -33,6 +34,7 @@ %ul.well-list - @members.each do |member| = render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true + = paginate @members, theme: 'gitlab' :coffeescript diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index ddd3df19eec..32fe0e37df8 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -24,8 +24,8 @@ Merge Requests - if current_user %span.count= MergeRequest.opened.of_group(@group).count - = nav_link(path: 'groups#members') do - = link_to members_group_path(@group), title: 'Members' do + = nav_link(controller: [:group_members]) do + = link_to group_group_members_path(@group), title: 'Members' do %i.fa.fa-users %span Members diff --git a/config/routes.rb b/config/routes.rb index 1b7ae09c773..459158dcb61 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -236,12 +236,13 @@ Gitlab::Application.routes.draw do member do get :issues get :merge_requests - get :members get :projects end scope module: :groups do - resources :group_members, only: [:create, :update, :destroy] + resources :group_members, only: [:index, :create, :update, :destroy] do + end + resource :avatar, only: [:destroy] resources :milestones, only: [:index, :show, :update] end diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb index ccbf6cda07e..0c2127d4c4b 100644 --- a/features/steps/explore/groups.rb +++ b/features/steps/explore/groups.rb @@ -35,7 +35,7 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps end step 'I visit group "TestGroup" members page' do - visit members_group_path(Group.find_by(name: "TestGroup")) + visit group_group_members_path(Group.find_by(name: "TestGroup")) end step 'I should not see project "Enterprise" items' do diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 77a90e80d14..e3cf1b92cda 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -32,7 +32,7 @@ module SharedPaths end step 'I visit group "Owned" members page' do - visit members_group_path(Group.find_by(name:"Owned")) + visit group_group_members_path(Group.find_by(name:"Owned")) end step 'I visit group "Owned" settings page' do @@ -52,7 +52,7 @@ module SharedPaths end step 'I visit group "Guest" members page' do - visit members_group_path(Group.find_by(name:"Guest")) + visit group_group_members_path(Group.find_by(name:"Guest")) end step 'I visit group "Guest" settings page' do diff --git a/spec/features/security/group/group_access_spec.rb b/spec/features/security/group/group_access_spec.rb index e0c5cbf4d3d..63793149459 100644 --- a/spec/features/security/group/group_access_spec.rb +++ b/spec/features/security/group/group_access_spec.rb @@ -59,8 +59,8 @@ describe "Group access", feature: true do it { is_expected.to be_denied_for :visitor } end - describe "GET /groups/:path/members" do - subject { members_group_path(group) } + describe "GET /groups/:path/group_members" do + subject { group_group_members_path(group) } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } diff --git a/spec/features/security/group/internal_group_access_spec.rb b/spec/features/security/group/internal_group_access_spec.rb index 5279a1bc13a..d17a7412e43 100644 --- a/spec/features/security/group/internal_group_access_spec.rb +++ b/spec/features/security/group/internal_group_access_spec.rb @@ -55,8 +55,8 @@ describe "Group with internal project access", feature: true do it { is_expected.to be_denied_for :visitor } end - describe "GET /groups/:path/members" do - subject { members_group_path(group) } + describe "GET /groups/:path/group_members" do + subject { group_group_members_path(group) } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } diff --git a/spec/features/security/group/mixed_group_access_spec.rb b/spec/features/security/group/mixed_group_access_spec.rb index efd14858b98..b3db7b5dea4 100644 --- a/spec/features/security/group/mixed_group_access_spec.rb +++ b/spec/features/security/group/mixed_group_access_spec.rb @@ -56,8 +56,8 @@ describe "Group access", feature: true do it { is_expected.to be_allowed_for :visitor } end - describe "GET /groups/:path/members" do - subject { members_group_path(group) } + describe "GET /groups/:path/group_members" do + subject { group_group_members_path(group) } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } diff --git a/spec/features/security/group/public_group_access_spec.rb b/spec/features/security/group/public_group_access_spec.rb index c7e3d0a8a40..c16f0c0d1e1 100644 --- a/spec/features/security/group/public_group_access_spec.rb +++ b/spec/features/security/group/public_group_access_spec.rb @@ -55,8 +55,8 @@ describe "Group with public project access", feature: true do it { is_expected.to be_allowed_for :visitor } end - describe "GET /groups/:path/members" do - subject { members_group_path(group) } + describe "GET /groups/:path/group_members" do + subject { group_group_members_path(group) } it { is_expected.to be_allowed_for owner } it { is_expected.to be_allowed_for master } -- GitLab From 84371de01f3ce7bab334539a93734658528736ec Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 16:28:33 +0100 Subject: [PATCH 1347/1609] Move group leave action from dashboard/groups to groups/group_members. --- app/controllers/dashboard/groups_controller.rb | 18 +----------------- .../groups/group_members_controller.rb | 11 +++++++++++ app/views/dashboard/groups/index.html.haml | 2 +- .../group_members/_group_member.html.haml | 5 +++-- config/routes.rb | 7 ++----- 5 files changed, 18 insertions(+), 25 deletions(-) diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index b827639978c..ed14f4e1f3b 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,21 +1,5 @@ class Dashboard::GroupsController < ApplicationController def index - @user_groups = current_user.group_members.page(params[:page]).per(PER_PAGE) - end - - def leave - @users_group = group.group_members.where(user_id: current_user.id).first - if can?(current_user, :destroy, @users_group) - @users_group.destroy - redirect_to(dashboard_groups_path, info: "You left #{group.name} group.") - else - return render_403 - end - end - - private - - def group - @group ||= Group.find_by(path: params[:id]) + @group_members = current_user.group_members.page(params[:page]).per(PER_PAGE) end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index d3d6ce1ca2c..2df51c97a22 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -46,6 +46,17 @@ class Groups::GroupMembersController < Groups::ApplicationController end end + def leave + @group_member = @group.group_members.where(user_id: current_user.id).first + + if can?(current_user, :destroy_group_member, @group_member) + @group_member.destroy + redirect_to(dashboard_groups_path, info: "You left #{group.name} group.") + else + return render_403 + end + end + protected def group diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 76f7d660f36..165db214d75 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -23,7 +23,7 @@ Settings - if can?(current_user, :destroy_group_member, group_member) - = link_to leave_dashboard_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Remove user from group' do + = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Remove user from group' do %i.fa.fa-sign-out Leave diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 2fc91df0931..3d120c5cdd7 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -1,6 +1,7 @@ - user = member.user - return unless user - show_roles = true if show_roles.nil? + %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %span{class: ("list-item-name" if show_controls)} = image_tag avatar_icon(user.email, 16), class: "avatar s16" @@ -21,8 +22,8 @@ title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o - if can?(current_user, :destroy_group_member, member) - - if current_user == member.user - = link_to leave_dashboard_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + - if current_user == user + = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse - else = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do diff --git a/config/routes.rb b/config/routes.rb index 459158dcb61..dd70ad2fa0d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -215,11 +215,7 @@ Gitlab::Application.routes.draw do scope module: :dashboard do resources :milestones, only: [:index, :show] - resources :groups, only: [:index] do - member do - delete :leave - end - end + resources :groups, only: [:index] resources :projects, only: [] do collection do @@ -241,6 +237,7 @@ Gitlab::Application.routes.draw do scope module: :groups do resources :group_members, only: [:index, :create, :update, :destroy] do + delete :leave, on: :collection end resource :avatar, only: [:destroy] -- GitLab From 5ad35bbe028c60c1281871b45619455a508f7b5b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 16:30:36 +0100 Subject: [PATCH 1348/1609] Use same layout and interactivity for project members as group members. --- app/assets/javascripts/dispatcher.js.coffee | 4 ++- .../javascripts/project_members.js.coffee | 4 +++ app/views/projects/_dropdown.html.haml | 2 +- .../project_members/_group_members.html.haml | 15 ++++++++ .../_new_project_member.html.haml | 15 ++++++++ .../project_members/_project_member.html.haml | 34 ++++++++++++++++++ .../projects/project_members/_team.html.haml | 11 ++++++ .../import.html.haml | 4 +-- .../projects/project_members/index.html.haml | 35 +++++++++++++++++++ .../projects/project_members/update.js.haml | 3 ++ .../projects/team_members/_form.html.haml | 29 --------------- .../team_members/_group_members.html.haml | 14 -------- .../projects/team_members/_team.html.haml | 9 ----- .../team_members/_team_member.html.haml | 18 ---------- .../projects/team_members/index.html.haml | 16 --------- app/views/projects/team_members/new.html.haml | 1 - .../projects/team_members/update.js.haml | 6 ---- features/project/team_management.feature | 2 +- features/steps/project/team_management.rb | 21 ++++++----- 19 files changed, 137 insertions(+), 106 deletions(-) create mode 100644 app/assets/javascripts/project_members.js.coffee create mode 100644 app/views/projects/project_members/_group_members.html.haml create mode 100644 app/views/projects/project_members/_new_project_member.html.haml create mode 100644 app/views/projects/project_members/_project_member.html.haml create mode 100644 app/views/projects/project_members/_team.html.haml rename app/views/projects/{team_members => project_members}/import.html.haml (66%) create mode 100644 app/views/projects/project_members/index.html.haml create mode 100644 app/views/projects/project_members/update.js.haml delete mode 100644 app/views/projects/team_members/_form.html.haml delete mode 100644 app/views/projects/team_members/_group_members.html.haml delete mode 100644 app/views/projects/team_members/_team.html.haml delete mode 100644 app/views/projects/team_members/_team_member.html.haml delete mode 100644 app/views/projects/team_members/index.html.haml delete mode 100644 app/views/projects/team_members/new.html.haml delete mode 100644 app/views/projects/team_members/update.js.haml diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 4dce6e66ce2..edf482f33d8 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -76,6 +76,9 @@ class Dispatcher when 'groups:group_members:index' new GroupMembers() new UsersSelect() + when 'projects:project_members:index' + new ProjectMembers() + new UsersSelect() when 'groups:new', 'groups:edit', 'admin:groups:edit' new GroupAvatar() when 'projects:tree:show' @@ -129,7 +132,6 @@ class Dispatcher shortcut_handler = new ShortcutsNavigation() when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches' shortcut_handler = new ShortcutsNavigation() - new UsersSelect() # If we haven't installed a custom shortcut handler, install the default one diff --git a/app/assets/javascripts/project_members.js.coffee b/app/assets/javascripts/project_members.js.coffee new file mode 100644 index 00000000000..896ba7e53ee --- /dev/null +++ b/app/assets/javascripts/project_members.js.coffee @@ -0,0 +1,4 @@ +class @ProjectMembers + constructor: -> + $('li.project_member').bind 'ajax:success', -> + $(this).fadeOut() diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml index 3cdbbb7b043..f4f4c2662cf 100644 --- a/app/views/projects/_dropdown.html.haml +++ b/app/views/projects/_dropdown.html.haml @@ -17,7 +17,7 @@ New snippet - if can?(current_user, :admin_project_member, @project) %li - = link_to new_namespace_project_team_member_path(@project.namespace, @project), title: "New project member" do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: "New project member" do New project member - if can? current_user, :push_code, @project %li.divider diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml new file mode 100644 index 00000000000..b050be1d21b --- /dev/null +++ b/app/views/projects/project_members/_group_members.html.haml @@ -0,0 +1,15 @@ +.panel.panel-default + .panel-heading + %strong #{@group.name} + group members + %small + (#{members.count}) + .pull-right + = link_to group_group_members_path(@group), class: 'btn btn-sm' do + %i.fa.fa-pencil-square-o + %ul.well-list + - members.each do |member| + = render 'groups/group_members/group_member', member: member, show_controls: false + - if members.count > 20 + %li + and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)} diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml new file mode 100644 index 00000000000..0f824bdabf8 --- /dev/null +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -0,0 +1,15 @@ += form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f| + .form-group + = f.label :user_ids, "People", class: 'control-label' + .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large') + + .form-group + = f.label :access_level, "Project Access", class: 'control-label' + .col-sm-10 + = select_tag :access_level, options_for_select(ProjectMember.access_roles, @project_member.access_level), class: "project-access-select select2" + .help-block + Read more about role permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + + .form-actions + = f.submit 'Add users to project', class: "btn btn-create" diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml new file mode 100644 index 00000000000..57dcc322d89 --- /dev/null +++ b/app/views/projects/project_members/_project_member.html.haml @@ -0,0 +1,34 @@ +- user = member.user +- return unless user + +%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)} + %span.list-item-name + = image_tag avatar_icon(user.email, 16), class: "avatar s16" + %strong= user.name + %span.cgray= user.username + - if user == current_user + %span.label.label-success It's you + - if user.blocked? + %label.label.label-danger + %strong Blocked + + - if current_user_can_admin_project + - unless @project.personal? && user == current_user + .pull-right + %strong= member.human_access + = button_tag class: "btn-xs btn js-toggle-button", + title: 'Edit access level', type: 'button' do + %i.fa.fa-pencil-square-o + + - if current_user == user + = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do + %i.fa.fa-minus.fa-inverse + - else + = link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do + %i.fa.fa-minus.fa-inverse + + .edit-member.hide.js-toggle-content + = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member.user), remote: true do |f| + .alert.prepend-top-20 + = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level) + = f.submit 'Save', class: 'btn btn-save btn-small' diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml new file mode 100644 index 00000000000..615c425e59a --- /dev/null +++ b/app/views/projects/project_members/_team.html.haml @@ -0,0 +1,11 @@ +- can_admin_project = can?(current_user, :admin_project, @project) + +.panel.panel-default.prepend-top-20 + .panel-heading + %strong #{@project.name} + project members + %small + (#{members.count}) + %ul.well-list + - members.each do |project_member| + = render 'project_member', member: project_member, current_user_can_admin_project: can_admin_project diff --git a/app/views/projects/team_members/import.html.haml b/app/views/projects/project_members/import.html.haml similarity index 66% rename from app/views/projects/team_members/import.html.haml rename to app/views/projects/project_members/import.html.haml index 9e31d47117e..293754cd0c0 100644 --- a/app/views/projects/team_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -3,12 +3,12 @@ %p.light Only project members will be imported. Group members will be skipped. %hr -= form_tag apply_import_namespace_project_team_members_path(@project.namespace, @project), method: 'post', class: 'form-horizontal' do += form_tag apply_import_namespace_project_project_members_path(@project.namespace, @project), method: 'post', class: 'form-horizontal' do .form-group = label_tag :source_project_id, "Project", class: 'control-label' .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions = button_tag 'Import project members', class: "btn btn-create" - = link_to "Cancel", namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml new file mode 100644 index 00000000000..36a6f6a1554 --- /dev/null +++ b/app/views/projects/project_members/index.html.haml @@ -0,0 +1,35 @@ +%h3.page-title + Users with access to this project + +%p.light + Read more about project permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + +%hr + +.clearfix.js-toggle-container + = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' } + = button_tag 'Search', class: 'btn' + + - if can?(current_user, :admin_project_member, @project) + %span.pull-right + = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do + Add members + %i.fa.fa-chevron-down + = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do + Import members + + .js-toggle-content.hide.new-group-member-holder + = render "new_project_member" + += render "team", members: @project_members + +- if @group + = render "group_members", members: @group_members + +:coffeescript + $('form.member-search-form').on 'submit', (event) -> + event.preventDefault() + Turbolinks.visit @.action + '?' + $(@).serialize() diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml new file mode 100644 index 00000000000..811b1858821 --- /dev/null +++ b/app/views/projects/project_members/update.js.haml @@ -0,0 +1,3 @@ +- can_admin_project = can?(current_user, :admin_project, @project) +:plain + $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member, current_user_can_admin_project: can_admin_project))}'); diff --git a/app/views/projects/team_members/_form.html.haml b/app/views/projects/team_members/_form.html.haml deleted file mode 100644 index 166b6362a07..00000000000 --- a/app/views/projects/team_members/_form.html.haml +++ /dev/null @@ -1,29 +0,0 @@ -%h3.page-title - New project member(s) - -= form_for @user_project_relation, as: :project_member, url: namespace_project_team_members_path(@project.namespace, @project), html: { class: "form-horizontal users-project-form" } do |f| - -if @user_project_relation.errors.any? - .alert.alert-danger - %ul - - @user_project_relation.errors.full_messages.each do |msg| - %li= msg - - %p 1. Choose people you want in the project - .form-group - = f.label :user_ids, "People", class: 'control-label' - .col-sm-10 - = users_select_tag(:user_ids, multiple: true) - - %p 2. Set access level for them - .form-group - = f.label :access_level, "Project Access", class: 'control-label' - .col-sm-10 - = select_tag :access_level, options_for_select(Gitlab::Access.options, @user_project_relation.access_level), class: "project-access-select select2" - .help-block - Read more about role permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" - - - .form-actions - = f.submit 'Add users', class: "btn btn-create" - = link_to "Cancel", namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/team_members/_group_members.html.haml b/app/views/projects/team_members/_group_members.html.haml deleted file mode 100644 index 12bd828a5e7..00000000000 --- a/app/views/projects/team_members/_group_members.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- group_users_count = @group.group_members.count -.panel.panel-default - .panel-heading - %strong #{@group.name} - group members (#{group_users_count}) - .pull-right - = link_to members_group_path(@group), class: 'btn btn-sm' do - %i.fa.fa-pencil-square-o - %ul.well-list - - @group.group_members.order('access_level DESC').limit(20).each do |member| - = render 'groups/group_members/group_member', member: member, show_controls: false - - if group_users_count > 20 - %li - and #{group_users_count - 20} more. For full list visit #{link_to 'group members page', members_group_path(@group)} diff --git a/app/views/projects/team_members/_team.html.haml b/app/views/projects/team_members/_team.html.haml deleted file mode 100644 index 0e5b8176132..00000000000 --- a/app/views/projects/team_members/_team.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.team-table - - can_admin_project = (can? current_user, :admin_project, @project) - .panel.panel-default - .panel-heading - %strong #{@project.name} - project members (#{members.count}) - %ul.well-list - - members.each do |team_member| - = render 'team_member', member: team_member, current_user_can_admin_project: can_admin_project diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml deleted file mode 100644 index 1a755bbd560..00000000000 --- a/app/views/projects/team_members/_team_member.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- user = member.user -%li{id: dom_id(user), class: "team_member_row access-#{member.human_access.downcase}"} - .pull-right - - if current_user_can_admin_project - - unless @project.personal? && user == current_user - .pull-left - = form_for(member, as: :project_member, url: namespace_project_team_member_path(@project.namespace, @project, member.user)) do |f| - = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: "trigger-submit" -   - = link_to namespace_project_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from team' do - %i.fa.fa-minus.fa-inverse - = image_tag avatar_icon(user.email, 32), class: "avatar s32" - %p - %strong= user.name - - if user.blocked? - %label.label.label-danger - %strong Blocked - %span.cgray= user.username diff --git a/app/views/projects/team_members/index.html.haml b/app/views/projects/team_members/index.html.haml deleted file mode 100644 index fcc879a58df..00000000000 --- a/app/views/projects/team_members/index.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -%h3.page-title - Users with access to this project - - - if can? current_user, :admin_team_member, @project - %span.pull-right - = link_to new_namespace_project_team_member_path(@project.namespace, @project), class: "btn btn-new btn-grouped", title: "New project member" do - New project member - = link_to import_namespace_project_team_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do - Import members - -%p.light - Read more about project permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" -= render "team", members: @project_members -- if @group - = render "group_members" diff --git a/app/views/projects/team_members/new.html.haml b/app/views/projects/team_members/new.html.haml deleted file mode 100644 index b1bc3ba0eba..00000000000 --- a/app/views/projects/team_members/new.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render "form" diff --git a/app/views/projects/team_members/update.js.haml b/app/views/projects/team_members/update.js.haml deleted file mode 100644 index c68fe9574a2..00000000000 --- a/app/views/projects/team_members/update.js.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if @user_project_relation.valid? - :plain - $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#529214"}, 1000);; -- else - :plain - $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#D12F19"}, 1000);; diff --git a/features/project/team_management.feature b/features/project/team_management.feature index 86ea6cd6e91..22393622bb9 100644 --- a/features/project/team_management.feature +++ b/features/project/team_management.feature @@ -13,7 +13,7 @@ Feature: Project Team Management @javascript Scenario: Add user to project - Given I click link "New Team Member" + Given I click link "Add members" And I select "Mike" as "Reporter" Then I should see "Mike" in team list as "Reporter" diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 7907f2a6fe3..304df17d6f1 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -15,18 +15,18 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps page.should have_content(user.username) end - step 'I click link "New Team Member"' do - click_link "New project member" + step 'I click link "Add members"' do + find(:css, 'a.btn-add').click end step 'I select "Mike" as "Reporter"' do user = User.find_by(name: "Mike") - select2(user.id, from: "#user_ids", multiple: true) - within "#new_project_member" do + within ".users-project-form" do + select2(user.id, from: "#user_ids", multiple: true) select "Reporter", from: "access_level" end - click_button "Add users" + click_button "Add users to project" end step 'I should see "Mike" in team list as "Reporter"' do @@ -42,8 +42,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps end step 'I change "Sam" role to "Reporter"' do - user = User.find_by(name: "Sam") - within "#user_#{user.id}" do + project = Project.find_by(name: "Shop") + user = User.find_by(name: 'Sam') + project_member = project.project_members.find_by(user_id: user.id) + within "#project_member_#{project_member.id}" do select "Reporter", from: "project_member_access_level" end end @@ -100,7 +102,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps end step 'I click cancel link for "Sam"' do - within "#user_#{User.find_by(name: 'Sam').id}" do + project = Project.find_by(name: "Shop") + user = User.find_by(name: 'Sam') + project_member = project.project_members.find_by(user_id: user.id) + within "#project_member_#{project_member.id}" do click_link('Remove user from team') end end -- GitLab From f66b77e6317acaaf382f2632325afc8b1413a9c7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 13 Mar 2015 22:20:14 +0100 Subject: [PATCH 1349/1609] Fix failing specs. --- features/steps/project/team_management.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index 304df17d6f1..0eefe2b5688 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -16,7 +16,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps end step 'I click link "Add members"' do - find(:css, 'a.btn-add').click + find(:css, 'button.btn-new').click end step 'I select "Mike" as "Reporter"' do @@ -46,7 +46,9 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps user = User.find_by(name: 'Sam') project_member = project.project_members.find_by(user_id: user.id) within "#project_member_#{project_member.id}" do + click_button "Edit access level" select "Reporter", from: "project_member_access_level" + click_button "Save" end end -- GitLab From e88d06c892eed5c50fb98eeb90d47380f47e6194 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 15 Mar 2015 13:56:32 +0100 Subject: [PATCH 1350/1609] Update button class. --- app/views/projects/project_members/_project_member.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 57dcc322d89..368d9419de3 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -31,4 +31,4 @@ = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member.user), remote: true do |f| .alert.prepend-top-20 = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level) - = f.submit 'Save', class: 'btn btn-save btn-small' + = f.submit 'Save', class: 'btn btn-save btn-sm' -- GitLab From 584580e5156020e639b2a578c332ae92e38b3a5c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 15 Mar 2015 13:56:56 +0100 Subject: [PATCH 1351/1609] Add changelog item. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 7bc561b6e06..dd37ab0c1c7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -68,6 +68,7 @@ v 7.9.0 (unreleased) - Execute hooks and services when branch or tag is created or deleted through web interface. - Block and unblock user if he/she was blocked/unblocked in Active Directory - Raise recommended number of unicorn workers from 2 to 3 + - Use same layout and interactivity for project members as group members. v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers -- GitLab From 3b1d5a1dffa35746d3619b90f3e82f8437e38c91 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 15 Mar 2015 15:42:31 +0100 Subject: [PATCH 1352/1609] Prevent gitlab-shell character encoding issues by receiving its changes as raw data. --- CHANGELOG | 1 + Gemfile | 3 +++ Gemfile.lock | 1 + app/workers/post_receive.rb | 17 ++++++++++++++++- spec/workers/post_receive_spec.rb | 18 +++++++++--------- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7bc561b6e06..6efa5f87439 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -68,6 +68,7 @@ v 7.9.0 (unreleased) - Execute hooks and services when branch or tag is created or deleted through web interface. - Block and unblock user if he/she was blocked/unblocked in Active Directory - Raise recommended number of unicorn workers from 2 to 3 + - Prevent gitlab-shell character encoding issues by receiving its changes as raw data. v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/Gemfile b/Gemfile index 44f024a4b8e..b7b7b0a1c28 100644 --- a/Gemfile +++ b/Gemfile @@ -177,6 +177,9 @@ gem 'ace-rails-ap' # Keyboard shortcuts gem 'mousetrap-rails' +# Detect and convert string character encoding +gem 'charlock_holmes' + # Shutting down requests that take too long gem "slowpoke" diff --git a/Gemfile.lock b/Gemfile.lock index b331c29f7ef..d3209ede86e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -684,6 +684,7 @@ DEPENDENCIES cal-heatmap-rails (~> 0.0.1) capybara (~> 2.2.1) carrierwave + charlock_holmes coffee-rails colored coveralls diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index ecc6c8e53a3..0c3ee6ba4ff 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -21,7 +21,9 @@ class PostReceive return false end - changes = changes.lines if changes.kind_of?(String) + changes = Base64.decode64(changes) unless changes.include?(" ") + changes = utf8_encode_changes(changes) + changes = changes.lines changes.each do |change| oldrev, newrev, ref = change.strip.split(' ') @@ -41,6 +43,19 @@ class PostReceive end end + def utf8_encode_changes(changes) + changes = changes.dup + + changes.force_encoding("UTF-8") + return changes if changes.valid_encoding? + + # Convert non-UTF-8 branch/tag names to UTF-8 so they can be dumped as JSON. + detection = CharlockHolmes::EncodingDetector.detect(changes) + return changes unless detection && detection[:encoding] + + CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8') + end + def log(message) Gitlab::GitLogger.error("POST-RECEIVE: #{message}") end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 8eabc46112b..df1a2b84a53 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -1,6 +1,10 @@ require 'spec_helper' describe PostReceive do + let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" } + let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") } + let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) } + context "as a resque worker" do it "reponds to #perform" do expect(PostReceive.new).to respond_to(:perform) @@ -14,7 +18,7 @@ describe PostReceive do it "fetches the correct project" do expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) - PostReceive.new.perform(pwd(project), key_id, changes) + PostReceive.new.perform(pwd(project), key_id, base64_changes) end it "does not run if the author is not in the project" do @@ -22,24 +26,20 @@ describe PostReceive do expect(project).not_to receive(:execute_hooks) - expect(PostReceive.new.perform(pwd(project), key_id, changes)).to be_falsey + expect(PostReceive.new.perform(pwd(project), key_id, base64_changes)).to be_falsey end it "asks the project to trigger all hooks" do Project.stub(find_with_namespace: project) - expect(project).to receive(:execute_hooks) - expect(project).to receive(:execute_services) + expect(project).to receive(:execute_hooks).twice + expect(project).to receive(:execute_services).twice expect(project).to receive(:update_merge_requests) - PostReceive.new.perform(pwd(project), key_id, changes) + PostReceive.new.perform(pwd(project), key_id, base64_changes) end end def pwd(project) File.join(Gitlab.config.gitlab_shell.repos_path, project.path_with_namespace) end - - def changes - 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master' - end end -- GitLab From 9698b36c1cd0808adb006593c0e8649cb42f3571 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Sun, 15 Mar 2015 18:17:12 +0200 Subject: [PATCH 1353/1609] Subscription --- app/assets/javascripts/subscription.js.coffee | 18 ++++++++++++++++++ app/controllers/projects/issues_controller.rb | 11 ++++++++++- .../projects/merge_requests_controller.rb | 11 ++++++++++- app/models/concerns/issuable.rb | 10 ++++++++++ app/models/subscribe.rb | 3 +++ app/services/notification_service.rb | 18 ++++++++++++++++++ .../projects/issues/_issue_context.html.haml | 16 ++++++++++++++++ .../merge_requests/show/_context.html.haml | 16 ++++++++++++++++ config/routes.rb | 4 ++++ .../20150313012111_create_subscribes_table.rb | 12 ++++++++++++ db/schema.rb | 11 ++++++++++- 11 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/subscription.js.coffee create mode 100644 app/models/subscribe.rb create mode 100644 db/migrate/20150313012111_create_subscribes_table.rb diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee new file mode 100644 index 00000000000..f457622fc3a --- /dev/null +++ b/app/assets/javascripts/subscription.js.coffee @@ -0,0 +1,18 @@ +class @Subscription + constructor: (url) -> + $(".subscribe-button").click (event)=> + self = @ + btn = $(event.currentTarget) + action = btn.prop("value") + current_status = $(".sub_status").text().trim() + $(".fa-spinner.subscription").removeClass("hidden") + $(".sub_status").empty() + + $.post url, subscription: action, => + $(".fa-spinner.subscription").addClass("hidden") + status = if current_status == "subscribed" then "unsubscribed" else "subscribed" + $(".sub_status").text(status) + action = if status == "subscribed" then "Unsubscribe" else "Subscribe" + btn.prop("value", action) + + diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4266bcaef16..4eb5092b9d9 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,6 +1,6 @@ class Projects::IssuesController < Projects::ApplicationController before_filter :module_enabled - before_filter :issue, only: [:edit, :update, :show] + before_filter :issue, only: [:edit, :update, :show, :set_subscription] # Allow read any issue before_filter :authorize_read_issue! @@ -97,6 +97,15 @@ class Projects::IssuesController < Projects::ApplicationController redirect_to :back, notice: "#{result[:count]} issues updated" end + def set_subscription + subscribed = params[:subscription] == "Subscribe" + + sub = @issue.subscribes.find_or_create_by(user_id: current_user.id) + sub.update(subscribed: subscribed) + + render nothing: true + end + protected def issue diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 93d79d81661..5613eee35c0 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -2,7 +2,7 @@ require 'gitlab/satellite/satellite' class Projects::MergeRequestsController < Projects::ApplicationController before_filter :module_enabled - before_filter :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status] + before_filter :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status, :set_subscription] before_filter :closes_issues, only: [:edit, :update, :show, :diffs] before_filter :validates_merge_request, only: [:show, :diffs] before_filter :define_show_vars, only: [:show, :diffs] @@ -174,6 +174,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end + def set_subscription + subscribed = params[:subscription] == "Subscribe" + + sub = @merge_request.subscribes.find_or_create_by(user_id: current_user.id) + sub.update(subscribed: subscribed) + + render nothing: true + end + protected def selected_target_project diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index f5e23e9dc2d..e89dcbf9acb 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -15,6 +15,7 @@ module Issuable has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links + has_many :subscribes, dependent: :destroy validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -132,6 +133,15 @@ module Issuable users.concat(mentions.reduce([], :|)).uniq end + def subscribe_status(user) + sub = subscribes.find_by_user_id(user.id) + if sub + return sub.subscribed + end + + participants.include?(user) + end + def to_hook_data(user) { object_kind: self.class.name.underscore, diff --git a/app/models/subscribe.rb b/app/models/subscribe.rb new file mode 100644 index 00000000000..a68546667f1 --- /dev/null +++ b/app/models/subscribe.rb @@ -0,0 +1,3 @@ +class Subscribe < ActiveRecord::Base + belongs_to :user +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 0063b7ce40c..4fa775a28ce 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -314,6 +314,13 @@ class NotificationService end end + def reject_unsubscribed_users(recipients, target) + recipients.reject do |user| + subscribe = target.subscribes.find_by_user_id(user.id) + subscribe && !subscribe.subscribed + end + end + def new_resource_email(target, project, method) recipients = build_recipients(target, project) recipients.delete(target.author) @@ -361,10 +368,21 @@ class NotificationService recipients = reject_muted_users(recipients, project) recipients = reject_mention_users(recipients, project) + recipients = add_subscribers(recipients, project) recipients = recipients.concat(project_watchers(project)).uniq + recipients = reject_unsubscribed_users(recipients, target) recipients end + def add_subscribers(recipients, target) + subs = target.subscribes + if subs.any? + recipients.merge(subs.where("subscribed is true").map(&:user)) + else + recipients + end + end + def mailer Notify.delay end diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 4c7654354f4..09c531ac7fc 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -26,3 +26,19 @@ = f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) = hidden_field_tag :issue_context = f.submit class: 'btn' + + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Subscription: + %i.fa.fa-spinner.fa-spin.hidden.subscription + %span.sub_status + = @issue.subscribe_status(current_user) ? "subscribed" : "unsubscribed" + - subscribe_action = @issue.subscribe_status(current_user) ? "Unsubscribe" : "Subscribe" + %input.btn.subscribe-button{:type => "button", :value => subscribe_action} + +:coffeescript + $ -> + new Subscription("#{set_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") + + \ No newline at end of file diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index a74f3fb24e7..aae0aa24ed7 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -28,3 +28,19 @@ = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'}) = hidden_field_tag :merge_request_context = f.submit class: 'btn' + + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Subscription: + %i.fa.fa-spinner.fa-spin.hidden.subscription + %span.sub_status + = @merge_request.subscribe_status(current_user) ? "subscribed" : "unsubscribed" + - subscribe_action = @merge_request.subscribe_status(current_user) ? "Unsubscribe" : "Subscribe" + %input.btn.subscribe-button{:type => "button", :value => subscribe_action} + +:coffeescript + $ -> + new Subscription("#{set_subscription_namespace_project_issue_path(@merge_request.project.namespace, @project, @merge_request)}") + + \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 889995e92a6..a976ba9d593 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -406,6 +406,7 @@ Gitlab::Application.routes.draw do post :automerge get :automerge_check get :ci_status + post :set_subscription end collection do @@ -440,6 +441,9 @@ Gitlab::Application.routes.draw do end resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do + member do + post :set_subscription + end collection do post :bulk_update end diff --git a/db/migrate/20150313012111_create_subscribes_table.rb b/db/migrate/20150313012111_create_subscribes_table.rb new file mode 100644 index 00000000000..706cf77118c --- /dev/null +++ b/db/migrate/20150313012111_create_subscribes_table.rb @@ -0,0 +1,12 @@ +class CreateSubscribesTable < ActiveRecord::Migration + def change + create_table :subscribes do |t| + t.integer :user_id + t.integer :merge_request_id + t.integer :issue_id + t.boolean :subscribed + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 3afbc082b70..6afb79069e4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150306023112) do +ActiveRecord::Schema.define(version: 20150313012111) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -397,6 +397,15 @@ ActiveRecord::Schema.define(version: 20150306023112) do add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree + create_table "subscribes", force: true do |t| + t.integer "user_id" + t.integer "merge_request_id" + t.integer "issue_id" + t.boolean "subscribed" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "taggings", force: true do |t| t.integer "tag_id" t.integer "taggable_id" -- GitLab From 8587a2937020eca2fda3efbcf31862697e7f5b3f Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 15 Mar 2015 12:54:36 -0600 Subject: [PATCH 1354/1609] Change permissions on backup files Use more restrictive permissions for backup tar files and for the db, uploads, and repositories directories inside the tar files. --- CHANGELOG | 1 + lib/backup/manager.rb | 4 +++ spec/tasks/gitlab/backup_rake_spec.rb | 50 +++++++++++++++++++++++---- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7bc561b6e06..6eddab758ec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ v 7.9.0 (unreleased) - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options + - Restrict permissions on backup files - Add grouped milestones from all projects to dashboard. - Web hook sends pusher email as well as commiter - Add Bitbucket omniauth provider. diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index ab8db4e9837..b499e5755bd 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -17,14 +17,18 @@ module Backup file << s.to_yaml.gsub(/^---\n/,'') end + FileUtils.chmod_R(0700, %w{db uploads repositories}) + # create archive $progress.print "Creating backup archive: #{tar_file} ... " + orig_umask = File.umask(0077) if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) $progress.puts "done".green else puts "creating archive #{tar_file} failed".red abort 'Backup failed' end + File.umask(orig_umask) upload(tar_file) end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 60942cc95fc..e6763be7b8f 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -10,17 +10,17 @@ describe 'gitlab:app namespace rake task' do Rake::Task.define_task :environment end + def run_rake_task(task_name) + Rake::Task[task_name].reenable + Rake.application.invoke_task task_name + end + describe 'backup_restore' do before do # avoid writing task output to spec progress allow($stdout).to receive :write end - let :run_rake_task do - Rake::Task["gitlab:backup:restore"].reenable - Rake.application.invoke_task "gitlab:backup:restore" - end - context 'gitlab version' do before do Dir.stub glob: [] @@ -36,7 +36,9 @@ describe 'gitlab:app namespace rake task' do it 'should fail on mismatch' do YAML.stub load_file: {gitlab_version: "not #{gitlab_version}" } - expect { run_rake_task }.to raise_error SystemExit + expect { run_rake_task('gitlab:backup:restore') }.to( + raise_error SystemExit + ) end it 'should invoke restoration on mach' do @@ -44,9 +46,43 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke expect(Rake::Task["gitlab:backup:repo:restore"]).to receive :invoke expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke - expect { run_rake_task }.to_not raise_error + expect { run_rake_task('gitlab:backup:restore') }.to_not raise_error end end end # backup_restore task + + describe 'backup_create' do + def tars_glob + Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) + end + + before :all do + FileUtils.rm(tars_glob) + orig_stdout = $stdout + $stdout = StringIO.new + run_rake_task('gitlab:backup:create') + $stdout = orig_stdout + + @backup_tar = tars_glob.first + end + + before do + backup_path = File.join(Gitlab.config.backup.path, 'test') + allow(Gitlab.config.backup).to receive(:path).and_return(backup_path) + end + + it 'should set correct permissions on the tar file' do + expect(File.exist?(@backup_tar)).to be_truthy + expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600') + end + + it 'should set correct permissions on the tar contents' do + tar_contents, exit_status = Gitlab::Popen.popen( + %W{tar -tvf #{@backup_tar} db uploads repositories} + ) + expect(exit_status).to eq(0) + expect(tar_contents).not_to match(/^.{4,9}[rwx]/) + end + end # backup_create task end # gitlab:app namespace -- GitLab From a2fb3711f07eba7fc33b798cee1827c95b204ca8 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Sun, 15 Mar 2015 18:06:25 -0700 Subject: [PATCH 1355/1609] Replace linux with GNU/Linux to recognize the work of Dr. Stallman. --- doc/development/architecture.md | 2 +- doc/ssh/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 714cc016004..541af487bb1 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -54,7 +54,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell ![GitLab Diagram Overview](gitlab_diagram_overview.png) -A typical install of GitLab will be on Ubuntu Linux or RHEL/CentOS. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs. +A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs. The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository. `/home/git/gitlab-satellites` keeps checked out repositories when performing actions such as a merge request, editing files in the web interface, etc. diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 6fe23dfa2a6..66941521c2e 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -45,7 +45,7 @@ clip < ~/.ssh/id_rsa.pub pbcopy < ~/.ssh/id_rsa.pub ``` -**Linux (requires xclip):** +**GNU/Linux (requires xclip):** ```bash xclip -sel clip < ~/.ssh/id_rsa.pub ``` -- GitLab From bacb05c554dfa8781c53c1db7a4f9a706e33fd50 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 15 Mar 2015 23:50:55 -0700 Subject: [PATCH 1356/1609] Small improvements to group/project member rows --- app/views/groups/group_members/_group_member.html.haml | 1 + app/views/projects/project_members/_group_members.html.haml | 3 ++- app/views/projects/project_members/_project_member.html.haml | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 3d120c5cdd7..003025221b2 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -22,6 +22,7 @@ title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o - if can?(current_user, :destroy_group_member, member) +   - if current_user == user = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do %i.fa.fa-minus.fa-inverse diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index b050be1d21b..43e92437cf5 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -4,9 +4,10 @@ group members %small (#{members.count}) - .pull-right + .panel-head-actions = link_to group_group_members_path(@group), class: 'btn btn-sm' do %i.fa.fa-pencil-square-o + Edit group members %ul.well-list - members.each do |member| = render 'groups/group_members/group_member', member: member, show_controls: false diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 368d9419de3..1f31d84dd1d 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -20,13 +20,14 @@ title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o +   - if current_user == user = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do %i.fa.fa-minus.fa-inverse - else = link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do %i.fa.fa-minus.fa-inverse - + .edit-member.hide.js-toggle-content = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member.user), remote: true do |f| .alert.prepend-top-20 -- GitLab From 09ef69b7c8e534cedff2ca1a42b58f534e349107 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 16 Mar 2015 13:52:45 +0200 Subject: [PATCH 1357/1609] code folding fix --- app/models/concerns/issuable.rb | 6 +++--- app/models/subscribe.rb | 3 +++ app/services/notification_service.rb | 10 +++++----- db/migrate/20150313012111_create_subscribes_table.rb | 4 ++++ db/schema.rb | 4 ++++ 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index e89dcbf9acb..c74d9cb9917 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -134,9 +134,9 @@ module Issuable end def subscribe_status(user) - sub = subscribes.find_by_user_id(user.id) - if sub - return sub.subscribed + subscribe = subscribes.find_by_user_id(user.id) + if subscribe + return subscribe.subscribed end participants.include?(user) diff --git a/app/models/subscribe.rb b/app/models/subscribe.rb index a68546667f1..be8b9e7605d 100644 --- a/app/models/subscribe.rb +++ b/app/models/subscribe.rb @@ -1,3 +1,6 @@ class Subscribe < ActiveRecord::Base belongs_to :user + + validates :issue_id, uniqueness: { scope: :user_id, allow_nil: true } + validates :merge_request_id, uniqueness: { scope: :user_id, allow_nil: true } end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 4fa775a28ce..edfb62a4b18 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -368,16 +368,16 @@ class NotificationService recipients = reject_muted_users(recipients, project) recipients = reject_mention_users(recipients, project) - recipients = add_subscribers(recipients, project) + recipients = add_subscribed_users(recipients, project) recipients = recipients.concat(project_watchers(project)).uniq recipients = reject_unsubscribed_users(recipients, target) recipients end - def add_subscribers(recipients, target) - subs = target.subscribes - if subs.any? - recipients.merge(subs.where("subscribed is true").map(&:user)) + def add_subscribed_users(recipients, target) + subscribes = target.subscribes + if subscribes.any? + recipients.merge(subscribes.where("subscribed is true").map(&:user)) else recipients end diff --git a/db/migrate/20150313012111_create_subscribes_table.rb b/db/migrate/20150313012111_create_subscribes_table.rb index 706cf77118c..ab0e9a2a5b5 100644 --- a/db/migrate/20150313012111_create_subscribes_table.rb +++ b/db/migrate/20150313012111_create_subscribes_table.rb @@ -8,5 +8,9 @@ class CreateSubscribesTable < ActiveRecord::Migration t.timestamps end + + add_index :subscribes, :user_id + add_index :subscribes, :issue_id + add_index :subscribes, :merge_request_id end end diff --git a/db/schema.rb b/db/schema.rb index 6afb79069e4..46663ad495b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -406,6 +406,10 @@ ActiveRecord::Schema.define(version: 20150313012111) do t.datetime "updated_at" end + add_index "subscribes", ["issue_id"], name: "index_subscribes_on_issue_id", using: :btree + add_index "subscribes", ["merge_request_id"], name: "index_subscribes_on_merge_request_id", using: :btree + add_index "subscribes", ["user_id"], name: "index_subscribes_on_user_id", using: :btree + create_table "taggings", force: true do |t| t.integer "tag_id" t.integer "taggable_id" -- GitLab From 0e20dc910f25db3b3f71867d54367db36334ff45 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 16 Mar 2015 13:58:34 +0200 Subject: [PATCH 1358/1609] update changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 97376c85ece..6c5ba28f0b0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,6 +61,7 @@ v 7.9.0 (unreleased) - Allow smb:// links in Markdown text. - Filter merge request by title or description at Merge Requests page - Block user if he/she was blocked in Active Directory + - Ability to unsubscribe/subscribe to issue or merge request v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers -- GitLab From 410d25c8ca8afabb25e5f89b36e3cfd09ffe6f87 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 16 Mar 2015 15:22:50 +0200 Subject: [PATCH 1359/1609] rename table subscribe; make it polymorfic --- app/controllers/projects/issues_controller.rb | 2 +- .../projects/merge_requests_controller.rb | 2 +- app/models/concerns/issuable.rb | 11 ++++++----- app/models/subscribe.rb | 6 ------ app/models/subscription.rb | 7 +++++++ app/services/notification_service.rb | 10 +++++----- .../projects/issues/_issue_context.html.haml | 4 ++-- .../merge_requests/show/_context.html.haml | 4 ++-- .../20150313012111_create_subscribes_table.rb | 16 ---------------- .../20150313012111_create_subscriptions_table.rb | 13 +++++++++++++ db/schema.rb | 10 ++++------ 11 files changed, 41 insertions(+), 44 deletions(-) delete mode 100644 app/models/subscribe.rb create mode 100644 app/models/subscription.rb delete mode 100644 db/migrate/20150313012111_create_subscribes_table.rb create mode 100644 db/migrate/20150313012111_create_subscriptions_table.rb diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4eb5092b9d9..903b7a68dc9 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -100,7 +100,7 @@ class Projects::IssuesController < Projects::ApplicationController def set_subscription subscribed = params[:subscription] == "Subscribe" - sub = @issue.subscribes.find_or_create_by(user_id: current_user.id) + sub = @issue.subscriptions.find_or_create_by(user_id: current_user.id) sub.update(subscribed: subscribed) render nothing: true diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 5613eee35c0..51ac61c3273 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -177,7 +177,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def set_subscription subscribed = params[:subscription] == "Subscribe" - sub = @merge_request.subscribes.find_or_create_by(user_id: current_user.id) + sub = @merge_request.subscriptions.find_or_create_by(user_id: current_user.id) sub.update(subscribed: subscribed) render nothing: true diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index c74d9cb9917..d1a35ca5294 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -15,7 +15,7 @@ module Issuable has_many :notes, as: :noteable, dependent: :destroy has_many :label_links, as: :target, dependent: :destroy has_many :labels, through: :label_links - has_many :subscribes, dependent: :destroy + has_many :subscriptions, dependent: :destroy, as: :subscribable validates :author, presence: true validates :title, presence: true, length: { within: 0..255 } @@ -133,10 +133,11 @@ module Issuable users.concat(mentions.reduce([], :|)).uniq end - def subscribe_status(user) - subscribe = subscribes.find_by_user_id(user.id) - if subscribe - return subscribe.subscribed + def subscription_status(user) + subscription = subscriptions.find_by_user_id(user.id) + + if subscription + return subscription.subscribed end participants.include?(user) diff --git a/app/models/subscribe.rb b/app/models/subscribe.rb deleted file mode 100644 index be8b9e7605d..00000000000 --- a/app/models/subscribe.rb +++ /dev/null @@ -1,6 +0,0 @@ -class Subscribe < ActiveRecord::Base - belongs_to :user - - validates :issue_id, uniqueness: { scope: :user_id, allow_nil: true } - validates :merge_request_id, uniqueness: { scope: :user_id, allow_nil: true } -end diff --git a/app/models/subscription.rb b/app/models/subscription.rb new file mode 100644 index 00000000000..7e57a8570ec --- /dev/null +++ b/app/models/subscription.rb @@ -0,0 +1,7 @@ +class Subscription < ActiveRecord::Base + belongs_to :subscribable, polymorphic: true + + validates :user_id, + uniqueness: { scope: [:subscribable_id, :subscribable_type]}, + presence: true +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index edfb62a4b18..e02418b7246 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -316,8 +316,8 @@ class NotificationService def reject_unsubscribed_users(recipients, target) recipients.reject do |user| - subscribe = target.subscribes.find_by_user_id(user.id) - subscribe && !subscribe.subscribed + subscription = target.subscriptions.find_by_user_id(user.id) + subscription && !subscription.subscribed end end @@ -375,9 +375,9 @@ class NotificationService end def add_subscribed_users(recipients, target) - subscribes = target.subscribes - if subscribes.any? - recipients.merge(subscribes.where("subscribed is true").map(&:user)) + subscriptions = target.subscriptions + if subscriptions.any? + recipients.merge(subscriptions.where("subscribed is true").map(&:user)) else recipients end diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 09c531ac7fc..24bfbdd4c58 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -33,8 +33,8 @@ Subscription: %i.fa.fa-spinner.fa-spin.hidden.subscription %span.sub_status - = @issue.subscribe_status(current_user) ? "subscribed" : "unsubscribed" - - subscribe_action = @issue.subscribe_status(current_user) ? "Unsubscribe" : "Subscribe" + = @issue.subscription_status(current_user) ? "subscribed" : "unsubscribed" + - subscribe_action = @issue.subscription_status(current_user) ? "Unsubscribe" : "Subscribe" %input.btn.subscribe-button{:type => "button", :value => subscribe_action} :coffeescript diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index aae0aa24ed7..d0c00c1aeaf 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -35,8 +35,8 @@ Subscription: %i.fa.fa-spinner.fa-spin.hidden.subscription %span.sub_status - = @merge_request.subscribe_status(current_user) ? "subscribed" : "unsubscribed" - - subscribe_action = @merge_request.subscribe_status(current_user) ? "Unsubscribe" : "Subscribe" + = @merge_request.subscription_status(current_user) ? "subscribed" : "unsubscribed" + - subscribe_action = @merge_request.subscription_status(current_user) ? "Unsubscribe" : "Subscribe" %input.btn.subscribe-button{:type => "button", :value => subscribe_action} :coffeescript diff --git a/db/migrate/20150313012111_create_subscribes_table.rb b/db/migrate/20150313012111_create_subscribes_table.rb deleted file mode 100644 index ab0e9a2a5b5..00000000000 --- a/db/migrate/20150313012111_create_subscribes_table.rb +++ /dev/null @@ -1,16 +0,0 @@ -class CreateSubscribesTable < ActiveRecord::Migration - def change - create_table :subscribes do |t| - t.integer :user_id - t.integer :merge_request_id - t.integer :issue_id - t.boolean :subscribed - - t.timestamps - end - - add_index :subscribes, :user_id - add_index :subscribes, :issue_id - add_index :subscribes, :merge_request_id - end -end diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb new file mode 100644 index 00000000000..78f7aeeaf7c --- /dev/null +++ b/db/migrate/20150313012111_create_subscriptions_table.rb @@ -0,0 +1,13 @@ +class CreateSubscriptionsTable < ActiveRecord::Migration + def change + create_table :subscriptions do |t| + t.integer :user_id + t.references :subscribable, polymorphic: true + t.boolean :subscribed + + t.timestamps + end + + add_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id], unique: true, name: 'subscriptions_user_id_and_ref_fields' + end +end diff --git a/db/schema.rb b/db/schema.rb index 46663ad495b..3f808d5ac3f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -397,18 +397,16 @@ ActiveRecord::Schema.define(version: 20150313012111) do add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree - create_table "subscribes", force: true do |t| + create_table "subscriptions", force: true do |t| t.integer "user_id" - t.integer "merge_request_id" - t.integer "issue_id" + t.integer "subscribable_id" + t.string "subscribable_type" t.boolean "subscribed" t.datetime "created_at" t.datetime "updated_at" end - add_index "subscribes", ["issue_id"], name: "index_subscribes_on_issue_id", using: :btree - add_index "subscribes", ["merge_request_id"], name: "index_subscribes_on_merge_request_id", using: :btree - add_index "subscribes", ["user_id"], name: "index_subscribes_on_user_id", using: :btree + add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree create_table "taggings", force: true do |t| t.integer "tag_id" -- GitLab From f53683e67fa0db7b13d0dee977bc21206af7e0fd Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 16 Mar 2015 15:35:48 +0200 Subject: [PATCH 1360/1609] fix specs --- app/models/subscription.rb | 3 ++- app/services/notification_service.rb | 29 ++++++++++++++++++---------- db/schema.rb | 2 +- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 7e57a8570ec..276cf0e9465 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,7 +1,8 @@ class Subscription < ActiveRecord::Base + belongs_to :user belongs_to :subscribable, polymorphic: true validates :user_id, - uniqueness: { scope: [:subscribable_id, :subscribable_type]}, + uniqueness: { scope: [:subscribable_id, :subscribable_type] }, presence: true end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index e02418b7246..5ebde8fea84 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -151,6 +151,10 @@ class NotificationService # Reject mutes users recipients = reject_muted_users(recipients, note.project) + recipients = add_subscribed_users(recipients, note.noteable) + + recipients = reject_unsubscribed_users(recipients, note.noteable) + # Reject author recipients.delete(note.author) @@ -315,12 +319,26 @@ class NotificationService end def reject_unsubscribed_users(recipients, target) + return recipients unless target.respond_to? :subscriptions + recipients.reject do |user| subscription = target.subscriptions.find_by_user_id(user.id) subscription && !subscription.subscribed end end + def add_subscribed_users(recipients, target) + return recipients unless target.respond_to? :subscriptions + + subscriptions = target.subscriptions + + if subscriptions.any? + recipients + subscriptions.where("subscribed is true").map(&:user) + else + recipients + end + end + def new_resource_email(target, project, method) recipients = build_recipients(target, project) recipients.delete(target.author) @@ -368,21 +386,12 @@ class NotificationService recipients = reject_muted_users(recipients, project) recipients = reject_mention_users(recipients, project) - recipients = add_subscribed_users(recipients, project) + recipients = add_subscribed_users(recipients, target) recipients = recipients.concat(project_watchers(project)).uniq recipients = reject_unsubscribed_users(recipients, target) recipients end - def add_subscribed_users(recipients, target) - subscriptions = target.subscriptions - if subscriptions.any? - recipients.merge(subscriptions.where("subscribed is true").map(&:user)) - else - recipients - end - end - def mailer Notify.delay end diff --git a/db/schema.rb b/db/schema.rb index 3f808d5ac3f..ebbeb2beab0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -334,12 +334,12 @@ ActiveRecord::Schema.define(version: 20150313012111) do t.string "import_url" t.integer "visibility_level", default: 0, null: false t.boolean "archived", default: false, null: false + t.string "avatar" t.string "import_status" t.float "repository_size", default: 0.0 t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.string "avatar" end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree -- GitLab From 67f55d9b25b079f2123284b73a525013286ea588 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 12 Mar 2015 11:30:46 +0100 Subject: [PATCH 1361/1609] Let the server fix unconfigured git --- CHANGELOG | 1 + lib/tasks/gitlab/check.rake | 25 +++++++++++++++---------- lib/tasks/gitlab/task_helpers.rake | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dd37ab0c1c7..ad757cd28fa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) - Fix broken email images (Hannes Rosenögger) + - Automaticly config git if user forgot, where possible - Fix mass SQL statements on initial push (Hannes Rosenögger) - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) - Add comment notification events to HipChat and Slack services (Stan Hu) diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 976c4b5f22f..d791b7155f9 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -329,16 +329,20 @@ namespace :gitlab do if correct_options.all? puts "yes".green else - puts "no".red - try_fixing_it( - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.name \"#{options["user.name"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.email \"#{options["user.email"]}\""), - sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") - ) - for_more_information( - see_installation_guide_section "GitLab" - ) - fix_and_rerun + print "Trying to fix Git error automatically. ..." + if auto_fix_git_config(options) + puts "Success".green + else + puts "Failed".red + try_fixing_it( + sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.name \"#{options["user.name"]}\""), + sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.email \"#{options["user.email"]}\""), + sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"") + ) + for_more_information( + see_installation_guide_section "GitLab" + ) + end end end end @@ -806,3 +810,4 @@ namespace :gitlab do end end end + diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index da61c6e007f..14a130be2ca 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -112,4 +112,20 @@ namespace :gitlab do @warned_user_not_gitlab = true end end + + # Tries to configure git itself + # + # Returns true if all subcommands were successfull (according to their exit code) + # Returns false if any or all subcommands failed. + def auto_fix_git_config(options) + if !@warned_user_not_gitlab && options['user.email'] != 'example@example.com' # default email should be overridden? + command_success = options.map do |name, value| + system(%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value})) + end + + command_success.all? + else + false + end + end end -- GitLab From 055457360c6316e9a10dfc8ee18ef8c87655d8ad Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 16 Mar 2015 08:09:31 -0700 Subject: [PATCH 1362/1609] Add HipChat integration documentation as this was a source of confusion --- CHANGELOG | 1 + doc/project_services/hipchat.md | 54 ++++++++++++++++++++++++ doc/project_services/project_services.md | 2 +- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 doc/project_services/hipchat.md diff --git a/CHANGELOG b/CHANGELOG index dd37ab0c1c7..e04f24d18e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.9.0 (unreleased) + - Add HipChat integration documentation (Stan Hu) - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) - Fix broken email images (Hannes Rosenögger) - Fix mass SQL statements on initial push (Hannes Rosenögger) diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md new file mode 100644 index 00000000000..021a93a288f --- /dev/null +++ b/doc/project_services/hipchat.md @@ -0,0 +1,54 @@ +# Atlassian HipChat + +GitLab provides a way to send HipChat notifications upon a number of events, +such as when a user pushes code, creates a branch or tag, adds a comment, and +creates a merge request. + +## Setup + +GitLab requires the use of a HipChat v2 API token to work. v1 tokens are +not supported at this time. Note the differences between v1 and v2 tokens: + +HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1 +token is allowed to send messages to *any* room. + +HipChat v2 API has tokens that are can be created using the Integrations tab +in the Group or Room admin page. By design, these are lightweight tokens that +allow GitLab to send messages only to *one* room. + +### Complete these steps in HipChat: + +1. Go to: https://admin.hipchat.com/admin +1. Click on "Group Admin" -> "Integrations". +1. Find "Build Your Own!" and click "Create". +1. Select the desired room, name the integration "GitLab", and click "Create". +1. In the "Send messages to this room by posting this URL" column, you should +see a URL in the format: + +``` + https://api.hipchat.com/v2/room//notification?auth_token= +``` + +HipChat is now ready to accept messages from GitLab. Next, set up the HipChat +service in GitLab. + +### Complete these steps in GitLab: + +1. Navigate to the project you want to configure for notifications. +1. Select "Settings" in the top navigation. +1. Select "Services" in the left navigation. +1. Click "HipChat". +1. Select the "Active" checkbox. +1. Insert the `token` field from the URL into the `Token` field on the Web page. +1. Insert the `room` field from the URL into the `Room` field on the Web page. +1. Save or optionally click "Test Settings". + +## Troubleshooting + +If you do not see notifications, make sure you are using a HipChat v2 API +token, not a v1 token. + +Note that the v2 token is tied to a specific room. If you want to be able to +specify arbitrary rooms, you can create an API token for a specific user in +HipChat under "Account settings" and "API access". Use the `XXX` value under +`auth_token=XXX`. diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 86eda341d6c..8510fcf0319 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -12,7 +12,7 @@ __Project integrations with external services for continuous integration and mor - Flowdock - Gemnasium - GitLab CI -- HipChat +- [HipChat](hipchat.md) An Atlassian product for private group chat and instant message. - [Irker](irker.md) An IRC gateway to receive messages on repository updates. - Pivotal Tracker - Pushover -- GitLab From 37973ef61b9c4dc8624bce79441113b35fa28e8e Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 16 Mar 2015 09:39:43 -0700 Subject: [PATCH 1363/1609] Fix typo for HipChat doc: messaging, not message --- doc/project_services/project_services.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 8510fcf0319..03937d20728 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -12,7 +12,7 @@ __Project integrations with external services for continuous integration and mor - Flowdock - Gemnasium - GitLab CI -- [HipChat](hipchat.md) An Atlassian product for private group chat and instant message. +- [HipChat](hipchat.md) An Atlassian product for private group chat and instant messaging. - [Irker](irker.md) An IRC gateway to receive messages on repository updates. - Pivotal Tracker - Pushover -- GitLab From 93e321e7c8e6c83f2b58098c514e148d8d70f79f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 16 Mar 2015 09:46:23 -0700 Subject: [PATCH 1364/1609] Fix typo in CHANGELOG and add credit --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index fc56a6af131..4bb7b29948a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,7 +4,7 @@ v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) - Fix broken email images (Hannes Rosenögger) - - Automaticly config git if user forgot, where possible + - Automatically config git if user forgot, where possible (Zeger-Jan van de Weg) - Fix mass SQL statements on initial push (Hannes Rosenögger) - Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu) - Add comment notification events to HipChat and Slack services (Stan Hu) -- GitLab From 7dedbfb097d1fae7a7d698ab0d3f981f8291b21e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 16 Mar 2015 10:56:54 -0700 Subject: [PATCH 1365/1609] Bump gitlab-shell version --- GITLAB_SHELL_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index fe16b348d97..e70b4523ae7 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.5.4 +2.6.0 -- GitLab From cba6d797d756e6cc3cf976610d5d21f55e6460b5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 16 Mar 2015 11:12:44 -0700 Subject: [PATCH 1366/1609] Remove ugly highlight styles --- app/assets/stylesheets/generic/forms.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index 5aa6f4cb66b..19bc11086e9 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -97,8 +97,3 @@ label { .wiki-content { margin-top: 35px; } - -.btn-group .btn.active { - text-shadow: 0 0 0.2em #D9534F, 0 0 0.2em #D9534F, 0 0 0.2em #D9534F; - background-color: #5487bf; -} -- GitLab From 9269606c94853c8cc0f9c850582dae09d809cdab Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Mon, 16 Mar 2015 12:09:53 -0700 Subject: [PATCH 1367/1609] Documentation about unicorn settings. --- doc/install/requirements.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 65ddb3e3cfb..f42af65796f 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -76,7 +76,8 @@ Notice: The 25 workers of Sidekiq will show up as separate processes in your pro ## Unicorn Workers -It's possible to increase the amount of unicorn workers and tis will usually help for to reduce the response time of the applications. +It's possible to increase the amount of unicorn workers and tis will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests. + For most instances we recommend using: CPU cores + 1 = unicorn workers. So for a machine with 2 cores, 3 unicorn workers is ideal. @@ -85,6 +86,8 @@ If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check). If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping. +To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). + ## Database If you want to run the database separately, the **recommended** database size is **1 MB per user**. -- GitLab From 7c3c836d3b62c72d1adf5431c0fedc91d92a8907 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Mon, 16 Mar 2015 13:29:27 -0600 Subject: [PATCH 1368/1609] Handle null restricted_visibility_levels setting Fix a 500 error when the `restricted_visibility_levels` setting is null in the database. --- app/helpers/visibility_level_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 7c090dc594c..0d573e72a80 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -62,6 +62,6 @@ module VisibilityLevelHelper def restricted_visibility_levels(show_all = false) return [] if current_user.is_admin? && !show_all - current_application_settings.restricted_visibility_levels + current_application_settings.restricted_visibility_levels || [] end end -- GitLab From 1b437ec3498bc544dbd1b252f5c755e9073407fd Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Mon, 16 Mar 2015 17:20:17 +0200 Subject: [PATCH 1369/1609] tests --- app/assets/javascripts/subscription.js.coffee | 3 +- app/controllers/projects/issues_controller.rb | 9 ++---- .../projects/merge_requests_controller.rb | 9 ++---- app/models/concerns/issuable.rb | 8 ++++- app/models/subscription.rb | 13 ++++++++ app/services/notification_service.rb | 4 ++- .../projects/issues/_issue_context.html.haml | 6 ++-- .../merge_requests/show/_context.html.haml | 6 ++-- config/routes.rb | 4 +-- ...150313012111_create_subscriptions_table.rb | 5 ++- features/project/issues/issues.feature | 8 +++++ features/project/merge_requests.feature | 7 ++++ features/steps/project/issues/issues.rb | 13 ++++++++ features/steps/project/merge_requests.rb | 13 ++++++++ spec/services/notification_service_spec.rb | 32 +++++++++++++++++++ 15 files changed, 115 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index f457622fc3a..a009969e4dc 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -1,14 +1,13 @@ class @Subscription constructor: (url) -> $(".subscribe-button").click (event)=> - self = @ btn = $(event.currentTarget) action = btn.prop("value") current_status = $(".sub_status").text().trim() $(".fa-spinner.subscription").removeClass("hidden") $(".sub_status").empty() - $.post url, subscription: action, => + $.post url, => $(".fa-spinner.subscription").addClass("hidden") status = if current_status == "subscribed" then "unsubscribed" else "subscribed" $(".sub_status").text(status) diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 903b7a68dc9..88302276b5e 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,6 +1,6 @@ class Projects::IssuesController < Projects::ApplicationController before_filter :module_enabled - before_filter :issue, only: [:edit, :update, :show, :set_subscription] + before_filter :issue, only: [:edit, :update, :show, :toggle_subscription] # Allow read any issue before_filter :authorize_read_issue! @@ -97,11 +97,8 @@ class Projects::IssuesController < Projects::ApplicationController redirect_to :back, notice: "#{result[:count]} issues updated" end - def set_subscription - subscribed = params[:subscription] == "Subscribe" - - sub = @issue.subscriptions.find_or_create_by(user_id: current_user.id) - sub.update(subscribed: subscribed) + def toggle_subscription + @issue.toggle_subscription(current_user) render nothing: true end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 51ac61c3273..c63a9b0cd44 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -2,7 +2,7 @@ require 'gitlab/satellite/satellite' class Projects::MergeRequestsController < Projects::ApplicationController before_filter :module_enabled - before_filter :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status, :set_subscription] + before_filter :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status, :toggle_subscription] before_filter :closes_issues, only: [:edit, :update, :show, :diffs] before_filter :validates_merge_request, only: [:show, :diffs] before_filter :define_show_vars, only: [:show, :diffs] @@ -174,11 +174,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end - def set_subscription - subscribed = params[:subscription] == "Subscribe" - - sub = @merge_request.subscriptions.find_or_create_by(user_id: current_user.id) - sub.update(subscribed: subscribed) + def toggle_subscription + @merge_request.toggle_subscription(current_user) render nothing: true end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index d1a35ca5294..88ac83744df 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -133,7 +133,7 @@ module Issuable users.concat(mentions.reduce([], :|)).uniq end - def subscription_status(user) + def subscribed?(user) subscription = subscriptions.find_by_user_id(user.id) if subscription @@ -143,6 +143,12 @@ module Issuable participants.include?(user) end + def toggle_subscription(user) + subscriptions. + find_or_initialize_by(user_id: user.id). + update(subscribed: !subscribed?(user)) + end + def to_hook_data(user) { object_kind: self.class.name.underscore, diff --git a/app/models/subscription.rb b/app/models/subscription.rb index 276cf0e9465..dd75d3ab8ba 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -1,3 +1,16 @@ +# == Schema Information +# +# Table name: subscriptions +# +# id :integer not null, primary key +# user_id :integer +# subscribable_id :integer +# subscribable_type :string(255) +# subscribed :boolean +# created_at :datetime +# updated_at :datetime +# + class Subscription < ActiveRecord::Base belongs_to :user belongs_to :subscribable, polymorphic: true diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 5ebde8fea84..3e1f4e62f10 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -92,6 +92,8 @@ class NotificationService # def merge_mr(merge_request, current_user) recipients = reject_muted_users([merge_request.author, merge_request.assignee], merge_request.target_project) + recipients = add_subscribed_users(recipients, merge_request) + recipients = reject_unsubscribed_users(recipients, merge_request) recipients = recipients.concat(project_watchers(merge_request.target_project)).uniq recipients.delete(current_user) @@ -333,7 +335,7 @@ class NotificationService subscriptions = target.subscriptions if subscriptions.any? - recipients + subscriptions.where("subscribed is true").map(&:user) + recipients + subscriptions.where(subscribed: true).map(&:user) else recipients end diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 24bfbdd4c58..85937e7bf42 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -33,12 +33,12 @@ Subscription: %i.fa.fa-spinner.fa-spin.hidden.subscription %span.sub_status - = @issue.subscription_status(current_user) ? "subscribed" : "unsubscribed" - - subscribe_action = @issue.subscription_status(current_user) ? "Unsubscribe" : "Subscribe" + = @issue.subscribed?(current_user) ? "subscribed" : "unsubscribed" + - subscribe_action = @issue.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" %input.btn.subscribe-button{:type => "button", :value => subscribe_action} :coffeescript $ -> - new Subscription("#{set_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") + new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") \ No newline at end of file diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index d0c00c1aeaf..79b0e7799a9 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -35,12 +35,12 @@ Subscription: %i.fa.fa-spinner.fa-spin.hidden.subscription %span.sub_status - = @merge_request.subscription_status(current_user) ? "subscribed" : "unsubscribed" - - subscribe_action = @merge_request.subscription_status(current_user) ? "Unsubscribe" : "Subscribe" + = @merge_request.subscribed?(current_user) ? "subscribed" : "unsubscribed" + - subscribe_action = @merge_request.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" %input.btn.subscribe-button{:type => "button", :value => subscribe_action} :coffeescript $ -> - new Subscription("#{set_subscription_namespace_project_issue_path(@merge_request.project.namespace, @project, @merge_request)}") + new Subscription("#{toggle_subscription_namespace_project_merge_request_path(@merge_request.project.namespace, @project, @merge_request)}") \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index a976ba9d593..ad5f2c10f63 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -406,7 +406,7 @@ Gitlab::Application.routes.draw do post :automerge get :automerge_check get :ci_status - post :set_subscription + post :toggle_subscription end collection do @@ -442,7 +442,7 @@ Gitlab::Application.routes.draw do resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do member do - post :set_subscription + post :toggle_subscription end collection do post :bulk_update diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb index 78f7aeeaf7c..a1d4d9dedc5 100644 --- a/db/migrate/20150313012111_create_subscriptions_table.rb +++ b/db/migrate/20150313012111_create_subscriptions_table.rb @@ -8,6 +8,9 @@ class CreateSubscriptionsTable < ActiveRecord::Migration t.timestamps end - add_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id], unique: true, name: 'subscriptions_user_id_and_ref_fields' + add_index :subscriptions, + [:subscribable_id, :subscribable_type, :user_id], + unique: true, + name: 'subscriptions_user_id_and_ref_fields' end end diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 283979204db..b9031f6f32b 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -202,3 +202,11 @@ Feature: Project Issues And I click link "Edit" for the issue And I preview a description text like "Bug fixed :smile:" Then I should see the Markdown write tab + + @javascript + Scenario: I can unsubscribe from issue + Given project "Shop" has "Tasks-open" open issue with task markdown + When I visit issue page "Tasks-open" + Then I should see that I am subscribed + When I click button "Unsubscribe" + Then I should see that I am unsubscribed diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index adad100e56c..91dc576f8b4 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -225,3 +225,10 @@ Feature: Project Merge Requests When I fill in merge request search with "Fe" Then I should see "Feature NS-03" in merge requests And I should not see "Bug NS-04" in merge requests + + @javascript + Scenario: I can unsubscribe from merge request + Given I visit merge request page "Bug NS-04" + Then I should see that I am subscribed + When I click button "Unsubscribe" + Then I should see that I am unsubscribed diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index 6d72c93ad13..cc0d6033a2b 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -18,10 +18,23 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps page.should_not have_content "Tweet control" end + step 'I should see that I am subscribed' do + find(".sub_status").text.should == "subscribed" + end + + step 'I should see that I am unsubscribed' do + sleep 0.2 + find(".sub_status").text.should == "unsubscribed" + end + step 'I click link "Closed"' do click_link "Closed" end + step 'I click button "Unsubscribe"' do + click_on "Unsubscribe" + end + step 'I should see "Release 0.3" in issues' do page.should have_content "Release 0.3" end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index b67b2e58caf..5a35d703765 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -56,6 +56,19 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.should_not have_content "Bug NS-04" end + step 'I should see that I am subscribed' do + find(".sub_status").text.should == "subscribed" + end + + step 'I should see that I am unsubscribed' do + sleep 0.2 + find(".sub_status").text.should == "unsubscribed" + end + + step 'I click button "Unsubscribe"' do + click_on "Unsubscribe" + end + step 'I click link "Close"' do first(:css, '.close-mr-link').click end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 2074f8e7f78..5badb635325 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -41,13 +41,18 @@ describe NotificationService do describe :new_note do it do + add_users_with_subscription(note.project, issue) + should_email(@u_watcher.id) should_email(note.noteable.author_id) should_email(note.noteable.assignee_id) should_email(@u_mentioned.id) + should_email(@subscriber.id) should_not_email(note.author_id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) + should_not_email(@unsubscriber.id) + notification.new_note(note) end @@ -191,6 +196,7 @@ describe NotificationService do before do build_team(issue.project) + add_users_with_subscription(issue.project, issue) end describe :new_issue do @@ -224,6 +230,8 @@ describe NotificationService do should_email(issue.assignee_id) should_email(@u_watcher.id) should_email(@u_participant_mentioned.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -245,6 +253,8 @@ describe NotificationService do should_email(issue.author_id) should_email(@u_watcher.id) should_email(@u_participant_mentioned.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -266,6 +276,8 @@ describe NotificationService do should_email(issue.author_id) should_email(@u_watcher.id) should_email(@u_participant_mentioned.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -287,6 +299,7 @@ describe NotificationService do before do build_team(merge_request.target_project) + add_users_with_subscription(merge_request.target_project, merge_request) end describe :new_merge_request do @@ -311,6 +324,8 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.reassigned_merge_request(merge_request, merge_request.author) @@ -329,6 +344,8 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.close_mr(merge_request, @u_disabled) @@ -347,6 +364,8 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.merge_mr(merge_request, @u_disabled) @@ -365,6 +384,8 @@ describe NotificationService do it do should_email(merge_request.assignee_id) should_email(@u_watcher.id) + should_email(@subscriber.id) + should_not_email(@unsubscriber.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) notification.reopen_mr(merge_request, @u_disabled) @@ -420,4 +441,15 @@ describe NotificationService do project.team << [@u_mentioned, :master] project.team << [@u_committer, :master] end + + def add_users_with_subscription(project, issuable) + @subscriber = create :user + @unsubscriber = create :user + + project.team << [@subscriber, :master] + project.team << [@unsubscriber, :master] + + issuable.subscriptions.create(user: @subscriber, subscribed: true) + issuable.subscriptions.create(user: @unsubscriber, subscribed: false) + end end -- GitLab From 2e672c39a09577a0a16e75a10a249c923d8ee863 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Mon, 16 Mar 2015 13:59:50 -0600 Subject: [PATCH 1370/1609] Fix restricted visibility bugs Check for nil values in the restricted_visibility_level validation method, and set the restricted visibility request parameter to `[]` when it's missing from the request. --- app/controllers/admin/application_settings_controller.rb | 4 +++- app/models/application_setting.rb | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8f7d5e8006f..9a5685877f8 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -21,7 +21,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController def application_setting_params restricted_levels = params[:application_setting][:restricted_visibility_levels] - unless restricted_levels.nil? + if restricted_levels.nil? + params[:application_setting][:restricted_visibility_levels] = [] + else restricted_levels.map! do |level| level.to_i end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 6abdf0c755a..1c87db613ae 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -27,9 +27,11 @@ class ApplicationSetting < ActiveRecord::Base if: :home_page_url_column_exist validates_each :restricted_visibility_levels do |record, attr, value| - value.each do |level| - unless Gitlab::VisibilityLevel.options.has_value?(level) - record.errors.add(attr, "'#{level}' is not a valid visibility level") + unless value.nil? + value.each do |level| + unless Gitlab::VisibilityLevel.options.has_value?(level) + record.errors.add(attr, "'#{level}' is not a valid visibility level") + end end end end -- GitLab From 8eebb6e56601738027d8c5e65c9c3bd77911e3a8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 16 Mar 2015 13:13:02 -0700 Subject: [PATCH 1371/1609] Fix editor UI bug --- app/assets/stylesheets/pages/editor.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 88aa256e56e..851f126318d 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -16,8 +16,6 @@ } } .commit-button-annotation { - @extend .alert; - @extend .alert-info; display: inline-block; margin: 0; padding: 2px; -- GitLab From 5c9a924fe7172f62ed359277e467c8570bda718c Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 16 Mar 2015 13:45:27 -0700 Subject: [PATCH 1372/1609] Add nodejs to installation doc. --- doc/install/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 2b204c72476..5170f6dc0de 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -56,7 +56,7 @@ up-to-date and install it. Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake libkrb5-dev + sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake libkrb5-dev nodejs Make sure you have the right version of Git installed -- GitLab From b70e5c57c31d4cc0970c01921322d647d01e3a8b Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 16 Mar 2015 13:47:21 -0700 Subject: [PATCH 1373/1609] Add nodejs dependency to upgrader and upgrade from 7.7 docs --- doc/update/7.8-to-7.9.md | 120 +++++++++++++++++++++++++++++++++++++++ doc/update/upgrader.md | 2 +- 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 doc/update/7.8-to-7.9.md diff --git a/doc/update/7.8-to-7.9.md b/doc/update/7.8-to-7.9.md new file mode 100644 index 00000000000..28fd433e1c8 --- /dev/null +++ b/doc/update/7.8-to-7.9.md @@ -0,0 +1,120 @@ +# From 7.8 to 7.9 + +### 0. Stop server + + sudo service gitlab stop + +### 1. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 2. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-9-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-9-stable-ee +``` + +### 3. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.0 +``` + +### 4. Install libs, migrations, etc. + +```bash +sudo apt-get install nodejs + +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 5. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-8-stable:config/gitlab.yml.example origin/7-9-stable:config/gitlab.yml.example +``` + +#### Change Nginx settings + +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/lib/support/nginx/gitlab-ssl) but with your settings. +* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. + +#### Setup time zone (optional) + +Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it. + +### 6. Start application + + sudo service gitlab start + sudo service nginx restart + +### 7. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations upgrade is complete! + +### 8. GitHub settings (if applicable) + +If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it +only contains a root URL (ex. `https://gitlab.example.com/`) + +## Things went south? Revert to previous version (7.8) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.7 to 7.8](7.7-to-7.8.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index 4ed35b2b562..d8476fb3457 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -24,7 +24,7 @@ If you have local changes to your GitLab repository the script will stash them a ## 2. Run GitLab upgrade tool -Note: GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX) while 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +Note: GitLab 7.9 adds nodejs as a dependency. GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX). GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) # Starting with GitLab version 7.0 upgrader script has been moved to bin directory cd /home/git/gitlab -- GitLab From 90aa870c3607c170091b6034c0150f119697b0b9 Mon Sep 17 00:00:00 2001 From: Christian Walther Date: Sat, 21 Feb 2015 22:12:13 +0100 Subject: [PATCH 1374/1609] Fix invalid Atom feeds when using emoji, horizontal rules, or images. Fixes issues #880, #723, #1113: Markdown must be rendered to XHTML, not HTML, when generating summary content for Atom feeds. Otherwise, content-less tags like and
    , generated when issue descriptions, merge request descriptions, comments, or commit messages use emoji, horizontal rules, or images, are not terminated and make the Atom XML invalid. --- CHANGELOG | 1 + app/views/events/_event_issue.atom.haml | 2 +- .../events/_event_merge_request.atom.haml | 2 +- app/views/events/_event_note.atom.haml | 2 +- app/views/events/_event_push.atom.haml | 2 +- lib/gitlab/markdown.rb | 30 ++++++++++++++----- lib/redcarpet/render/gitlab_html.rb | 6 +--- spec/features/atom/users_spec.rb | 27 +++++++++++++++-- 8 files changed, 53 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c4b5a847e12..92aadc05849 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ v 7.9.0 (unreleased) - Move labels/milestones tabs to sidebar - Improve UI for commits, issues and merge request lists - Fix commit comments on first line of diff not rendering in Merge Request Discussion view. + - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) v 7.8.0 (unreleased) - Fix access control and protection against XSS for note attachments and other uploads. diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index eba2b63797a..0edb61ea246 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,3 +1,3 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - if issue.description.present? - = markdown issue.description + = markdown(issue.description, xhtml: true) diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index 0aea2d17d65..1a8b62abeab 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,3 +1,3 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - if merge_request.description.present? - = markdown merge_request.description + = markdown(merge_request.description, xhtml: true) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index be0e05481ed..b49c331ccf2 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - = markdown note.note + = markdown(note.note, xhtml: true) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 2b63519edac..2fb9f7ec246 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -6,7 +6,7 @@ %i at = commit[:timestamp].to_time.to_s(:short) - %blockquote= markdown(escape_once(commit[:message])) + %blockquote= markdown(escape_once(commit[:message]), xhtml: true) - if event.commits_count > 15 %p %i diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index fb0218a2778..dceb2bc71f1 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -33,17 +33,23 @@ module Gitlab attr_reader :html_options - def gfm_with_tasks(text, project = @project, html_options = {}) - text = gfm(text, project, html_options) - parse_tasks(text) + # Public: Parse the provided text with GitLab-Flavored Markdown + # + # text - the source text + # project - extra options for the reference links as given to link_to + # html_options - extra options for the reference links as given to link_to + def gfm(text, project = @project, html_options = {}) + gfm_with_options(text, {}, project, html_options) end # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text + # options - parse_tasks: true - render tasks + # - xhtml: true - output XHTML instead of HTML # project - extra options for the reference links as given to link_to # html_options - extra options for the reference links as given to link_to - def gfm(text, project = @project, html_options = {}) + def gfm_with_options(text, options = {}, project = @project, html_options = {}) return text if text.nil? # Duplicate the string so we don't alter the original, then call to_str @@ -86,14 +92,22 @@ module Gitlab markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline result = markdown_pipeline.call(text, markdown_context) - text = result[:output].to_html(save_with: 0) + saveoptions = 0 + if options[:xhtml] + saveoptions |= Nokogiri::XML::Node::SaveOptions::AS_XHTML + end + text = result[:output].to_html(save_with: saveoptions) allowed_attributes = ActionView::Base.sanitized_allowed_attributes allowed_tags = ActionView::Base.sanitized_allowed_tags - sanitize text.html_safe, - attributes: allowed_attributes + %w(id class style), - tags: allowed_tags + %w(table tr td th) + text = sanitize text.html_safe, + attributes: allowed_attributes + %w(id class style), + tags: allowed_tags + %w(table tr td th) + if options[:parse_tasks] + text = parse_tasks(text) + end + text end private diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 714261f815c..8b0c193f3db 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -58,10 +58,6 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML unless @template.instance_variable_get("@project_wiki") || @project.nil? full_document = h.create_relative_links(full_document) end - if @options[:parse_tasks] - h.gfm_with_tasks(full_document) - else - h.gfm(full_document) - end + h.gfm_with_options(full_document, @options) end end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index c0316b073ad..770ac04c2c5 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -15,17 +15,24 @@ describe "User Feed", feature: true do let(:project) { create(:project) } let(:issue) do create(:issue, project: project, - author: user, description: '') + author: user, description: "Houston, we have a bug!\n\n***\n\nI guess.") end let(:note) do create(:note, noteable: issue, author: user, - note: 'Bug confirmed', project: project) + note: 'Bug confirmed :+1:', project: project) + end + let(:merge_request) do + create(:merge_request, + title: 'Fix bug', author: user, + source_project: project, target_project: project, + description: "Here is the fix: ![an image](image.png)") end before do project.team << [user, :master] issue_event(issue, user) note_event(note, user) + merge_request_event(merge_request, user) visit user_path(user, :atom, private_token: user.private_token) end @@ -37,6 +44,18 @@ describe "User Feed", feature: true do expect(body). to have_content("#{safe_name} commented on issue ##{issue.iid}") end + + it 'should have XHTML summaries in issue descriptions' do + expect(body).to match /we have a bug!<\/p>\n\n
    \n\n

    I guess/ + end + + it 'should have XHTML summaries in notes' do + expect(body).to match /Bug confirmed ]*\/>/ + end + + it 'should have XHTML summaries in merge request descriptions' do + expect(body).to match /Here is the fix: ]*\/>/ + end end end @@ -48,6 +67,10 @@ describe "User Feed", feature: true do EventCreateService.new.leave_note(note, user) end + def merge_request_event(request, user) + EventCreateService.new.open_mr(request, user) + end + def safe_name html_escape(user.name) end -- GitLab From 84edc020b2107252a383ce73ca41924b8be9ddad Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 16 Mar 2015 23:44:30 -0700 Subject: [PATCH 1375/1609] Fix button color inside alert --- app/assets/stylesheets/base/gl_bootstrap.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index be1ee90c18f..82c51cf4852 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -193,7 +193,7 @@ .panel-warning .panel-heading, .panel-primary .panel-heading, .alert { - a { + a:not(.btn) { @extend .alert-link; color: #fff; text-decoration: underline; -- GitLab From 3a324d9c6d52b7605e8b701ac70eddc2528b408b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 16 Mar 2015 23:51:46 -0700 Subject: [PATCH 1376/1609] Revert "Merge branch 'backup-permissions' into 'master'" This reverts commit c42262b43b009af990e5769840391862d64a1c2d, reversing changes made to c6586b1283a94c8f08bc669f4d8a9384b263073e. --- CHANGELOG | 1 - lib/backup/manager.rb | 4 --- spec/tasks/gitlab/backup_rake_spec.rb | 50 ++++----------------------- 3 files changed, 7 insertions(+), 48 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 86e18d09f33..bd66a92933d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -32,7 +32,6 @@ v 7.9.0 (unreleased) - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options - - Restrict permissions on backup files - Add grouped milestones from all projects to dashboard. - Web hook sends pusher email as well as commiter - Add Bitbucket omniauth provider. diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index b499e5755bd..ab8db4e9837 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -17,18 +17,14 @@ module Backup file << s.to_yaml.gsub(/^---\n/,'') end - FileUtils.chmod_R(0700, %w{db uploads repositories}) - # create archive $progress.print "Creating backup archive: #{tar_file} ... " - orig_umask = File.umask(0077) if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) $progress.puts "done".green else puts "creating archive #{tar_file} failed".red abort 'Backup failed' end - File.umask(orig_umask) upload(tar_file) end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index e6763be7b8f..60942cc95fc 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -10,17 +10,17 @@ describe 'gitlab:app namespace rake task' do Rake::Task.define_task :environment end - def run_rake_task(task_name) - Rake::Task[task_name].reenable - Rake.application.invoke_task task_name - end - describe 'backup_restore' do before do # avoid writing task output to spec progress allow($stdout).to receive :write end + let :run_rake_task do + Rake::Task["gitlab:backup:restore"].reenable + Rake.application.invoke_task "gitlab:backup:restore" + end + context 'gitlab version' do before do Dir.stub glob: [] @@ -36,9 +36,7 @@ describe 'gitlab:app namespace rake task' do it 'should fail on mismatch' do YAML.stub load_file: {gitlab_version: "not #{gitlab_version}" } - expect { run_rake_task('gitlab:backup:restore') }.to( - raise_error SystemExit - ) + expect { run_rake_task }.to raise_error SystemExit end it 'should invoke restoration on mach' do @@ -46,43 +44,9 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke expect(Rake::Task["gitlab:backup:repo:restore"]).to receive :invoke expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke - expect { run_rake_task('gitlab:backup:restore') }.to_not raise_error + expect { run_rake_task }.to_not raise_error end end end # backup_restore task - - describe 'backup_create' do - def tars_glob - Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) - end - - before :all do - FileUtils.rm(tars_glob) - orig_stdout = $stdout - $stdout = StringIO.new - run_rake_task('gitlab:backup:create') - $stdout = orig_stdout - - @backup_tar = tars_glob.first - end - - before do - backup_path = File.join(Gitlab.config.backup.path, 'test') - allow(Gitlab.config.backup).to receive(:path).and_return(backup_path) - end - - it 'should set correct permissions on the tar file' do - expect(File.exist?(@backup_tar)).to be_truthy - expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600') - end - - it 'should set correct permissions on the tar contents' do - tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads repositories} - ) - expect(exit_status).to eq(0) - expect(tar_contents).not_to match(/^.{4,9}[rwx]/) - end - end # backup_create task end # gitlab:app namespace -- GitLab From 409097bd7e0f5857cf0bc5462bd47484980ec787 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 00:06:25 -0700 Subject: [PATCH 1377/1609] Properly align save user profile button --- app/views/profiles/show.html.haml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index e6b204451c4..409b6b5a193 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -96,5 +96,7 @@ .row .col-md-7 - .col-sm-2 - = f.submit 'Save changes', class: "btn btn-success" + .form-group + .col-sm-2   + .col-sm-10 + = f.submit 'Save changes', class: "btn btn-success" -- GitLab From df91781a346e6b70c43195f2f4f550b097ac9d2e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 10:09:49 +0100 Subject: [PATCH 1378/1609] Fix changelog. --- CHANGELOG | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ec30b09b90b..15e220b4834 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -74,6 +74,7 @@ v 7.9.0 (unreleased) - Raise recommended number of unicorn workers from 2 to 3 - Use same layout and interactivity for project members as group members. - Prevent gitlab-shell character encoding issues by receiving its changes as raw data. + - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers @@ -101,9 +102,6 @@ v 7.8.1 - Fix urls for the issues when relative url was enabled v 7.8.0 - - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) - -v 7.8.0 (unreleased) - Fix access control and protection against XSS for note attachments and other uploads. - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) -- GitLab From 9c7fffb6559facdcf8bbda680795f70d836293bf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 14:55:43 +0100 Subject: [PATCH 1379/1609] Delete deploy key when last connection to a project is destroyed. --- CHANGELOG | 1 + .../projects/deploy_keys_controller.rb | 5 +-- app/models/deploy_keys_project.rb | 8 +++++ spec/models/deploy_keys_project_spec.rb | 33 +++++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bd66a92933d..23744c0405c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -74,6 +74,7 @@ v 7.9.0 (unreleased) - Raise recommended number of unicorn workers from 2 to 3 - Use same layout and interactivity for project members as group members. - Prevent gitlab-shell character encoding issues by receiving its changes as raw data. + - Delete deploy key when last connection to a project is destroyed. v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index b7cc305899c..2ecde8381e7 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -37,7 +37,8 @@ class Projects::DeployKeysController < Projects::ApplicationController @key.destroy respond_to do |format| - format.html { redirect_to project_deploy_keys_url } + format.html { redirect_to namespace_project_deploy_keys_path(@project.namespace, + @project) } format.js { render nothing: true } end end @@ -50,7 +51,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def disable - @project.deploy_keys_projects.where(deploy_key_id: params[:id]).last.destroy + @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index f23d8205ddc..7e88903b9af 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -16,4 +16,12 @@ class DeployKeysProject < ActiveRecord::Base validates :deploy_key_id, presence: true validates :deploy_key_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :project_id, presence: true + + after_destroy :destroy_orphaned_deploy_key + + private + + def destroy_orphaned_deploy_key + self.deploy_key.destroy if self.deploy_key.deploy_keys_projects.length == 0 + end end diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index aacd9bf38bf..f351aab9238 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -21,4 +21,37 @@ describe DeployKeysProject do it { is_expected.to validate_presence_of(:project_id) } it { is_expected.to validate_presence_of(:deploy_key_id) } end + + describe "Destroying" do + let(:project) { create(:project) } + subject { create(:deploy_keys_project, project: project) } + let(:deploy_key) { subject.deploy_key } + + context "when the deploy key is only used by this project" do + it "destroys the deploy key" do + subject.destroy + + expect { + deploy_key.reload + }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context "when the deploy key is used by more than one project" do + + let!(:other_project) { create(:project) } + + before do + other_project.deploy_keys << deploy_key + end + + it "doesn't destroy the deploy key" do + subject.destroy + + expect { + deploy_key.reload + }.not_to raise_error(ActiveRecord::RecordNotFound) + end + end + end end -- GitLab From 7d2b34bd61df9722ac2461e87ce595228eecef21 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 16:00:32 +0100 Subject: [PATCH 1380/1609] Satisfy Rubocop. --- app/controllers/projects/deploy_keys_controller.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 2ecde8381e7..679a5d76ec0 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -37,8 +37,7 @@ class Projects::DeployKeysController < Projects::ApplicationController @key.destroy respond_to do |format| - format.html { redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) } + format.html { redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) } format.js { render nothing: true } end end -- GitLab From 22fcb2f418ed6a2c7e68c0cd3ec2d414510ad4ec Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 17 Mar 2015 15:04:25 +0200 Subject: [PATCH 1381/1609] improve UI --- app/assets/javascripts/subscription.js.coffee | 16 ++++++++-------- .../projects/issues/_issue_context.html.haml | 14 +++++++++----- .../merge_requests/show/_context.html.haml | 14 +++++++++----- features/steps/project/issues/issues.rb | 4 ++-- features/steps/project/merge_requests.rb | 4 ++-- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee index a009969e4dc..7f41616d4e7 100644 --- a/app/assets/javascripts/subscription.js.coffee +++ b/app/assets/javascripts/subscription.js.coffee @@ -1,17 +1,17 @@ class @Subscription constructor: (url) -> - $(".subscribe-button").click (event)=> + $(".subscribe-button").unbind("click").click (event)=> btn = $(event.currentTarget) - action = btn.prop("value") - current_status = $(".sub_status").text().trim() - $(".fa-spinner.subscription").removeClass("hidden") - $(".sub_status").empty() + action = btn.find("span").text() + current_status = $(".subscription-status").attr("data-status") + btn.prop("disabled", true) $.post url, => - $(".fa-spinner.subscription").addClass("hidden") + btn.prop("disabled", false) status = if current_status == "subscribed" then "unsubscribed" else "subscribed" - $(".sub_status").text(status) + $(".subscription-status").attr("data-status", status) action = if status == "subscribed" then "Unsubscribe" else "Subscribe" - btn.prop("value", action) + btn.find("span").text(action) + $(".subscription-status>div").toggleClass("hidden") diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 85937e7bf42..cb4846a41d1 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -31,11 +31,15 @@ .issuable-context-title %label Subscription: - %i.fa.fa-spinner.fa-spin.hidden.subscription - %span.sub_status - = @issue.subscribed?(current_user) ? "subscribed" : "unsubscribed" - - subscribe_action = @issue.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" - %input.btn.subscribe-button{:type => "button", :value => subscribe_action} + %button.btn.btn-block.subscribe-button + %i.fa.fa-eye + %span= @issue.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" + - subscribtion_status = @issue.subscribed?(current_user) ? "subscribed" : "unsubscribed" + .subscription-status{"data-status" => subscribtion_status} + .description-block.unsubscribed{class: ( "hidden" if @issue.subscribed?(current_user) )} + You're not receiving notifications from this thread. + .description-block.subscribed{class: ( "hidden" unless @issue.subscribed?(current_user) )} + You're receiving notifications because you're subscribed to this thread. :coffeescript $ -> diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index 79b0e7799a9..753c7e0e611 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -33,11 +33,15 @@ .issuable-context-title %label Subscription: - %i.fa.fa-spinner.fa-spin.hidden.subscription - %span.sub_status - = @merge_request.subscribed?(current_user) ? "subscribed" : "unsubscribed" - - subscribe_action = @merge_request.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" - %input.btn.subscribe-button{:type => "button", :value => subscribe_action} + %button.btn.btn-block.subscribe-button + %i.fa.fa-eye + %span= @merge_request.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" + - subscribtion_status = @merge_request.subscribed?(current_user) ? "subscribed" : "unsubscribed" + .subscription-status{"data-status" => subscribtion_status} + .description-block.unsubscribed{class: ( "hidden" if @merge_request.subscribed?(current_user) )} + You're not receiving notifications from this thread. + .description-block.subscribed{class: ( "hidden" unless @merge_request.subscribed?(current_user) )} + You're receiving notifications because you're subscribed to this thread. :coffeescript $ -> diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index cc0d6033a2b..e8ca3f7c176 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -19,12 +19,12 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps end step 'I should see that I am subscribed' do - find(".sub_status").text.should == "subscribed" + find(".subscribe-button span").text.should == "Unsubscribe" end step 'I should see that I am unsubscribed' do sleep 0.2 - find(".sub_status").text.should == "unsubscribed" + find(".subscribe-button span").text.should == "Subscribe" end step 'I click link "Closed"' do diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 5a35d703765..6e2f60972b6 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -57,12 +57,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see that I am subscribed' do - find(".sub_status").text.should == "subscribed" + find(".subscribe-button span").text.should == "Unsubscribe" end step 'I should see that I am unsubscribed' do sleep 0.2 - find(".sub_status").text.should == "unsubscribed" + find(".subscribe-button span").text.should == "Subscribe" end step 'I click button "Unsubscribe"' do -- GitLab From b27622a16f04b92634c7de7765ef182f69f3c6a3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 16:34:17 +0100 Subject: [PATCH 1382/1609] Update Grack. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d6e66707c8a..9ca0e4e3f74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -200,7 +200,7 @@ GEM gitlab-flowdock-git-hook (0.4.2.2) gitlab-grit (>= 2.4.1) multi_json - gitlab-grack (2.0.0.rc2) + gitlab-grack (2.0.0) rack (~> 1.5.1) gitlab-grit (2.7.2) charlock_holmes (~> 0.6) -- GitLab From 16b73176694d248d4ee6f8c8525857169872e12e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 17:15:39 +0100 Subject: [PATCH 1383/1609] Update omniauth-ldap. --- Gemfile | 2 +- Gemfile.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 9dd75455c87..5bbbed9c139 100644 --- a/Gemfile +++ b/Gemfile @@ -45,7 +45,7 @@ gem "gitlab_git", '~> 7.1.0' gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' # LDAP Auth -gem 'gitlab_omniauth-ldap', '1.2.0', require: "omniauth-ldap" +gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap" # Git Wiki gem 'gollum-lib', '~> 4.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index d6e66707c8a..bcf36897162 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -219,7 +219,7 @@ GEM gitlab-linguist (~> 3.0) rugged (~> 0.21.2) gitlab_meta (7.0) - gitlab_omniauth-ldap (1.2.0) + gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.1) @@ -336,7 +336,7 @@ GEM multi_xml (0.5.5) multipart-post (1.2.0) mysql2 (0.3.16) - net-ldap (0.9.0) + net-ldap (0.11) net-scp (1.1.2) net-ssh (>= 2.6.5) net-ssh (2.8.0) @@ -516,7 +516,7 @@ GEM sexp_processor (~> 4.0) ruby_parser (3.5.0) sexp_processor (~> 4.1) - rubyntlm (0.4.0) + rubyntlm (0.5.0) rubypants (0.2.0) rugged (0.21.4) rugments (1.0.0.beta4) @@ -707,7 +707,7 @@ DEPENDENCIES gitlab_emoji (~> 0.1) gitlab_git (~> 7.1.0) gitlab_meta (= 7.0) - gitlab_omniauth-ldap (= 1.2.0) + gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.0) gon (~> 5.0.0) grape (~> 0.6.1) -- GitLab From 6a269450e6b8443a6a15b8ba6e0fe6737c78bd5b Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Mon, 16 Mar 2015 19:48:36 -0500 Subject: [PATCH 1384/1609] Fix UI bug regarding services --- app/views/admin/services/_form.html.haml | 6 +++--- app/views/projects/services/_form.html.haml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 291e48efc12..a953833b37c 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -14,9 +14,9 @@ = preserve do = markdown @service.help - .form-group - = f.label :url, "Trigger", class: 'control-label' - - if @service.supported_events.length > 1 + - if @service.supported_events.length > 1 + .form-group + = f.label :url, "Trigger", class: 'control-label' .col-sm-10 - if @service.supported_events.include?("push") %div diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 3492dd5babd..bb983229b1c 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -27,9 +27,9 @@ .col-sm-10 = f.check_box :active - .form-group - = f.label :url, "Trigger", class: 'control-label' - - if @service.supported_events.length > 1 + - if @service.supported_events.length > 1 + .form-group + = f.label :url, "Trigger", class: 'control-label' .col-sm-10 - if @service.supported_events.include?("push") %div -- GitLab From 02f61649cca4b2836ef33925ae7fcbb9855ba78f Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 17 Mar 2015 12:12:36 -0700 Subject: [PATCH 1385/1609] REadme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0563ceca409..d9476a811c8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ![logo](https://about.gitlab.com/images/gitlab_logo.png) GitLab - +sdfsd ## Open source software to collaborate on code ![Animated screenshots](https://about.gitlab.com/images/animated/compiled.gif) -- GitLab From 9a0c99274e21c86da84f903fad257a82dc7cc40f Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 17 Mar 2015 14:07:05 -0700 Subject: [PATCH 1386/1609] Update upgrade and installation docs for 7.9. --- doc/install/installation.md | 6 ++--- ...-or-7.x-to-7.8.md => 6.x-or-7.x-to-7.9.md} | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) rename doc/update/{6.x-or-7.x-to-7.8.md => 6.x-or-7.x-to-7.9.md} (93%) diff --git a/doc/install/installation.md b/doc/install/installation.md index 5170f6dc0de..d6208bb0797 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -183,9 +183,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-8-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-9-stable gitlab -**Note:** You can change `7-8-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-9-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -280,7 +280,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.5.4] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.0] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: diff --git a/doc/update/6.x-or-7.x-to-7.8.md b/doc/update/6.x-or-7.x-to-7.9.md similarity index 93% rename from doc/update/6.x-or-7.x-to-7.8.md rename to doc/update/6.x-or-7.x-to-7.9.md index 673d9253d62..bd6eb6b211f 100644 --- a/doc/update/6.x-or-7.x-to-7.8.md +++ b/doc/update/6.x-or-7.x-to-7.9.md @@ -1,7 +1,7 @@ -# From 6.x or 7.x to 7.8 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.8.md) for the most up to date instructions.* +# From 6.x or 7.x to 7.9 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.9.md) for the most up to date instructions.* -This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.8. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.9. ## Global issue numbers @@ -71,7 +71,7 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut For GitLab Community Edition: ```bash -sudo -u git -H git checkout 7-8-stable +sudo -u git -H git checkout 7-9-stable ``` OR @@ -79,7 +79,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 7-8-stable-ee +sudo -u git -H git checkout 7-9-stable-ee ``` ## 4. Install additional packages @@ -93,6 +93,9 @@ sudo apt-get install pkg-config cmake # Install Kerberos header files, which are needed for GitLab EE Kerberos support sudo apt-get install libkrb5-dev + +# Install nodejs, javascript runtime required for assets +sudo apt-get install nodejs ``` ## 5. Configure Redis to use sockets @@ -123,7 +126,7 @@ sudo apt-get install libkrb5-dev ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.5.4 +sudo -u git -H git checkout v2.6.0 ``` ## 7. Install libs, migrations, etc. @@ -158,12 +161,12 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7-8-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7-9-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/config/unicorn.rb.example but with your settings. -* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.5.4/config.yml.example but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.0/config.yml.example but with your settings. * Copy rack attack middleware config ```bash @@ -178,8 +181,8 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab ### Change Nginx settings -* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-8-stable/lib/support/nginx/gitlab-ssl but with your settings. +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/lib/support/nginx/gitlab-ssl but with your settings. * A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. ## 9. Start application -- GitLab From cb3b671839ccb99fbe100ae1fe684d391a30f191 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 17:06:40 -0700 Subject: [PATCH 1387/1609] Its time for 7.10.0.pre --- CHANGELOG | 2 ++ VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 27b930d23a6..c4e47346fd8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. +v 7.10.0 (unreleased) + v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) diff --git a/VERSION b/VERSION index e5d25bf79a9..67fc32adaba 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.9.0.pre +7.10.0.pre -- GitLab From d659c1d1fddcb4d915f07ad1152b1d8a999a1f64 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 18:01:33 -0700 Subject: [PATCH 1388/1609] More actively use css variabled to prevent colors duplication --- app/assets/stylesheets/generic/files.scss | 2 +- app/assets/stylesheets/generic/lists.scss | 9 ++--- .../stylesheets/generic/nav_sidebar.scss | 34 +++++++++---------- .../stylesheets/generic/typography.scss | 3 +- app/assets/stylesheets/pages/diff.scss | 2 +- app/assets/stylesheets/pages/editor.scss | 2 +- app/assets/stylesheets/pages/events.scss | 2 +- .../stylesheets/pages/merge_requests.scss | 2 +- app/assets/stylesheets/pages/tree.scss | 2 +- .../notify/project_was_moved_email.html.haml | 4 +-- 10 files changed, 31 insertions(+), 31 deletions(-) diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index 91220a856ac..8014dcb165b 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -94,7 +94,7 @@ } .author, .blame_commit { - background: #f5f5f5; + background: $background-color; vertical-align: top; } .lines { diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/generic/lists.scss index 5950885c42c..85176fec0dc 100644 --- a/app/assets/stylesheets/generic/lists.scss +++ b/app/assets/stylesheets/generic/lists.scss @@ -35,7 +35,7 @@ color: #8a6d3b; } - &.smoke { background-color: #f5f5f5; } + &.smoke { background-color: $background-color; } &:hover { background: $hover; @@ -46,7 +46,7 @@ border-bottom: none; &.bottom { - background: #f5f5f5; + background: $background-color; } } @@ -74,9 +74,10 @@ } .row_title { - color: #444; + color: $text-color; + &:hover { - color: #444; + color: $text-color; text-decoration: underline; } } diff --git a/app/assets/stylesheets/generic/nav_sidebar.scss b/app/assets/stylesheets/generic/nav_sidebar.scss index c14f12284da..bb890171985 100644 --- a/app/assets/stylesheets/generic/nav_sidebar.scss +++ b/app/assets/stylesheets/generic/nav_sidebar.scss @@ -1,18 +1,18 @@ .page-with-sidebar { - background: #F5F5F5; + background: $background-color; .sidebar-wrapper { position: fixed; top: 0; left: 0; height: 100%; - border-right: 1px solid #EAEAEA; + border-right: 1px solid $border-color; } } .sidebar-wrapper { z-index: 99; - background: #F5F5F5; + background: $background-color; } .content-wrapper { @@ -39,7 +39,7 @@ .nav-sidebar li { &.active a { - color: #333; + color: $text-color; background: #FFF !important; font-weight: bold; border: 1px solid #EEE; @@ -52,32 +52,33 @@ } i { - color: #444; + color: $text-color; } } } .nav-sidebar li { + text-shadow: 0 1px 1px $border-color; + &.separate-item { - border-top: 1px solid #ddd; + border-top: 1px solid $border-color; padding-top: 10px; margin-top: 10px; } a { - color: #555; + color: #3b5a5b; display: block; text-decoration: none; padding: 8px 15px; font-size: 13px; line-height: 20px; - text-shadow: 0 1px 2px #FFF; padding-left: 20px; &:hover { text-decoration: none; - color: #333; - background: #EEE; + color: $text-color; + background: #f2f6f7; } &:active, &:focus { @@ -86,7 +87,7 @@ i { width: 20px; - color: #888; + color: $gray-dark; margin-right: 23px; } } @@ -156,18 +157,17 @@ position: fixed; top: 46px; padding: 5px 13px 5px 13px; - left: 197px; + left: 198px; font-size: 13px; - background: #EEE; + background: transparent; color: black; - border-left: 1px solid rgba(0,0,0,0.035); - border-right: 1px solid rgba(0,0,0,0.035); + border-left: 1px solid $border-color; + border-bottom: 1px solid $border-color; } .collapse-nav a:hover { text-decoration: none; - color: #333; - background: #eaeaea; + background: #f2f6f7; } @media (max-width: $screen-md-max) { diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index 4d940ee6b29..80190424c1b 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -4,7 +4,6 @@ */ .page-title { margin-top: 0px; - color: #333; line-height: 1.5; font-weight: normal; margin-bottom: 5px; @@ -16,7 +15,7 @@ pre { &.dark { background: #333; - color: #f5f5f5; + color: $background-color; } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 5a9f93dc03d..83f65913ee6 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -89,7 +89,7 @@ margin: 0px; padding: 0px; border: none; - background: #F5F5F5; + background: $background-color; color: rgba(0,0,0,0.3); padding: 0px 5px; border-right: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 851f126318d..759ba6b1c22 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -37,7 +37,7 @@ } .editor-ref { - background: #f5f5f5; + background: $background-color; padding: 11px 15px; border-right: 1px solid #CCC; display: inline-block; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 3e9e36e477e..480cc2c50a9 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -188,7 +188,7 @@ li a { font-size: 13px; padding: 5px 10px; - background: rgba(0,0,0,0.045); + background: $background-color; margin-left: 4px; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d41e34caba1..fe5667a587f 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -129,7 +129,7 @@ font-size: 15px; border-bottom: 1px solid #BBB; color: #777; - background-color: #F5F5F5; + background-color: $background-color; &.ci-success { color: $gl-success; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index ce02cdb1652..b0e6a05fa06 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -27,7 +27,7 @@ } &.selected { td { - background: #f5f5f5; + background: $background-color; border-top: 1px solid #EEE; border-bottom: 1px solid #EEE; } diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml index f53de2de287..d2df398ed4a 100644 --- a/app/views/notify/project_was_moved_email.html.haml +++ b/app/views/notify/project_was_moved_email.html.haml @@ -6,10 +6,10 @@ = @project.name_with_namespace %p To update the remote url in your local repository run (for ssh): -%p{ style: "background:#f5f5f5; padding:10px; border:1px solid #ddd" } +%p{ style: "background:$background-color; padding:10px; border:1px solid #ddd" } git remote set-url origin #{@project.ssh_url_to_repo} %p or for http(s): -%p{ style: "background:#f5f5f5; padding:10px; border:1px solid #ddd" } +%p{ style: "background:$background-color; padding:10px; border:1px solid #ddd" } git remote set-url origin #{@project.http_url_to_repo} %br -- GitLab From 61c06c5e1ae87914343312b956d5b289d568b71f Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 15 Mar 2015 12:54:36 -0600 Subject: [PATCH 1389/1609] Change permissions on backup files Use more restrictive permissions for backup tar files and for the db, uploads, and repositories directories inside the tar files. --- CHANGELOG | 1 + lib/backup/manager.rb | 18 +++++--- spec/tasks/gitlab/backup_rake_spec.rb | 63 ++++++++++++++++++++++++--- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c4e47346fd8..4787117cbbc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -34,6 +34,7 @@ v 7.9.0 (unreleased) - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options + - Restrict permissions on backup files - Add grouped milestones from all projects to dashboard. - Web hook sends pusher email as well as commiter - Add Bitbucket omniauth provider. diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index ab8db4e9837..1a4f28d106d 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -11,22 +11,28 @@ module Backup s[:tar_version] = tar_version tar_file = "#{s[:backup_created_at].to_i}_gitlab_backup.tar" + orig_pwd = Dir.pwd Dir.chdir(Gitlab.config.backup.path) File.open("#{Gitlab.config.backup.path}/backup_information.yml", "w+") do |file| file << s.to_yaml.gsub(/^---\n/,'') end + FileUtils.chmod_R(0700, %w{db uploads repositories}) + # create archive $progress.print "Creating backup archive: #{tar_file} ... " + orig_umask = File.umask(0077) if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) $progress.puts "done".green else puts "creating archive #{tar_file} failed".red abort 'Backup failed' end + File.umask(orig_umask) upload(tar_file) + Dir.chdir(orig_pwd) end def upload(tar_file) @@ -51,11 +57,13 @@ module Backup def cleanup $progress.print "Deleting tmp directories ... " - if Kernel.system('rm', '-rf', *BACKUP_CONTENTS) - $progress.puts "done".green - else - puts "deleting tmp directory failed".red - abort 'Backup failed' + BACKUP_CONTENTS.each do |dir| + if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir)) + $progress.puts "done".green + else + puts "deleting tmp directory '#{dir}' failed".red + abort 'Backup failed' + end end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 60942cc95fc..8a411b7720a 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -10,17 +10,17 @@ describe 'gitlab:app namespace rake task' do Rake::Task.define_task :environment end + def run_rake_task(task_name) + Rake::Task[task_name].reenable + Rake.application.invoke_task task_name + end + describe 'backup_restore' do before do # avoid writing task output to spec progress allow($stdout).to receive :write end - let :run_rake_task do - Rake::Task["gitlab:backup:restore"].reenable - Rake.application.invoke_task "gitlab:backup:restore" - end - context 'gitlab version' do before do Dir.stub glob: [] @@ -36,7 +36,9 @@ describe 'gitlab:app namespace rake task' do it 'should fail on mismatch' do YAML.stub load_file: {gitlab_version: "not #{gitlab_version}" } - expect { run_rake_task }.to raise_error SystemExit + expect { run_rake_task('gitlab:backup:restore') }.to( + raise_error SystemExit + ) end it 'should invoke restoration on mach' do @@ -44,9 +46,56 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke expect(Rake::Task["gitlab:backup:repo:restore"]).to receive :invoke expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke - expect { run_rake_task }.to_not raise_error + expect { run_rake_task('gitlab:backup:restore') }.to_not raise_error end end end # backup_restore task + + describe 'backup_create' do + def tars_glob + Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) + end + + before :all do + # Record the existing backup tars so we don't touch them + existing_tars = tars_glob + + # Redirect STDOUT and run the rake task + orig_stdout = $stdout + $stdout = StringIO.new + run_rake_task('gitlab:backup:create') + $stdout = orig_stdout + + @backup_tar = (tars_glob - existing_tars).first + end + + after :all do + FileUtils.rm(@backup_tar) + end + + it 'should set correct permissions on the tar file' do + expect(File.exist?(@backup_tar)).to be_truthy + expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600') + end + + it 'should set correct permissions on the tar contents' do + tar_contents, exit_status = Gitlab::Popen.popen( + %W{tar -tvf #{@backup_tar} db uploads repositories} + ) + expect(exit_status).to eq(0) + expect(tar_contents).to match('db/') + expect(tar_contents).to match('uploads/') + expect(tar_contents).to match('repositories/') + expect(tar_contents).not_to match(/^.{4,9}[rwx]/) + end + + it 'should delete temp directories' do + temp_dirs = Dir.glob( + File.join(Gitlab.config.backup.path, '{db,repositories,uploads}') + ) + + expect(temp_dirs).to be_empty + end + end # backup_create task end # gitlab:app namespace -- GitLab From f8c8d988e85bd62abbba3a0433e5a74c47b6245d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 18:11:27 -0700 Subject: [PATCH 1390/1609] Fix scroll for last push widget --- app/assets/stylesheets/pages/events.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 3e9e36e477e..bbd12172672 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -154,10 +154,12 @@ overflow: auto; .event-last-push-text { @include str-truncated(100%); + padding: 5px 0; + font-size: 13px; float:left; margin-right: -150px; padding-right: 150px; - line-height: 24px; + line-height: 20px; } } -- GitLab From aa4691acacade89cb811221a64829d290badd343 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 18:38:10 -0700 Subject: [PATCH 1391/1609] Fix email template css for project moved emails --- app/views/notify/project_was_moved_email.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml index d2df398ed4a..3cd759f1f57 100644 --- a/app/views/notify/project_was_moved_email.html.haml +++ b/app/views/notify/project_was_moved_email.html.haml @@ -6,10 +6,10 @@ = @project.name_with_namespace %p To update the remote url in your local repository run (for ssh): -%p{ style: "background:$background-color; padding:10px; border:1px solid #ddd" } +%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" } git remote set-url origin #{@project.ssh_url_to_repo} %p or for http(s): -%p{ style: "background:$background-color; padding:10px; border:1px solid #ddd" } +%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" } git remote set-url origin #{@project.http_url_to_repo} %br -- GitLab From 0e70fbe3338ad18e6d08fe1c5d487582f48642d4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 19:01:36 -0700 Subject: [PATCH 1392/1609] Move CHANGELOG item to correct milestone --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bc341ae1b8b..09b60e8e54a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Add a service to support external wikis (Hannes Rosenögger) v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) @@ -14,7 +15,6 @@ v 7.9.0 (unreleased) - Fix merge request URL passed to Webhooks. (Stan Hu) - Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu) - Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu) - - Add a service to support external wikis (Hannes Rosenögger) - Move labels/milestones tabs to sidebar - Upgrade Rails gem to version 4.1.9. - Improve error messages for file edit failures -- GitLab From 066fb568e7b8ee7d04e934a82171fece48768034 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 19:06:43 -0700 Subject: [PATCH 1393/1609] Align services like in EE for easier merging --- app/models/service.rb | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/app/models/service.rb b/app/models/service.rb index 33734e97c55..918af88b393 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -112,7 +112,7 @@ class Service < ActiveRecord::Base def async_execute(data) return unless supported_events.include?(data[:object_kind]) - + Sidekiq::Client.enqueue(ProjectServiceWorker, id, data) end @@ -121,9 +121,27 @@ class Service < ActiveRecord::Base end def self.available_services_names - %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla asana - emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira - redmine custom_issue_tracker irker) + %w( + gitlab_ci + campfire + hipchat + pivotaltracker + flowdock + assembla + asana + emails_on_push + gemnasium + slack + pushover + buildbox + bamboo + teamcity + jira + redmine + custom_issue_tracker + irker + external_wiki_service + ) end def self.create_from_template(project_id, template) -- GitLab From 2aaf685305e2af63aeb23e41e140bd05165f01f7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 22:25:25 -0700 Subject: [PATCH 1394/1609] More use of sass variables. Reject flatly gray colors --- app/assets/stylesheets/base/gl_variables.scss | 20 +++++++------------ app/assets/stylesheets/base/variables.scss | 5 +++-- app/assets/stylesheets/generic/forms.scss | 4 ++-- .../stylesheets/generic/nav_sidebar.scss | 8 +++----- app/assets/stylesheets/generic/timeline.scss | 4 ++-- 5 files changed, 17 insertions(+), 24 deletions(-) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index ce82ad80318..2e60f3dc7eb 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -15,12 +15,6 @@ // $gray: lighten($gray-base, 33.5%) // #555 // $gray-light: lighten($gray-base, 46.7%) // #777 // $gray-lighter: lighten($gray-base, 93.5%) // #eee -$gray-base: #000; -$gray-darker: lighten($gray-base, 13.5%); // #222 -$gray-dark: #7b8a8b; // #333 -$gray: #95a5a6; // #555 -$gray-light: #b4bcc2; // #999 -$gray-lighter: #ecf0f1; // #eee $brand-primary: $gl-primary; $brand-success: $gl-success; @@ -36,7 +30,7 @@ $brand-danger: $gl-danger; //** Background color for ``. // $body-bg: #fff //** Global text color on ``. -$text-color: $brand-primary; +$text-color: $gl-text-color; //** Global textual link color. $link-color: $gl-link-color; @@ -763,8 +757,8 @@ $panel-default-heading-bg: $background-color; // //## -$well-bg: $gray-lighter; -$well-border: transparent; +//$well-bg: $gray-lighter; +//$well-border: transparent; //== Badges @@ -838,9 +832,9 @@ $code-bg: #f9f2f4; $kbd-color: #fff; $kbd-bg: #333; -$pre-bg: $gray-lighter; -$pre-color: $text-color; -$pre-border-color: #ccc; +//$pre-bg: $gray-lighter; +//$pre-color: $text-color; +//$pre-border-color: #ccc; // $pre-scrollable-max-height: 340px @@ -855,7 +849,7 @@ $pre-border-color: #ccc; //** Abbreviations and acronyms border color // $abbr-border-color: $gray-light //** Headings small color -$headings-small-color: $gray-dark; +//$headings-small-color: $gray-dark; //** Blockquote small color // $blockquote-small-color: $gray-light //** Blockquote font size diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 4e2c64aa132..7804b748377 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -1,5 +1,6 @@ $style_color: #474D57; $hover: #FFF3EB; +$gl-text-color: #222222; $gl-link-color: #446e9b; $nprogress-color: #c0392b; $gl-font-size: 14px; @@ -8,8 +9,8 @@ $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; $code_line_height: 1.5; -$border-color: #dce4ec; -$background-color: #ECF0F1; +$border-color: #E5E5E5; +$background-color: #f5f5f5; /* * State colors: diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index 19bc11086e9..31fe5a03f37 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -29,8 +29,8 @@ fieldset legend { padding: 17px 20px 18px; margin-top: 18px; margin-bottom: 18px; - background-color: #ecf0f1; - border-top: 1px solid #e5e5e5; + background-color: $background-color; + border-top: 1px solid $border-color; } @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/generic/nav_sidebar.scss b/app/assets/stylesheets/generic/nav_sidebar.scss index bb890171985..3bcb7b81333 100644 --- a/app/assets/stylesheets/generic/nav_sidebar.scss +++ b/app/assets/stylesheets/generic/nav_sidebar.scss @@ -58,8 +58,6 @@ } .nav-sidebar li { - text-shadow: 0 1px 1px $border-color; - &.separate-item { border-top: 1px solid $border-color; padding-top: 10px; @@ -67,7 +65,7 @@ } a { - color: #3b5a5b; + color: $gray; display: block; text-decoration: none; padding: 8px 15px; @@ -78,7 +76,7 @@ &:hover { text-decoration: none; color: $text-color; - background: #f2f6f7; + background: $border-color; } &:active, &:focus { @@ -87,7 +85,7 @@ i { width: 20px; - color: $gray-dark; + color: $gray-light; margin-right: 23px; } } diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/generic/timeline.scss index f92a79f7a5f..97831eb7c27 100644 --- a/app/assets/stylesheets/generic/timeline.scss +++ b/app/assets/stylesheets/generic/timeline.scss @@ -54,7 +54,7 @@ .timeline-content { position: relative; - background: #f5f5f6; + background: $background-color; padding: 10px 15px; margin-left: 60px; @@ -70,7 +70,7 @@ height: 0; border-style: solid; border-width: 9px 9px 9px 0; - border-color: transparent #f5f5f6 transparent transparent; + border-color: transparent $background-color transparent transparent; left: 0; top: 10px; margin-left: -9px; -- GitLab From 293553747aab45ce2d63e73a366a2d4d1c80363b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 22:31:45 -0700 Subject: [PATCH 1395/1609] Cleanup bootstrap variable example file --- app/assets/stylesheets/base/gl_variables.scss | 745 +----------------- 1 file changed, 4 insertions(+), 741 deletions(-) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index 2e60f3dc7eb..aa5fca7e66c 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -1,5 +1,5 @@ // Override Bootstrap variables here (defaults from bootstrap-sass v3.3.3): - +// For all variables see https://github.com/twbs/bootstrap-sass/blob/master/templates/project/_bootstrap-variables.sass // // Variables // -------------------------------------------------- @@ -25,68 +25,17 @@ $brand-danger: $gl-danger; //== Scaffolding // -//## Settings for some of the most global styles. - -//** Background color for ``. -// $body-bg: #fff -//** Global text color on ``. $text-color: $gl-text-color; - -//** Global textual link color. $link-color: $gl-link-color; -//** Link hover color set via `darken()` function. -// $link-hover-color: darken($link-color, 15%) -//** Link hover decoration. -// $link-hover-decoration: underline //== Typography // //## Font, line-height, and color for body text, headings, and more. -// $font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif -// $font-family-serif: Georgia, "Times New Roman", Times, serif -//** Default monospace fonts for ``, ``, and `

    `.
    -// $font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace
    -// $font-family-base:        $font-family-sans-serif
    -
    -$font-size-base:          $gl-font-size;
    -// $font-size-large:         ceil(($font-size-base * 1.25)) // ~18px
    -// $font-size-small:         ceil(($font-size-base * 0.85)) // ~12px
    -
    -// $font-size-h1:            floor(($font-size-base * 2.6)) // ~36px
    -// $font-size-h2:            floor(($font-size-base * 2.15)) // ~30px
    -// $font-size-h3:            ceil(($font-size-base * 1.7)) // ~24px
    -// $font-size-h4:            ceil(($font-size-base * 1.25)) // ~18px
    -// $font-size-h5:            $font-size-base
    -// $font-size-h6:            ceil(($font-size-base * 0.85)) // ~12px
    -
    -//** Unit-less `line-height` for use in components like buttons.
    -// $line-height-base:        1.428571429 // 20/14
    -//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
    -// $line-height-computed:    floor(($font-size-base * $line-height-base)) // ~20px
    -
    -//** By default, this inherits from the ``.
    -// $headings-font-family:    inherit
    -// $headings-font-weight:    500
    -// $headings-line-height:    1.1
    -// $headings-color:          inherit
    -
    -
    -//== Iconography
    -//
    -//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
    -
    -//** Load fonts from this directory.
    -
    -// [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
    -// [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
    -// $icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../fonts/bootstrap/")
    -
    -//** File name for all font files.
    -// $icon-font-name:          "glyphicons-halflings-regular"
    -//** Element ID within SVG icon file.
    -// $icon-font-svg-id:        "glyphicons_halflingsregular"
    +$font-family-sans-serif: $regular_font;
    +$font-family-monospace:  $monospace_font;
    +$font-size-base:         $gl-font-size;
     
     
     //== Components
    @@ -96,348 +45,15 @@ $font-size-base:          $gl-font-size;
     $padding-base-vertical:     6px;
     $padding-base-horizontal:   14px;
     
    -// $padding-large-vertical:    10px
    -// $padding-large-horizontal:  16px
    -
    -// $padding-small-vertical:    5px
    -// $padding-small-horizontal:  10px
    -
    -// $padding-xs-vertical:       1px
    -// $padding-xs-horizontal:     5px
    -
    -// $line-height-large:         1.3333333 // extra decimals for Win 8.1 Chrome
    -// $line-height-small:         1.5
    -
    -// $border-radius-base:        4px
    -// $border-radius-large:       6px
    -// $border-radius-small:       3px
    -
    -//** Global color for active items (e.g., navs or dropdowns).
    -// $component-active-color:    #fff
    -//** Global background color for active items (e.g., navs or dropdowns).
    -// $component-active-bg:       $brand-primary
    -
    -//** Width of the `border` for generating carets that indicator dropdowns.
    -// $caret-width-base:          4px
    -//** Carets increase slightly in size for larger components.
    -// $caret-width-large:         5px
    -
    -
    -//== Tables
    -//
    -//## Customizes the `.table` component with basic values, each used across all table variations.
    -
    -//** Padding for `
    `s and ``s. -// $table-cell-padding: 8px -//** Padding for cells in `.table-condensed`. -// $table-condensed-cell-padding: 5px - -//** Default background color used for all tables. -// $table-bg: transparent -//** Background color used for `.table-striped`. -// $table-bg-accent: #f9f9f9 -//** Background color used for `.table-hover`. -// $table-bg-hover: #f5f5f5 -// $table-bg-active: $table-bg-hover - -//** Border color for table and cell borders. -// $table-border-color: #ddd - - -//== Buttons -// -//## For each of Bootstrap's buttons, define text, background and border color. - -// $btn-font-weight: normal - -// $btn-default-color: #333 -// $btn-default-bg: #fff -// $btn-default-border: #ccc - -// $btn-primary-color: #fff -// $btn-primary-bg: $brand-primary -// $btn-primary-border: darken($btn-primary-bg, 5%) - -// $btn-success-color: #fff -// $btn-success-bg: $brand-success -// $btn-success-border: darken($btn-success-bg, 5%) - -// $btn-info-color: #fff -// $btn-info-bg: $brand-info -// $btn-info-border: darken($btn-info-bg, 5%) - -// $btn-warning-color: #fff -// $btn-warning-bg: $brand-warning -// $btn-warning-border: darken($btn-warning-bg, 5%) - -// $btn-danger-color: #fff -// $btn-danger-bg: $brand-danger -// $btn-danger-border: darken($btn-danger-bg, 5%) - -// $btn-link-disabled-color: $gray-light - //== Forms // //## -//** `` background color -// $input-bg: #fff -//** `` background color -// $input-bg-disabled: $gray-lighter - -//** Text color for ``s $input-color: $text-color; -//** `` border color $input-border: #dce4ec; - -// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4 -//** Default `.form-control` border radius -// This has no effect on ``s in CSS. -// $input-border-radius: $border-radius-base -//** Large `.form-control` border radius -// $input-border-radius-large: $border-radius-large -//** Small `.form-control` border radius -// $input-border-radius-small: $border-radius-small - -//** Border color for inputs on focus $input-border-focus: $brand-info; - -//** Placeholder text color -// $input-color-placeholder: #999 - -//** Default `.form-control` height -// $input-height-base: ($line-height-computed + ($padding-base-vertical * 2) + 2) -//** Large `.form-control` height -// $input-height-large: (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) -//** Small `.form-control` height -// $input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) - $legend-color: $text-color; -// $legend-border-color: #e5e5e5 - -//** Background color for textual input addons -// $input-group-addon-bg: $gray-lighter -//** Border color for textual input addons -// $input-group-addon-border-color: $input-border - -//** Disabled cursor for form controls and buttons. -// $cursor-disabled: not-allowed - - -//== Dropdowns -// -//## Dropdown menu container and contents. - -//** Background for the dropdown menu. -// $dropdown-bg: #fff -//** Dropdown menu `border-color`. -// $dropdown-border: rgba(0,0,0,.15) -//** Dropdown menu `border-color` **for IE8**. -// $dropdown-fallback-border: #ccc -//** Divider color for between dropdown items. -// $dropdown-divider-bg: #e5e5e5 - -//** Dropdown link text color. -// $dropdown-link-color: $gray-dark -//** Hover color for dropdown links. -// $dropdown-link-hover-color: darken($gray-dark, 5%) -//** Hover background for dropdown links. -// $dropdown-link-hover-bg: #f5f5f5 - -//** Active dropdown menu item text color. -// $dropdown-link-active-color: $component-active-color -//** Active dropdown menu item background color. -// $dropdown-link-active-bg: $component-active-bg - -//** Disabled dropdown menu item background color. -// $dropdown-link-disabled-color: $gray-light - -//** Text color for headers within dropdown menus. -// $dropdown-header-color: $gray-light - -//** Deprecated `$dropdown-caret-color` as of v3.1.0 -// $dropdown-caret-color: #000 - - -//-- Z-index master list -// -// Warning: Avoid customizing these values. They're used for a bird's eye view -// of components dependent on the z-axis and are designed to all work together. -// -// Note: These variables are not generated into the Customizer. - -// $zindex-navbar: 1000 -// $zindex-dropdown: 1000 -// $zindex-popover: 1060 -// $zindex-tooltip: 1070 -// $zindex-navbar-fixed: 1030 -// $zindex-modal: 1040 - - -//== Media queries breakpoints -// -//## Define the breakpoints at which your layout will change, adapting to different screen sizes. - -// Extra small screen / phone -//** Deprecated `$screen-xs` as of v3.0.1 -// $screen-xs: 480px -//** Deprecated `$screen-xs-min` as of v3.2.0 -// $screen-xs-min: $screen-xs -//** Deprecated `$screen-phone` as of v3.0.1 -// $screen-phone: $screen-xs-min - -// Small screen / tablet -//** Deprecated `$screen-sm` as of v3.0.1 -// $screen-sm: 768px -// $screen-sm-min: $screen-sm -//** Deprecated `$screen-tablet` as of v3.0.1 -// $screen-tablet: $screen-sm-min - -// Medium screen / desktop -//** Deprecated `$screen-md` as of v3.0.1 -// $screen-md: 992px -// $screen-md-min: $screen-md -//** Deprecated `$screen-desktop` as of v3.0.1 -// $screen-desktop: $screen-md-min - -// Large screen / wide desktop -//** Deprecated `$screen-lg` as of v3.0.1 -// $screen-lg: 1200px -// $screen-lg-min: $screen-lg -//** Deprecated `$screen-lg-desktop` as of v3.0.1 -// $screen-lg-desktop: $screen-lg-min - -// So media queries don't overlap when required, provide a maximum -// $screen-xs-max: ($screen-sm-min - 1) -// $screen-sm-max: ($screen-md-min - 1) -// $screen-md-max: ($screen-lg-min - 1) - - -//== Grid system -// -//## Define your custom responsive grid. - -//** Number of columns in the grid. -// $grid-columns: 12 -//** Padding between columns. Gets divided in half for the left and right. -// $grid-gutter-width: 30px -// Navbar collapse -//** Point at which the navbar becomes uncollapsed. -// $grid-float-breakpoint: $screen-sm-min -//** Point at which the navbar begins collapsing. -// $grid-float-breakpoint-max: ($grid-float-breakpoint - 1) - - -//== Container sizes -// -//## Define the maximum width of `.container` for different screen sizes. - -// Small screen / tablet -// $container-tablet: (720px + $grid-gutter-width) -//** For `$screen-sm-min` and up. -// $container-sm: $container-tablet - -// Medium screen / desktop -// $container-desktop: (940px + $grid-gutter-width) -//** For `$screen-md-min` and up. -// $container-md: $container-desktop - -// Large screen / wide desktop -// $container-large-desktop: (1140px + $grid-gutter-width) -//** For `$screen-lg-min` and up. -// $container-lg: $container-large-desktop - - -//== Navbar -// -//## - -// Basics of a navbar -// $navbar-height: 50px -// $navbar-margin-bottom: $line-height-computed -// $navbar-border-radius: $border-radius-base -// $navbar-padding-horizontal: floor(($grid-gutter-width / 2)) -// $navbar-padding-vertical: (($navbar-height - $line-height-computed) / 2) -// $navbar-collapse-max-height: 340px - -// $navbar-default-color: #777 -// $navbar-default-bg: #f8f8f8 -// $navbar-default-border: darken($navbar-default-bg, 6.5%) - -// Navbar links -// $navbar-default-link-color: #777 -// $navbar-default-link-hover-color: #333 -// $navbar-default-link-hover-bg: transparent -// $navbar-default-link-active-color: #555 -// $navbar-default-link-active-bg: darken($navbar-default-bg, 6.5%) -// $navbar-default-link-disabled-color: #ccc -// $navbar-default-link-disabled-bg: transparent - -// Navbar brand label -// $navbar-default-brand-color: $navbar-default-link-color -// $navbar-default-brand-hover-color: darken($navbar-default-brand-color, 10%) -// $navbar-default-brand-hover-bg: transparent - -// Navbar toggle -// $navbar-default-toggle-hover-bg: #ddd -// $navbar-default-toggle-icon-bar-bg: #888 -// $navbar-default-toggle-border-color: #ddd - - -// Inverted navbar -// Reset inverted navbar basics -// $navbar-inverse-color: lighten($gray-light, 15%) -// $navbar-inverse-bg: #222 -// $navbar-inverse-border: darken($navbar-inverse-bg, 10%) - -// Inverted navbar links -// $navbar-inverse-link-color: lighten($gray-light, 15%) -// $navbar-inverse-link-hover-color: #fff -// $navbar-inverse-link-hover-bg: transparent -// $navbar-inverse-link-active-color: $navbar-inverse-link-hover-color -// $navbar-inverse-link-active-bg: darken($navbar-inverse-bg, 10%) -// $navbar-inverse-link-disabled-color: #444 -// $navbar-inverse-link-disabled-bg: transparent - -// Inverted navbar brand label -// $navbar-inverse-brand-color: $navbar-inverse-link-color -// $navbar-inverse-brand-hover-color: #fff -// $navbar-inverse-brand-hover-bg: transparent - -// Inverted navbar toggle -// $navbar-inverse-toggle-hover-bg: #333 -// $navbar-inverse-toggle-icon-bar-bg: #fff -// $navbar-inverse-toggle-border-color: #333 - - -//== Navs -// -//## - -//=== Shared nav styles -// $nav-link-padding: 10px 15px -// $nav-link-hover-bg: $gray-lighter - -// $nav-disabled-link-color: $gray-light -// $nav-disabled-link-hover-color: $gray-light - -//== Tabs -// $nav-tabs-border-color: #ddd - -// $nav-tabs-link-hover-border-color: $gray-lighter - -// $nav-tabs-active-link-hover-bg: $body-bg -// $nav-tabs-active-link-hover-color: $gray -// $nav-tabs-active-link-hover-border-color: #ddd - -// $nav-tabs-justified-link-border-color: #ddd -// $nav-tabs-justified-active-link-border-color: $body-bg - -//== Pills -// $nav-pills-border-radius: $border-radius-base -// $nav-pills-active-link-hover-bg: $component-active-bg -// $nav-pills-active-link-hover-color: $component-active-color //== Pagination @@ -461,38 +77,10 @@ $pagination-disabled-bg: lighten($brand-success, 15%); $pagination-disabled-border: transparent; -//== Pager -// -//## - -// $pager-bg: $pagination-bg -// $pager-border: $pagination-border -// $pager-border-radius: 15px - -// $pager-hover-bg: $pagination-hover-bg - -// $pager-active-bg: $pagination-active-bg -// $pager-active-color: $pagination-active-color - -// $pager-disabled-color: $pagination-disabled-color - - -//== Jumbotron -// -//## - -// $jumbotron-padding: 30px -// $jumbotron-color: inherit -// $jumbotron-bg: $gray-lighter -// $jumbotron-heading-color: inherit -// $jumbotron-font-size: ceil(($font-size-base * 1.5)) - - //== Form states and alerts // //## Define colors for form feedback states and, by default, alerts. - $state-success-text: #fff; $state-success-bg: $brand-success; $state-success-border: $brand-success; @@ -510,316 +98,22 @@ $state-danger-bg: $brand-danger; $state-danger-border: $brand-danger; -//== Tooltips -// -//## - -//** Tooltip max width -// $tooltip-max-width: 200px -//** Tooltip text color -// $tooltip-color: #fff -//** Tooltip background color -// $tooltip-bg: #000 -// $tooltip-opacity: .9 - -//** Tooltip arrow width -// $tooltip-arrow-width: 5px -//** Tooltip arrow color -// $tooltip-arrow-color: $tooltip-bg - - -//== Popovers -// -//## - -//** Popover body background color -// $popover-bg: #fff -//** Popover maximum width -// $popover-max-width: 276px -//** Popover border color -// $popover-border-color: rgba(0,0,0,.2) -//** Popover fallback border color -// $popover-fallback-border-color: #ccc - -//** Popover title background color -// $popover-title-bg: darken($popover-bg, 3%) - -//** Popover arrow width -// $popover-arrow-width: 10px -//** Popover arrow color -// $popover-arrow-color: $popover-bg - -//** Popover outer arrow width -// $popover-arrow-outer-width: ($popover-arrow-width + 1) -//** Popover outer arrow color -// $popover-arrow-outer-color: fade_in($popover-border-color, 0.05) -//** Popover outer arrow fallback color -// $popover-arrow-outer-fallback-color: darken($popover-fallback-border-color, 20%) - - -//== Labels -// -//## - -//** Default label background color -// $label-default-bg: $gray-light -//** Primary label background color -// $label-primary-bg: $brand-primary -//** Success label background color -// $label-success-bg: $brand-success -//** Info label background color -// $label-info-bg: $brand-info -//** Warning label background color -// $label-warning-bg: $brand-warning -//** Danger label background color -// $label-danger-bg: $brand-danger - -//** Default label text color -// $label-color: #fff -//** Default text color of a linked label -// $label-link-hover-color: #fff - - -//== Modals -// -//## - -//** Padding applied to the modal body -// $modal-inner-padding: 15px - -//** Padding applied to the modal title -// $modal-title-padding: 15px -//** Modal title line-height -// $modal-title-line-height: $line-height-base - -//** Background color of modal content area -// $modal-content-bg: #fff -//** Modal content border color -// $modal-content-border-color: rgba(0,0,0,.2) -//** Modal content border color **for IE8** -// $modal-content-fallback-border-color: #999 - -//** Modal backdrop background color -// $modal-backdrop-bg: #000 -//** Modal backdrop opacity -// $modal-backdrop-opacity: .5 -//** Modal header border color -// $modal-header-border-color: #e5e5e5 -//** Modal footer border color -// $modal-footer-border-color: $modal-header-border-color - -// $modal-lg: 900px -// $modal-md: 600px -// $modal-sm: 300px - - //== Alerts // //## Define alert colors, border radius, and padding. -// $alert-padding: 15px $alert-border-radius: 0; -// $alert-link-font-weight: bold - -// $alert-success-bg: $state-success-bg -// $alert-success-text: $state-success-text -// $alert-success-border: $state-success-border - -// $alert-info-bg: $state-info-bg -// $alert-info-text: $state-info-text -// $alert-info-border: $state-info-border - -// $alert-warning-bg: $state-warning-bg -// $alert-warning-text: $state-warning-text -// $alert-warning-border: $state-warning-border - -// $alert-danger-bg: $state-danger-bg -// $alert-danger-text: $state-danger-text -// $alert-danger-border: $state-danger-border - - -//== Progress bars -// -//## - -//** Background color of the whole progress component -// $progress-bg: #f5f5f5 -//** Progress bar text color -// $progress-bar-color: #fff -//** Variable for setting rounded corners on progress bar. -// $progress-border-radius: $border-radius-base - -//** Default progress bar color -// $progress-bar-bg: $brand-primary -//** Success progress bar color -// $progress-bar-success-bg: $brand-success -//** Warning progress bar color -// $progress-bar-warning-bg: $brand-warning -//** Danger progress bar color -// $progress-bar-danger-bg: $brand-danger -//** Info progress bar color -// $progress-bar-info-bg: $brand-info - - -//== List group -// -//## - -//** Background color on `.list-group-item` -// $list-group-bg: #fff -//** `.list-group-item` border color -// $list-group-border: #ddd -//** List group border radius -// $list-group-border-radius: $border-radius-base - -//** Background color of single list items on hover -// $list-group-hover-bg: #f5f5f5 -//** Text color of active list items -// $list-group-active-color: $component-active-color -//** Background color of active list items -// $list-group-active-bg: $component-active-bg -//** Border color of active list elements -// $list-group-active-border: $list-group-active-bg -//** Text color for content within active list items -// $list-group-active-text-color: lighten($list-group-active-bg, 40%) - -//** Text color of disabled list items -// $list-group-disabled-color: $gray-light -//** Background color of disabled list items -// $list-group-disabled-bg: $gray-lighter -//** Text color for content within disabled list items -// $list-group-disabled-text-color: $list-group-disabled-color - -// $list-group-link-color: #555 -// $list-group-link-hover-color: $list-group-link-color -// $list-group-link-heading-color: #333 //== Panels // //## -// $panel-bg: #fff -// $panel-body-padding: 15px -// $panel-heading-padding: 10px 15px -// $panel-footer-padding: $panel-heading-padding $panel-border-radius: 0; - -//** Border color for elements within panels -// $panel-inner-border: #ddd -// $panel-footer-bg: #f5f5f5 - $panel-default-text: $text-color; $panel-default-border: $border-color; $panel-default-heading-bg: $background-color; -// $panel-primary-text: #fff -// $panel-primary-border: $brand-primary -// $panel-primary-heading-bg: $brand-primary - -// $panel-success-text: $state-success-text -// $panel-success-border: $state-success-border -// $panel-success-heading-bg: $state-success-bg - -// $panel-info-text: $state-info-text -// $panel-info-border: $state-info-border -// $panel-info-heading-bg: $state-info-bg - -// $panel-warning-text: $state-warning-text -// $panel-warning-border: $state-warning-border -// $panel-warning-heading-bg: $state-warning-bg - -// $panel-danger-text: $state-danger-text -// $panel-danger-border: $state-danger-border -// $panel-danger-heading-bg: $state-danger-bg - - -//== Thumbnails -// -//## - -//** Padding around the thumbnail image -// $thumbnail-padding: 4px -//** Thumbnail background color -// $thumbnail-bg: $body-bg -//** Thumbnail border color -// $thumbnail-border: #ddd -//** Thumbnail border radius -// $thumbnail-border-radius: $border-radius-base - -//** Custom text color for thumbnail captions -// $thumbnail-caption-color: $text-color -//** Padding around the thumbnail caption -// $thumbnail-caption-padding: 9px - - -//== Wells -// -//## - -//$well-bg: $gray-lighter; -//$well-border: transparent; - - -//== Badges -// -//## - -// $badge-color: #fff -//** Linked badge text color on hover -// $badge-link-hover-color: #fff -// $badge-bg: $gray-light - -//** Badge text color in active nav link -// $badge-active-color: $link-color -//** Badge background color in active nav link -// $badge-active-bg: #fff - -// $badge-font-weight: bold -// $badge-line-height: 1 -// $badge-border-radius: 10px - - -//== Breadcrumbs -// -//## - -// $breadcrumb-padding-vertical: 8px -// $breadcrumb-padding-horizontal: 15px -//** Breadcrumb background color -// $breadcrumb-bg: #f5f5f5 -//** Breadcrumb text color -// $breadcrumb-color: #ccc -//** Text color of current page in the breadcrumb -// $breadcrumb-active-color: $gray-light -//** Textual separator for between breadcrumb elements -// $breadcrumb-separator: "/" - - -//== Carousel -// -//## - -// $carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6) - -// $carousel-control-color: #fff -// $carousel-control-width: 15% -// $carousel-control-opacity: .5 -// $carousel-control-font-size: 20px - -// $carousel-indicator-active-bg: #fff -// $carousel-indicator-border-color: #fff - -// $carousel-caption-color: #fff - - -//== Close -// -//## - -// $close-font-weight: bold -// $close-color: #000 -// $close-text-shadow: 0 1px 0 #fff //== Code @@ -831,34 +125,3 @@ $code-bg: #f9f2f4; $kbd-color: #fff; $kbd-bg: #333; - -//$pre-bg: $gray-lighter; -//$pre-color: $text-color; -//$pre-border-color: #ccc; -// $pre-scrollable-max-height: 340px - - -//== Type -// -//## - -//** Horizontal offset for forms and lists. -// $component-offset-horizontal: 180px -//** Text muted color -// $text-muted: $gray-light -//** Abbreviations and acronyms border color -// $abbr-border-color: $gray-light -//** Headings small color -//$headings-small-color: $gray-dark; -//** Blockquote small color -// $blockquote-small-color: $gray-light -//** Blockquote font size -// $blockquote-font-size: ($font-size-base * 1.25) -//** Blockquote border color -// $blockquote-border-color: $gray-lighter -//** Page header border color -// $page-header-border-color: $gray-lighter -//** Width of horizontal description list titles -// $dl-horizontal-offset: $component-offset-horizontal -//** Horizontal line color. -// $hr-border: $gray-lighter -- GitLab From 521785157b1f6f729e77fdad2ec83492120e8c7c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 22:48:00 -0700 Subject: [PATCH 1396/1609] Remove to old GitLab colors for succes and primary --- app/assets/stylesheets/base/gl_variables.scss | 2 +- app/assets/stylesheets/base/variables.scss | 9 ++------- app/assets/stylesheets/generic/lists.scss | 4 ++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index aa5fca7e66c..17b5622d74a 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -51,7 +51,7 @@ $padding-base-horizontal: 14px; //## $input-color: $text-color; -$input-border: #dce4ec; +$input-border: #DDD; $input-border-focus: $brand-info; $legend-color: $text-color; diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index 7804b748377..596376c3970 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -15,17 +15,12 @@ $background-color: #f5f5f5; /* * State colors: */ -$gl-success: #019875; -$gl-danger: #d9534f; $gl-primary: #446e9b; +$gl-success: #019875; $gl-info: #029ACF; $gl-warning: #EB9532; +$gl-danger: #d9534f; -$gl-primary: #2C3E50; -$gl-success: #18BC9C; -$gl-info: #3498DB; -$gl-warning: #F39C12; -$gl-danger: #E74C3C; /* * Commit Diff Colors */ diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/generic/lists.scss index 85176fec0dc..08bf6e943d2 100644 --- a/app/assets/stylesheets/generic/lists.scss +++ b/app/assets/stylesheets/generic/lists.scss @@ -61,7 +61,7 @@ p { padding-top: 1px; margin: 0; - color: #222; + color: $gray-dark; img { position: relative; top: 3px; @@ -74,7 +74,7 @@ } .row_title { - color: $text-color; + color: $gray-dark; &:hover { color: $text-color; -- GitLab From 63d0bf1f5e45f46ed346425585f258fc22acee8a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 17 Mar 2015 22:59:45 -0700 Subject: [PATCH 1397/1609] Fix external wiki service --- app/models/project.rb | 2 +- app/models/service.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 2d198de6611..c50b8a12621 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -89,11 +89,11 @@ class Project < ActiveRecord::Base has_one :redmine_service, dependent: :destroy has_one :custom_issue_tracker_service, dependent: :destroy has_one :gitlab_issue_tracker_service, dependent: :destroy + has_one :external_wiki_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link - has_one :external_wiki_service, dependent: :destroy # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' # Merge requests from source project should be kept when source project was removed diff --git a/app/models/service.rb b/app/models/service.rb index 918af88b393..f54ad19666b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -140,7 +140,7 @@ class Service < ActiveRecord::Base redmine custom_issue_tracker irker - external_wiki_service + external_wiki ) end -- GitLab From 847bd0d0e5bd5bf5c8dafa38de2f420da902bdd6 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 18 Mar 2015 02:16:10 -0400 Subject: [PATCH 1398/1609] fix public issue --- .../projects/issues/_issue_context.html.haml | 27 ++++++++++--------- .../merge_requests/show/_context.html.haml | 27 ++++++++++--------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index cb4846a41d1..d43ce0aa293 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -27,19 +27,20 @@ = hidden_field_tag :issue_context = f.submit class: 'btn' - %div.prepend-top-20.clearfix - .issuable-context-title - %label - Subscription: - %button.btn.btn-block.subscribe-button - %i.fa.fa-eye - %span= @issue.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" - - subscribtion_status = @issue.subscribed?(current_user) ? "subscribed" : "unsubscribed" - .subscription-status{"data-status" => subscribtion_status} - .description-block.unsubscribed{class: ( "hidden" if @issue.subscribed?(current_user) )} - You're not receiving notifications from this thread. - .description-block.subscribed{class: ( "hidden" unless @issue.subscribed?(current_user) )} - You're receiving notifications because you're subscribed to this thread. + - if current_user + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Subscription: + %button.btn.btn-block.subscribe-button + %i.fa.fa-eye + %span= @issue.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" + - subscribtion_status = @issue.subscribed?(current_user) ? "subscribed" : "unsubscribed" + .subscription-status{"data-status" => subscribtion_status} + .description-block.unsubscribed{class: ( "hidden" if @issue.subscribed?(current_user) )} + You're not receiving notifications from this thread. + .description-block.subscribed{class: ( "hidden" unless @issue.subscribed?(current_user) )} + You're receiving notifications because you're subscribed to this thread. :coffeescript $ -> diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index 753c7e0e611..14ad89a2000 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -29,19 +29,20 @@ = hidden_field_tag :merge_request_context = f.submit class: 'btn' - %div.prepend-top-20.clearfix - .issuable-context-title - %label - Subscription: - %button.btn.btn-block.subscribe-button - %i.fa.fa-eye - %span= @merge_request.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" - - subscribtion_status = @merge_request.subscribed?(current_user) ? "subscribed" : "unsubscribed" - .subscription-status{"data-status" => subscribtion_status} - .description-block.unsubscribed{class: ( "hidden" if @merge_request.subscribed?(current_user) )} - You're not receiving notifications from this thread. - .description-block.subscribed{class: ( "hidden" unless @merge_request.subscribed?(current_user) )} - You're receiving notifications because you're subscribed to this thread. + - if current_user + %div.prepend-top-20.clearfix + .issuable-context-title + %label + Subscription: + %button.btn.btn-block.subscribe-button + %i.fa.fa-eye + %span= @merge_request.subscribed?(current_user) ? "Unsubscribe" : "Subscribe" + - subscribtion_status = @merge_request.subscribed?(current_user) ? "subscribed" : "unsubscribed" + .subscription-status{"data-status" => subscribtion_status} + .description-block.unsubscribed{class: ( "hidden" if @merge_request.subscribed?(current_user) )} + You're not receiving notifications from this thread. + .description-block.subscribed{class: ( "hidden" unless @merge_request.subscribed?(current_user) )} + You're receiving notifications because you're subscribed to this thread. :coffeescript $ -> -- GitLab From 9e5738b0072f8715d52801f3469be9f3742beb97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Fri, 13 Mar 2015 11:39:26 +0100 Subject: [PATCH 1399/1609] Extend the commit calendar to show the actual commits for a date --- CHANGELOG | 1 + app/assets/javascripts/calendar.js.coffee | 17 ++++- app/assets/stylesheets/generic/calendar.scss | 62 ++++++++++++------- app/controllers/users_controller.rb | 19 ++++++ app/models/project_contributions.rb | 9 +++ app/models/repository.rb | 14 +++++ app/views/users/calendar.html.haml | 3 +- app/views/users/calendar_activities.html.haml | 33 ++++++++++ app/views/users/show.html.haml | 1 + config/routes.rb | 5 +- db/schema.rb | 2 +- lib/gitlab/commits_calendar.rb | 8 +++ spec/controllers/users_controller_spec.rb | 48 +++++++++++--- spec/models/repository_spec.rb | 19 +++++- 14 files changed, 202 insertions(+), 39 deletions(-) create mode 100644 app/views/users/calendar_activities.html.haml diff --git a/CHANGELOG b/CHANGELOG index 09b60e8e54a..b9b9ec964a1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) - Add a service to support external wikis (Hannes Rosenögger) v 7.9.0 (unreleased) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 19ea4ccc4cf..2891a48e249 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -4,7 +4,7 @@ class @calendar day: "numeric" year: "numeric" - constructor: (timestamps, starting_year, starting_month) -> + constructor: (timestamps, starting_year, starting_month, calendar_activities_path) -> cal = new CalHeatMap() cal.init itemName: ["commit"] @@ -26,5 +26,16 @@ class @calendar ] legendCellPadding: 3 onClick: (date, count) -> - return - return + formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() + $(".calendar_commit_activity").fadeOut 400 + $.ajax + url: calendar_activities_path + data: + date: formated_date + cache: false + dataType: "html" + success: (data) -> + $(".user-calendar-activities").html data + $(".calendar_commit_activity").find(".js-toggle-content").hide() + $(".calendar_commit_activity").fadeIn 400 + diff --git a/app/assets/stylesheets/generic/calendar.scss b/app/assets/stylesheets/generic/calendar.scss index 9483b26164e..e2ab7fc51a5 100644 --- a/app/assets/stylesheets/generic/calendar.scss +++ b/app/assets/stylesheets/generic/calendar.scss @@ -1,29 +1,45 @@ -.calendar_onclick_placeholder { - padding: 0 0 2px 0; -} - -.calendar_commit_activity { - padding: 5px 0 0; -} - -.calendar_onclick_second { - font-size: 14px; - display: block; -} - -.calendar_onclick_hr { - padding: 0; - margin: 10px 0; -} +.user-calendar-activities { + + .calendar_commit_activity { + padding: 5px 0 0; + } + + .calendar_onclick_hr { + padding: 0; + margin: 10px 0; + } + + .calendar_commit_date { + color: #999; + } + + .calendar_activity_summary { + font-size: 14px; + } -.calendar_commit_date { - color: #999; -} + .str-truncated { + max-width: 70%; + } -.calendar_activity_summary { - font-size: 14px; + .text-expander { + background: #eee; + color: #555; + padding: 0 5px; + cursor: pointer; + margin-left: 4px; + &:hover { + background-color: #ddd; + } + } + + .commit-row-message { + color: #333; + &:hover { + color: #444; + text-decoration: underline; + } + } } - /** * This overwrites the default values of the cal-heatmap gem */ diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 8a13394dbac..68130eb128c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -32,6 +32,7 @@ class UsersController < ApplicationController def calendar projects = Project.where(id: authorized_projects_ids & @user.contributed_projects_ids) + calendar = Gitlab::CommitsCalendar.new(projects, @user) @timestamps = calendar.timestamps @starting_year = calendar.starting_year @@ -40,6 +41,24 @@ class UsersController < ApplicationController render 'calendar', layout: false end + def calendar_activities + projects = Project.where(id: authorized_projects_ids & @user.contributed_projects_ids) + + date = Date.parse(params[:date]) rescue nil + if date + @calendar_activities = Gitlab::CommitsCalendar.get_commits_for_date(projects, @user, date) + else + @calendar_activities = {} + end + + # get the total number of unique commits + @commit_count = @calendar_activities.values.flatten.map(&:id).uniq.count + + @calendar_date = date + + render 'calendar_activities', layout: false + end + def determine_layout if current_user 'navless' diff --git a/app/models/project_contributions.rb b/app/models/project_contributions.rb index 8ab2d814a94..bfe9928b158 100644 --- a/app/models/project_contributions.rb +++ b/app/models/project_contributions.rb @@ -17,6 +17,15 @@ class ProjectContributions end end + def user_commits_on_date(date) + repository = @project.repository + + if !repository.exists? || repository.empty? + return [] + end + commits = repository.commits_by_user_on_date_log(@user, date) + end + def cache_key "#{Date.today.to_s}-commits-log-#{project.id}-#{user.email}" end diff --git a/app/models/repository.rb b/app/models/repository.rb index 47758b8ad68..7addbca8fb1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -157,6 +157,20 @@ class Repository end end + def commits_by_user_on_date_log(user, date) + # format the date string for git + start_date = date.strftime("%Y-%m-%d 00:00:00") + end_date = date.strftime("%Y-%m-%d 23:59:59") + + author_emails = '(' + user.all_emails.map{ |e| Regexp.escape(e) }.join('|') + ')' + args = %W(git log -E --author=#{author_emails} --after=#{start_date.to_s} --until=#{end_date.to_s} --branches --pretty=format:%h) + commits = Gitlab::Popen.popen(args, path_to_repo).first.split("\n") + + commits.map! do |commit_id| + commit(commit_id) + end + end + def commits_per_day_for_user(user) timestamps_by_user_log(user). group_by { |commit_date| commit_date }. diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 1d1c974da24..d113ceeb753 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -4,5 +4,6 @@ new calendar( #{@timestamps.to_json}, #{@starting_year}, - #{@starting_month} + #{@starting_month}, + '#{user_calendar_activities_path}' ); diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml new file mode 100644 index 00000000000..7c0cecfadb5 --- /dev/null +++ b/app/views/users/calendar_activities.html.haml @@ -0,0 +1,33 @@ +.calendar_commit_activity + %hr + %h4 + Commit Activity + %strong + - if @commit_count == 0 + no + - else + = @commit_count + %span.calendar_commit_date + unique + = 'commit'.pluralize(@commit_count) + on + = @calendar_date.strftime("%b %d, %Y") rescue '' + -unless @commit_count == 0 + %hr + - @calendar_activities.each do |project, commits| + - next if commits.empty? + %div.js-toggle-container + %strong + = pluralize(commits.count, 'commit') + in project + = link_to project.name_with_namespace, project_path(project) + %a.text-expander.js-toggle-button … + %hr + %div.js-toggle-content + - commits.each do |commit| + %span.monospace + = commit.committed_date.strftime("%H:%M") + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" + = link_to commit.message, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message str-truncated" + %br + %hr diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index abd6b229782..6d6beb58711 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -25,6 +25,7 @@ .user-calendar %h4.center.light %i.fa.fa-spinner.fa-spin + .user-calendar-activities %hr %h4 User Activity diff --git a/config/routes.rb b/config/routes.rb index e65ef30afb7..0950bed3cf1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -198,7 +198,10 @@ Gitlab::Application.routes.draw do end get 'u/:username/calendar' => 'users#calendar', as: :user_calendar, - constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } + constraints: { username: /.*/ } + + get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities, + constraints: { username: /.*/ } get '/u/:username' => 'users#show', as: :user, constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } diff --git a/db/schema.rb b/db/schema.rb index e7dccbad4f9..1be3782dcb3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -458,7 +458,6 @@ ActiveRecord::Schema.define(version: 20150313012111) do t.integer "notification_level", default: 1, null: false t.datetime "password_expires_at" t.integer "created_by_id" - t.datetime "last_credential_check_at" t.string "avatar" t.string "confirmation_token" t.datetime "confirmed_at" @@ -466,6 +465,7 @@ ActiveRecord::Schema.define(version: 20150313012111) do t.string "unconfirmed_email" t.boolean "hide_no_ssh_key", default: false t.string "website_url", default: "", null: false + t.datetime "last_credential_check_at" t.string "github_access_token" t.string "gitlab_access_token" t.string "notification_email" diff --git a/lib/gitlab/commits_calendar.rb b/lib/gitlab/commits_calendar.rb index 2f30d238e6b..8963d346b6f 100644 --- a/lib/gitlab/commits_calendar.rb +++ b/lib/gitlab/commits_calendar.rb @@ -22,6 +22,14 @@ module Gitlab end end + def self.get_commits_for_date(projects, user, date) + user_commits = {} + projects.reject(&:forked?).each do |project| + user_commits[project] = ProjectContributions.new(project, user).user_commits_on_date(date) + end + user_commits + end + def starting_year (Time.now - 1.year).strftime("%Y") end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 44225c054f2..7962bcdde71 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -1,27 +1,59 @@ require 'spec_helper' describe UsersController do - let(:user) { create(:user, username: "user1", name: "User 1", email: "user1@gitlab.com") } + let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } before do sign_in(user) end - describe "GET #show" do + describe 'GET #show' do render_views - it "renders the show template" do + it 'renders the show template' do get :show, username: user.username expect(response.status).to eq(200) - expect(response).to render_template("show") + expect(response).to render_template('show') end end - describe "GET #calendar" do - it "renders calendar" do + describe 'GET #calendar' do + it 'renders calendar' do get :calendar, username: user.username - expect(response).to render_template("calendar") + expect(response).to render_template('calendar') end end -end + describe 'GET #calendar_activities' do + include RepoHelpers + let(:project) { create(:project) } + let(:calendar_user) { create(:user, email: sample_commit.author_email) } + let(:commit1) { '0ed8c6c6752e8c6ea63e7b92a517bf5ac1209c80' } + let(:commit2) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } + + before do + allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id]) + project.team << [user, :developer] + end + + it 'assigns @commit_count' do + get :calendar_activities, username: calendar_user.username, date: '2014-07-31' + expect(assigns(:commit_count)).to eq(2) + end + + it 'assigns @calendar_date' do + get :calendar_activities, username: calendar_user.username, date: '2014-07-31' + expect(assigns(:calendar_date)).to eq(Date.parse('2014-07-31')) + end + + it 'assigns @calendar_activities' do + get :calendar_activities, username: calendar_user.username, date: '2014-07-31' + expect(assigns(:calendar_activities).values.flatten.map(&:id)).to eq([commit1, commit2]) + end + + it 'renders calendar_activities' do + get :calendar_activities, username: calendar_user.username + expect(response).to render_template('calendar_activities') + end + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b3a38f6c5b9..0e3e0b167d7 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -29,7 +29,7 @@ describe Repository do subject { repository.timestamps_by_user_log(user) } - it { is_expected.to eq(["2014-08-06", "2014-07-31", "2014-07-31"]) } + it { is_expected.to eq(['2014-08-06', '2014-07-31', '2014-07-31']) } end describe 'multiple emails for user' do @@ -38,7 +38,22 @@ describe Repository do subject { repository.timestamps_by_user_log(user) } - it { is_expected.to eq(["2015-01-10", "2014-08-06", "2014-07-31", "2014-07-31"]) } + it { is_expected.to eq(['2015-01-10', '2014-08-06', '2014-07-31', '2014-07-31']) } + end + end + + context :commits_by_user_on_date_log do + + describe 'single e-mail for user' do + let(:user) { create(:user, email: sample_commit.author_email) } + let(:commit1) { '0ed8c6c6752e8c6ea63e7b92a517bf5ac1209c80' } + let(:commit2) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } + + subject { repository.commits_by_user_on_date_log(user,Date.new(2014, 07, 31)) } + + it 'contains the exepected commits' do + expect(subject.flatten.map(&:id)).to eq([commit1, commit2]) + end end end end -- GitLab From 120f032b1a6c7df431da6821d22c806b6c9bf4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Tue, 17 Mar 2015 15:39:21 +0100 Subject: [PATCH 1400/1609] enable line wrapping by default and remove the checkbox to change it --- CHANGELOG | 1 + .../toggle_diff_line_wrap_behavior.coffee | 14 -------------- app/views/projects/diffs/_file.html.haml | 7 +------ 3 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee diff --git a/CHANGELOG b/CHANGELOG index 09b60e8e54a..f279c7c923a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) - Add a service to support external wikis (Hannes Rosenögger) v 7.9.0 (unreleased) diff --git a/app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee b/app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee deleted file mode 100644 index 691ed4f98ae..00000000000 --- a/app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee +++ /dev/null @@ -1,14 +0,0 @@ -$ -> - # Toggle line wrapping in diff. - # - # %div.diff-file - # %input.js-toggle-diff-line-wrap - # %td.line_content - # - $("body").on "click", ".js-toggle-diff-line-wrap", (e) -> - diffFile = $(@).closest(".diff-file") - if $(@).is(":checked") - diffFile.addClass("diff-wrap-lines") - else - diffFile.removeClass("diff-wrap-lines") - diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 36d98b26712..a9e4d63cd98 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -22,11 +22,6 @@ .diff-btn-group - if blob.text? - - unless params[:view] == 'parallel' - %label - = check_box_tag nil, 1, false, class: 'js-toggle-diff-line-wrap' - Wrap text -   = link_to '#', class: 'js-toggle-diff-comments btn btn-sm' do %i.fa.fa-chevron-down Show/Hide comments @@ -39,7 +34,7 @@ = view_file_btn(@commit.id, diff_file, project) - .diff-content + .diff-content.diff-wrap-lines -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - if blob.text? -- GitLab From bf235053adc60bb0b940ef6fb68a59485bc815aa Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 13:55:39 +0100 Subject: [PATCH 1401/1609] Send EmailsOnPush email when branch or tag is created or deleted. --- CHANGELOG | 1 + app/mailers/emails/projects.rb | 60 ++++++--- .../emails_on_push_service.rb | 2 +- .../notify/repository_push_email.html.haml | 115 +++++++++--------- .../notify/repository_push_email.text.haml | 80 ++++++------ app/workers/emails_on_push_worker.rb | 33 +++-- spec/mailers/notify_spec.rb | 98 ++++++++++++++- 7 files changed, 262 insertions(+), 127 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 09b60e8e54a..7216676d5e8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) - Add a service to support external wikis (Hannes Rosenögger) + - Send EmailsOnPush email when branch or tag is created or deleted. v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index b55129de292..d2165c9f764 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -16,31 +16,59 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, author_id, branch, compare, reverse_compare = false, send_from_committer_email = false, disable_diffs = false) + def repository_push_email(project_id, recipient, author_id, ref, action, compare, reverse_compare = false, send_from_committer_email = false, disable_diffs = false) @project = Project.find(project_id) @author = User.find(author_id) @reverse_compare = reverse_compare @compare = compare - @commits = Commit.decorate(compare.commits) - @diffs = compare.diffs - @branch = Gitlab::Git.ref_name(branch) + @ref_name = Gitlab::Git.ref_name(ref) + @ref_type = Gitlab::Git.tag_ref?(ref) ? "tag" : "branch" + @action = action @disable_diffs = disable_diffs - @subject = "[#{@project.path_with_namespace}][#{@branch}] " + if @compare + @commits = Commit.decorate(compare.commits) + @diffs = compare.diffs + end + + @action_name = + case action + when :create + "pushed new" + when :delete + "deleted" + else + "pushed to" + end + + @subject = "[#{@project.path_with_namespace}]" + @subject << "[#{@ref_name}]" if action == :push + @subject << " " + + if action == :push + if @commits.length > 1 + @target_url = namespace_project_compare_url(@project.namespace, + @project, + from: Commit.new(@compare.base), + to: Commit.new(@compare.head)) + @subject << "Deleted " if @reverse_compare + @subject << "#{@commits.length} commits: #{@commits.first.title}" + else + @target_url = namespace_project_commit_url(@project.namespace, + @project, @commits.first) - if @commits.length > 1 - @target_url = namespace_project_compare_url(@project.namespace, - @project, - from: Commit.new(@compare.base), - to: Commit.new(@compare.head)) - @subject << "Deleted " if @reverse_compare - @subject << "#{@commits.length} commits: #{@commits.first.title}" + @subject << "Deleted 1 commit: " if @reverse_compare + @subject << @commits.first.title + end else - @target_url = namespace_project_commit_url(@project.namespace, - @project, @commits.first) + unless action == :delete + @target_url = namespace_project_tree_url(@project.namespace, + @project, @ref_name) + end - @subject << "Deleted 1 commit: " if @reverse_compare - @subject << @commits.first.title + subject_action = @action_name.dup + subject_action[0] = subject_action[0].capitalize + @subject << "#{subject_action} #{@ref_type} #{@ref_name}" end @disable_footer = true diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index acb5e7f1af5..3e465ab1a38 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -36,7 +36,7 @@ class EmailsOnPushService < Service end def supported_events - %w(push) + %w(push tag_push) end def execute(push_data) diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 039b92df2be..bbf7004c906 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -1,66 +1,67 @@ -%h3 #{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} +%h3 #{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} -- if @reverse_compare - %p - %strong WARNING: - The push did not contain any new commits, but force pushed to delete the commits and changes below. +- if @compare + - if @reverse_compare + %p + %strong WARNING: + The push did not contain any new commits, but force pushed to delete the commits and changes below. -%h4 - = @reverse_compare ? "Deleted commits:" : "Commits:" + %h4 + = @reverse_compare ? "Deleted commits:" : "Commits:" -%ul - - @commits.each do |commit| - %li - %strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)} - %div - %span by #{commit.author_name} - %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} - %pre.commit-message - = commit.safe_message + %ul + - @commits.each do |commit| + %li + %strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)} + %div + %span by #{commit.author_name} + %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} + %pre.commit-message + = commit.safe_message -%h4 #{pluralize @diffs.count, "changed file"}: + %h4 #{pluralize @diffs.count, "changed file"}: -%ul - - @diffs.each_with_index do |diff, i| - %li.file-stats - %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" } - - if diff.deleted_file - %span.deleted-file - − + %ul + - @diffs.each_with_index do |diff, i| + %li.file-stats + %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" } + - if diff.deleted_file + %span.deleted-file + − + = diff.old_path + - elsif diff.renamed_file = diff.old_path - - elsif diff.renamed_file - = diff.old_path - → - = diff.new_path - - elsif diff.new_file - %span.new-file - + + → = diff.new_path - - else - = diff.new_path - -- unless @disable_diffs - %h4 Changes: - - @diffs.each_with_index do |diff, i| - %li{id: "diff-#{i}"} - %a{href: @target_url + "#diff-#{i}"} - - if diff.deleted_file - %strong - = diff.old_path - deleted - - elsif diff.renamed_file - %strong - = diff.old_path - → - %strong + - elsif diff.new_file + %span.new-file + + + = diff.new_path + - else = diff.new_path - - else - %strong - = diff.new_path - %hr - %pre - = color_email_diff(diff.diff) - %br -- if @compare.timeout - %h5 Huge diff. To prevent performance issues changes are hidden + - unless @disable_diffs + %h4 Changes: + - @diffs.each_with_index do |diff, i| + %li{id: "diff-#{i}"} + %a{href: @target_url + "#diff-#{i}"} + - if diff.deleted_file + %strong + = diff.old_path + deleted + - elsif diff.renamed_file + %strong + = diff.old_path + → + %strong + = diff.new_path + - else + %strong + = diff.new_path + %hr + %pre + = color_email_diff(diff.diff) + %br + + - if @compare.timeout + %h5 Huge diff. To prevent performance issues changes are hidden diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml index 8d67a42234e..97a176ed2a3 100644 --- a/app/views/notify/repository_push_email.text.haml +++ b/app/views/notify/repository_push_email.text.haml @@ -1,47 +1,49 @@ -#{@author.name} pushed to #{@branch} at #{@project.name_with_namespace} -\ -\ -- if @reverse_compare - WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. +#{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{@project.name_with_namespace} +- if @compare \ \ -= @reverse_compare ? "Deleted commits:" : "Commits:" -- @commits.each do |commit| - #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} - #{commit.safe_message} - \- - - - - -\ -\ -#{pluralize @diffs.count, "changed file"}: -\ -- @diffs.each do |diff| - - if diff.deleted_file - \- − #{diff.old_path} - - elsif diff.renamed_file - \- #{diff.old_path} → #{diff.new_path} - - elsif diff.new_file - \- + #{diff.new_path} - - else - \- #{diff.new_path} -- unless @disable_diffs + - if @reverse_compare + WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below. + \ + \ + = @reverse_compare ? "Deleted commits:" : "Commits:" + - @commits.each do |commit| + #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")} + #{commit.safe_message} + \- - - - - + \ \ + #{pluralize @diffs.count, "changed file"}: \ - Changes: - @diffs.each do |diff| - \ - \===================================== - if diff.deleted_file - #{diff.old_path} deleted + \- − #{diff.old_path} - elsif diff.renamed_file - #{diff.old_path} → #{diff.new_path} + \- #{diff.old_path} → #{diff.new_path} + - elsif diff.new_file + \- + #{diff.new_path} - else - = diff.new_path - \===================================== - != diff.diff -- if @compare.timeout - \ - \ - Huge diff. To prevent performance issues it was hidden -\ -\ -View it on GitLab: #{@target_url} + \- #{diff.new_path} + - unless @disable_diffs + \ + \ + Changes: + - @diffs.each do |diff| + \ + \===================================== + - if diff.deleted_file + #{diff.old_path} deleted + - elsif diff.renamed_file + #{diff.old_path} → #{diff.new_path} + - else + = diff.new_path + \===================================== + != diff.diff + - if @compare.timeout + \ + \ + Huge diff. To prevent performance issues it was hidden + - if @target_url + \ + \ + View it on GitLab: #{@target_url} diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index e59ca81defe..429b29525dc 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -5,24 +5,32 @@ class EmailsOnPushWorker project = Project.find(project_id) before_sha = push_data["before"] after_sha = push_data["after"] - branch = push_data["ref"] + ref = push_data["ref"] author_id = push_data["user_id"] - if Gitlab::Git.blank_ref?(before_sha) || Gitlab::Git.blank_ref?(after_sha) - # skip if new branch was pushed or branch was removed - return true - end + action = + if Gitlab::Git.blank_ref?(before_sha) + :create + elsif Gitlab::Git.blank_ref?(after_sha) + :delete + else + :push + end - compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) + compare = nil + reverse_compare = false + if action == :push + compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - return false if compare.same + return false if compare.same - if compare.commits.empty? - compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) + if compare.commits.empty? + compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) - reverse_compare = true + reverse_compare = true - return false if compare.commits.empty? + return false if compare.commits.empty? + end end recipients.split(" ").each do |recipient| @@ -30,7 +38,8 @@ class EmailsOnPushWorker project_id, recipient, author_id, - branch, + ref, + action, compare, reverse_compare, send_from_committer_email, diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index e3a3b542358..aeb0e14d836 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -640,6 +640,100 @@ describe Notify do end end + describe 'email on push for a created branch' do + let(:example_site_path) { root_path } + let(:user) { create(:user) } + let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") } + + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/heads/master', :create, nil) } + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(user.name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'is sent to recipient' do + is_expected.to deliver_to 'devs@company.name' + end + + it 'has the correct subject' do + is_expected.to have_subject /Pushed new branch master/ + end + + it 'contains a link to the branch' do + is_expected.to have_body_text /#{tree_path}/ + end + end + + describe 'email on push for a created tag' do + let(:example_site_path) { root_path } + let(:user) { create(:user) } + let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") } + + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/tags/v1.0', :create, nil) } + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(user.name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'is sent to recipient' do + is_expected.to deliver_to 'devs@company.name' + end + + it 'has the correct subject' do + is_expected.to have_subject /Pushed new tag v1\.0/ + end + + it 'contains a link to the tag' do + is_expected.to have_body_text /#{tree_path}/ + end + end + + describe 'email on push for a deleted branch' do + let(:example_site_path) { root_path } + let(:user) { create(:user) } + + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/heads/master', :delete, nil) } + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(user.name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'is sent to recipient' do + is_expected.to deliver_to 'devs@company.name' + end + + it 'has the correct subject' do + is_expected.to have_subject /Deleted branch master/ + end + end + + describe 'email on push for a deleted tag' do + let(:example_site_path) { root_path } + let(:user) { create(:user) } + + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/tags/v1.0', :delete, nil) } + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(user.name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'is sent to recipient' do + is_expected.to deliver_to 'devs@company.name' + end + + it 'has the correct subject' do + is_expected.to have_subject /Deleted tag v1\.0/ + end + end + describe 'email on push with multiple commits' do let(:example_site_path) { root_path } let(:user) { create(:user) } @@ -648,7 +742,7 @@ describe Notify do let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base), to: Commit.new(compare.head)) } let(:send_from_committer_email) { false } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare, false, send_from_committer_email) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/heads/master', :push, compare, false, send_from_committer_email) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -736,7 +830,7 @@ describe Notify do let(:commits) { Commit.decorate(compare.commits) } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/heads/master', :push, compare) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] -- GitLab From 88f9ed3d196776295294d540556216b95d0bcd60 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 10:28:50 +0100 Subject: [PATCH 1402/1609] Move finding of contributing file from tree to repository. --- app/models/repository.rb | 7 ++++++- app/models/tree.rb | 7 +------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 47758b8ad68..9e82d79ec8c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -197,7 +197,12 @@ class Repository end def contribution_guide - cache.fetch(:contribution_guide) { tree(:head).contribution_guide } + cache.fetch(:contribution_guide) do + tree(:head).blobs.find do |file| + file.contributing? + end + end + end end def head_commit diff --git a/app/models/tree.rb b/app/models/tree.rb index 4f5d81f0a5e..9095fb134ff 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -1,7 +1,7 @@ class Tree include Gitlab::MarkdownHelper - attr_accessor :entries, :readme, :contribution_guide + attr_accessor :entries, :readme def initialize(repository, sha, path = '/') path = '/' if path.blank? @@ -28,11 +28,6 @@ class Tree readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path) end - - if contribution_tree = @entries.find(&:contributing?) - contribution_path = path == '/' ? contribution_tree.name : File.join(path, contribution_tree.name) - @contribution_guide = Gitlab::Git::Blob.find(git_repo, sha, contribution_path) - end end def trees -- GitLab From 31e484085a22ccc012b825670e5bbff25684933c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 10:29:06 +0100 Subject: [PATCH 1403/1609] Find tree readme lazily. --- app/models/tree.rb | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/app/models/tree.rb b/app/models/tree.rb index 9095fb134ff..d3f9176c79c 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -1,33 +1,32 @@ class Tree include Gitlab::MarkdownHelper - attr_accessor :entries, :readme + attr_accessor :entries def initialize(repository, sha, path = '/') path = '/' if path.blank? git_repo = repository.raw_repository @entries = Gitlab::Git::Tree.where(git_repo, sha, path) + end + + def readme + return @readme if defined?(@readme) + + available_readmes = @blobs.select(&:readme?) - available_readmes = @entries.select(&:readme?) - - if available_readmes.count > 0 - # If there is more than 1 readme in tree, find readme which is supported - # by markup renderer. - if available_readmes.length > 1 - supported_readmes = available_readmes.select do |readme| - previewable?(readme.name) - end - - # Take the first supported readme, or the first available readme, if we - # don't support any of them - readme_tree = supported_readmes.first || available_readmes.first - else - readme_tree = available_readmes.first - end - - readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) - @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path) + if available_readmes.count == 0 + return @readme = nil end + + # Take the first previewable readme, or the first available readme, if we + # can't preview any of them + readme_tree = available_readmes.find do |readme| + previewable?(readme.name) + end || available_readmes.first + + readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) + + @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path) end def trees -- GitLab From 6581aea3bf8e3529aa4fed85ccd923b2afb630b5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 10:29:17 +0100 Subject: [PATCH 1404/1609] Link project version to changelog if there is one. --- app/models/repository.rb | 9 ++++++++- app/views/projects/show.html.haml | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 9e82d79ec8c..8d0306e820d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -122,7 +122,7 @@ class Repository def expire_cache %i(size branch_names tag_names commit_count graph_log - readme version contribution_guide).each do |key| + readme version contribution_guide changelog).each do |key| cache.expire(key) end end @@ -203,6 +203,13 @@ class Repository end end end + + def changelog + cache.fetch(:changelog) do + tree(:head).blobs.find do |file| + file.name =~ /^(changelog|history)/i + end + end end def head_commit diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 74b07395650..0eaf4b95d66 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -52,7 +52,8 @@ - if @repository.version - version = @repository.version - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, version.name)), class: 'btn btn-block' do + - detail_file = @repository.changelog.try(:name) || version.name + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, detail_file)), class: 'btn btn-block' do Version: %span.count = @repository.blob_by_oid(version.id).data -- GitLab From d328b4166ed235346e3ea841f178a52224c23c99 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 10:33:25 +0100 Subject: [PATCH 1405/1609] Fix tree readme. --- app/models/tree.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/models/tree.rb b/app/models/tree.rb index d3f9176c79c..f279e896cda 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -1,18 +1,23 @@ class Tree include Gitlab::MarkdownHelper - attr_accessor :entries + attr_accessor :repository, :sha, :path, :entries def initialize(repository, sha, path = '/') path = '/' if path.blank? - git_repo = repository.raw_repository - @entries = Gitlab::Git::Tree.where(git_repo, sha, path) + + @repository = repository + @sha = sha + @path = path + + git_repo = @repository.raw_repository + @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path) end def readme return @readme if defined?(@readme) - available_readmes = @blobs.select(&:readme?) + available_readmes = blobs.select(&:readme?) if available_readmes.count == 0 return @readme = nil @@ -26,6 +31,7 @@ class Tree readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) + git_repo = repository.raw_repository @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path) end -- GitLab From 5adb1128dcca2810a32b7b974372970c6d36a98c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 11:43:43 +0100 Subject: [PATCH 1406/1609] Show changelog link even if no version is known. --- app/views/projects/show.html.haml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 0eaf4b95d66..25dee2043be 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -50,13 +50,17 @@ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do Compare code - - if @repository.version - - version = @repository.version - - detail_file = @repository.changelog.try(:name) || version.name + - version = @repository.version + - changelog = @repository.changelog + - if version + - detail_file = changelog.try(:name) || version.name = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, detail_file)), class: 'btn btn-block' do Version: %span.count = @repository.blob_by_oid(version.id).data + - elsif changelog + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, changelog.name)), class: 'btn btn-block' do + View changelog .prepend-top-10 %p -- GitLab From 9d938fd77da033f09530571a6194609aee8bbc7b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 15:51:14 +0100 Subject: [PATCH 1407/1609] List new commits for newly pushed branch in activity view. --- CHANGELOG | 1 + app/helpers/events_helper.rb | 2 +- app/models/event.rb | 2 +- app/views/events/event/_push.html.haml | 10 ++++++++-- lib/gitlab/push_data_builder.rb | 15 +++++++-------- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 09b60e8e54a..23a48375b45 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) - Add a service to support external wikis (Hannes Rosenögger) + - List new commits for newly pushed branch in activity view. v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 779cebc0136..c9fd0f0362b 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -96,7 +96,7 @@ module EventsHelper end end elsif event.push? - if event.push_with_commits? + if event.push_with_commits? && event.md_ref? if event.commits_count > 1 namespace_project_compare_url(event.project.namespace, event.project, from: event.commit_from, to: diff --git a/app/models/event.rb b/app/models/event.rb index 8d20d7ef252..2103a48a71b 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -247,7 +247,7 @@ class Event < ActiveRecord::Base end def push_with_commits? - md_ref? && commits.any? && commit_from && commit_to + !commits.empty? && commit_from && commit_to end def last_push_to_non_root? diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 489138887ae..60d7978b13f 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -21,5 +21,11 @@ %li.commits-stat - if event.commits_count > 2 %span ... and #{event.commits_count - 2} more commits. - = link_to namespace_project_compare_path(event.project.namespace, event.project, from: event.commit_from, to: event.commit_to) do - %strong Compare → #{truncate_sha(event.commit_from)}...#{truncate_sha(event.commit_to)} + - if event.md_ref? + - from = event.commit_from + - from_label = truncate_sha(from) + - else + - from = event.project.default_branch + - from_label = from + = link_to namespace_project_compare_path(event.project.namespace, event.project, from: from, to: event.commit_to) do + %strong Compare → #{from_label}...#{truncate_sha(event.commit_to)} diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index ea9012b8844..694a30db5df 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -27,6 +27,12 @@ module Gitlab # Get latest 20 commits ASC commits_limited = commits.last(20) + + # For performance purposes maximum 20 latest commits + # will be passed as post receive hook data. + commit_attrs = commits_limited.map do |commit| + commit.hook_attrs(project) + end type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" # Hash to be passed as post_receive_data @@ -49,17 +55,10 @@ module Gitlab git_ssh_url: project.ssh_url_to_repo, visibility_level: project.visibility_level }, - commits: [], + commits: commit_attrs, total_commits_count: commits_count } - # For performance purposes maximum 20 latest commits - # will be passed as post receive hook data. - commits_limited.each do |commit| - data[:commits] << commit.hook_attrs(project) - end - - data[:commits] = "" if data[:commits].count == 0 data end -- GitLab From b1b8261f56d35e471bc32b0f2050e27650b7c797 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 17 Mar 2015 18:37:50 +0100 Subject: [PATCH 1408/1609] Add license and contribution guide links to project sidebar. --- CHANGELOG | 1 + app/helpers/projects_helper.rb | 37 ++++++++++++++++++- app/models/repository.rb | 10 ++++- app/views/projects/_issuable_form.html.haml | 4 +- .../merge_requests/_new_submit.html.haml | 4 +- app/views/projects/show.html.haml | 26 ++++++++----- 6 files changed, 65 insertions(+), 17 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 09b60e8e54a..ec5ada56311 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) - Add a service to support external wikis (Hannes Rosenögger) + - Add changelog, license and contribution guide links to project sidebar. v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 2225b110651..a14277180c7 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -232,12 +232,45 @@ module ProjectsHelper end def contribution_guide_url(project) - if project && project.repository.contribution_guide + if project && contribution_guide = project.repository.contribution_guide namespace_project_blob_path( project.namespace, project, tree_join(project.default_branch, - project.repository.contribution_guide.name) + contribution_guide.name) + ) + end + end + + def changelog_url(project) + if project && changelog = project.repository.changelog + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + changelog.name) + ) + end + end + + def license_url(project) + if project && license = project.repository.license + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + license.name) + ) + end + end + + def version_url(project) + if project && version = project.repository.version + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + version.name) ) end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 8d0306e820d..86c9c9b4d48 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -122,7 +122,7 @@ class Repository def expire_cache %i(size branch_names tag_names commit_count graph_log - readme version contribution_guide changelog).each do |key| + readme version contribution_guide changelog license).each do |key| cache.expire(key) end end @@ -212,6 +212,14 @@ class Repository end end + def license + cache.fetch(:license) do + tree(:head).blobs.find do |file| + file.name =~ /^license/i + end + end + end + def head_commit commit(self.root_ref) end diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index a7cd129b631..7fd5fe8a6e1 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -71,10 +71,10 @@ = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank .form-actions - - if !issuable.project.empty_repo? && contribution_guide_url(issuable.project) && !issuable.persisted? + - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted? %p Please review the - %strong #{link_to 'guidelines for contribution', contribution_guide_url(issuable.project)} + %strong #{link_to 'guidelines for contribution', guide_url} to this repository. - if issuable.new_record? = f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index bf80afe8785..1d8eef4e8ce 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -69,10 +69,10 @@ = link_to 'Create new label', new_namespace_project_label_path(@merge_request.target_project.namespace, @merge_request.target_project), target: :blank .form-actions - - if contribution_guide_url(@target_project) + - if guide_url = contribution_guide_url(@target_project) %p Please review the - %strong #{link_to 'guidelines for contribution', contribution_guide_url(@target_project)} + %strong #{link_to 'guidelines for contribution', guide_url} to this repository. = f.hidden_field :source_project_id = f.hidden_field :source_branch diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 25dee2043be..822e67c5616 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -47,21 +47,27 @@ = link_to @project.forked_from_project.name_with_namespace, namespace_project_path(@project.namespace, @project.forked_from_project) - unless @project.empty_repo? - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do - Compare code - - - version = @repository.version - - changelog = @repository.changelog - - if version - - detail_file = changelog.try(:name) || version.name - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, detail_file)), class: 'btn btn-block' do + - if version = @repository.version + - detail_url = changelog_url(@project) || version_url(@project) + = link_to detail_url, class: 'btn btn-block' do Version: %span.count = @repository.blob_by_oid(version.id).data - - elsif changelog - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, changelog.name)), class: 'btn btn-block' do + - elsif @repository.changelog + = link_to changelog_url(@project), class: 'btn btn-block' do View changelog + - if @repository.contribution_guide + = link_to contribution_guide_url(@project), class: 'btn btn-block' do + View contribution guide + + - if @repository.license + = link_to license_url(@project), class: 'btn btn-block' do + View license + + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do + Compare code + .prepend-top-10 %p %span.light Created on -- GitLab From ce8359c15173e92af0d083dbfd1d618e18645e24 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 18 Mar 2015 08:25:50 -0700 Subject: [PATCH 1409/1609] Fix the order of signin and signup on features page. --- app/views/admin/application_settings/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 781600a3766..edfcccfcf4c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -12,13 +12,13 @@ .checkbox = f.label :signup_enabled do = f.check_box :signup_enabled - Signin enabled + Signup enabled .form-group .col-sm-offset-2.col-sm-10 .checkbox = f.label :signin_enabled do = f.check_box :signin_enabled - Signup enabled + Signin enabled .form-group .col-sm-offset-2.col-sm-10 .checkbox -- GitLab From ebfc7d052bbde2f0adcd986a207788d2aec7d85d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 18 Mar 2015 16:37:29 +0100 Subject: [PATCH 1410/1609] Fix condensed range in MR push comment. --- app/models/note.rb | 8 ++++++-- app/services/merge_requests/refresh_service.rb | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/note.rb b/app/models/note.rb index 9ca3e4d7e97..649e9b4e852 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -151,7 +151,7 @@ class Note < ActiveRecord::Base ) end - def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = []) + def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = [], oldrev = nil) total_count = new_commits.length + existing_commits.length commits_text = ActionController::Base.helpers.pluralize(total_count, 'commit') body = "Added #{commits_text}:\n\n" @@ -161,7 +161,11 @@ class Note < ActiveRecord::Base if existing_commits.length == 1 existing_commits.first.short_id else - "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" + if oldrev + "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}" + else + "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}" + end end commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit') diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index cab8a1e880e..7eef2c2d6a5 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -89,7 +89,7 @@ module MergeRequests end Note.create_new_commits_note(merge_request, merge_request.project, - @current_user, new_commits, existing_commits) + @current_user, new_commits, existing_commits, @oldrev) end end -- GitLab From 7602cef572fd770a06eb3d94f2aef57c2693dcb6 Mon Sep 17 00:00:00 2001 From: Samuel Bernard Date: Wed, 18 Mar 2015 15:28:32 +0100 Subject: [PATCH 1411/1609] Note: add default_scope { order(created_at: :asc, id: :asc) } --- app/models/note.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/note.rb b/app/models/note.rb index 9ca3e4d7e97..2be06978d5f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -44,6 +44,7 @@ class Note < ActiveRecord::Base mount_uploader :attachment, AttachmentUploader # Scopes + default_scope { order(created_at: :asc, id: :asc) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :inline, ->{ where("line_code IS NOT NULL") } scope :not_inline, ->{ where(line_code: [nil, '']) } -- GitLab From 85d226e57f24fd4c368bed3fe352a9f1f8db51a1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 10:25:22 -0700 Subject: [PATCH 1412/1609] Bump gitlab_git to 7.1.2 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 5bbbed9c139..ad33116db3e 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.1.0' +gem "gitlab_git", '~> 7.1.2' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' diff --git a/Gemfile.lock b/Gemfile.lock index e728115fa98..a454461ec26 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -213,7 +213,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.1.1) + gitlab_git (7.1.2) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -705,7 +705,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.1.0) + gitlab_git (~> 7.1.2) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.0) -- GitLab From 1446af25b1b5938871f697a9643bf04c78ba33f0 Mon Sep 17 00:00:00 2001 From: Jeroen van Baarsen Date: Wed, 18 Mar 2015 19:35:38 +0100 Subject: [PATCH 1413/1609] Added binstub for guard Signed-off-by: Jeroen van Baarsen --- bin/guard | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100755 bin/guard diff --git a/bin/guard b/bin/guard new file mode 100755 index 00000000000..0c1a532bd01 --- /dev/null +++ b/bin/guard @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# +# This file was generated by Bundler. +# +# The application 'guard' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'pathname' +ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require 'rubygems' +require 'bundler/setup' + +load Gem.bin_path('guard', 'guard') -- GitLab From 0eaabc270868b79185f72ac6e99551e33dbb3c23 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 11:42:03 -0700 Subject: [PATCH 1414/1609] Revert "Note: add default_scope { order(created_at: :asc, id: :asc) }" This reverts commit 7602cef572fd770a06eb3d94f2aef57c2693dcb6. --- app/models/note.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/note.rb b/app/models/note.rb index bbbe71173a9..649e9b4e852 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -44,7 +44,6 @@ class Note < ActiveRecord::Base mount_uploader :attachment, AttachmentUploader # Scopes - default_scope { order(created_at: :asc, id: :asc) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :inline, ->{ where("line_code IS NOT NULL") } scope :not_inline, ->{ where(line_code: [nil, '']) } -- GitLab From e535d217687d3f00844857400282462dfa13049c Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Wed, 18 Mar 2015 12:38:58 -0600 Subject: [PATCH 1415/1609] Handle nil restricted visibility settings Return `true` from `non_restricted_level?` when the `restricted_visibility_levels` setting is nil. --- lib/gitlab/visibility_level.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 1851e76067c..e4306bd2a56 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -35,7 +35,11 @@ module Gitlab end def non_restricted_level?(level) - ! current_application_settings.restricted_visibility_levels.include?(level) + if current_application_settings.restricted_visibility_levels.nil? + true + else + ! current_application_settings.restricted_visibility_levels.include?(level) + end end def valid_level?(level) -- GitLab From 63f712948307723052589895d5bb993df03eafbd Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 18 Mar 2015 13:55:41 -0700 Subject: [PATCH 1416/1609] Move application setting to separate variable. --- lib/gitlab/visibility_level.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index e4306bd2a56..582fc759efd 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -35,10 +35,12 @@ module Gitlab end def non_restricted_level?(level) - if current_application_settings.restricted_visibility_levels.nil? + restricted_levels = current_application_settings.restricted_visibility_levels + + if restricted_levels.nil? true else - ! current_application_settings.restricted_visibility_levels.include?(level) + !restricted_levels.include?(level) end end -- GitLab From e7e329d36402ff84f4ba08184283853518136d7f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 16:17:30 -0700 Subject: [PATCH 1417/1609] Fix link to project from fork --- app/views/projects/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 822e67c5616..d5a44b7ed51 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -44,7 +44,7 @@ %i.fa.fa-code-fork.project-fork-icon Forked from: %br - = link_to @project.forked_from_project.name_with_namespace, namespace_project_path(@project.namespace, @project.forked_from_project) + = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) - unless @project.empty_repo? - if version = @repository.version @@ -64,7 +64,7 @@ - if @repository.license = link_to license_url(@project), class: 'btn btn-block' do View license - + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do Compare code -- GitLab From e0f8e022f7c8cf73247a3918a689c10c23b19516 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 17:53:43 -0700 Subject: [PATCH 1418/1609] Improve comment toggle button in diff --- app/assets/javascripts/application.js.coffee | 4 +--- app/views/projects/diffs/_file.html.haml | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index c7acde2afe5..fda142293bc 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -169,9 +169,7 @@ $ -> # Show/hide comments on diff $("body").on "click", ".js-toggle-diff-comments", (e) -> - $(@).find('i'). - toggleClass('fa fa-chevron-down'). - toggleClass('fa fa-chevron-up') + $(@).toggleClass('active') $(@).closest(".diff-file").find(".notes_holder").toggle() e.preventDefault() diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index a9e4d63cd98..2beb768b926 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -22,9 +22,8 @@ .diff-btn-group - if blob.text? - = link_to '#', class: 'js-toggle-diff-comments btn btn-sm' do - %i.fa.fa-chevron-down - Show/Hide comments + = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active' do + %i.fa.fa-comments   - if @merge_request && @merge_request.source_project -- GitLab From 22038106b48f04e2834b0b4ae0bbc4c10b146517 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 19:11:27 -0700 Subject: [PATCH 1419/1609] Replace show diff button with link --- app/views/projects/diffs/_stats.html.haml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index d387ec2f75c..1625930615a 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -1,17 +1,14 @@ .js-toggle-container .commit-stat-summary Showing - %strong.cdark #{pluralize(diffs.count, "changed file")} + = link_to '#', class: 'js-toggle-button' do + %strong #{pluralize(diffs.count, "changed file")} - if current_controller?(:commit) - unless @commit.has_zero_stats? with %strong.cgreen #{@commit.stats.additions} additions and %strong.cred #{@commit.stats.deletions} deletions -   - = link_to '#', class: 'btn btn-sm js-toggle-button' do - Show diff stats - %i.fa.fa-chevron-down .file-stats.js-toggle-content.hide %ul.bordered-list - diffs.each_with_index do |diff, i| -- GitLab From e07d32eb418584bef4289260fcb4792d6b6ca39e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 19:21:24 -0700 Subject: [PATCH 1420/1609] Add some space aroung diff stats block --- app/views/projects/diffs/_diffs.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 48d4c33ce85..1747f36dcf3 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,4 +1,4 @@ -.row +.row.prepend-top-20.append-bottom-10 .col-md-8 = render 'projects/diffs/stats', diffs: diffs .col-md-4 -- GitLab From 1cf138170c17913b180e7b3d2840ca4439c618a1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 20:12:19 -0700 Subject: [PATCH 1421/1609] Add tooltip for comment toggle in diff. Add changelog item and fix tests --- CHANGELOG | 1 + app/views/projects/diffs/_file.html.haml | 2 +- features/steps/project/merge_requests.rb | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 22f38024f93..328c672338d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 7.10.0 (unreleased) - Add a service to support external wikis (Hannes Rosenögger) - List new commits for newly pushed branch in activity view. - Add changelog, license and contribution guide links to project sidebar. + - Improve diff UI v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 2beb768b926..860ab096341 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -22,7 +22,7 @@ .diff-btn-group - if blob.text? - = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active' do + = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do %i.fa.fa-comments   diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 6e2f60972b6..6f6ce439f3e 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -209,13 +209,13 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I click link "Hide inline discussion" of the second file' do within '.files [id^=diff]:nth-child(2)' do - click_link 'Show/Hide comments' + find('.js-toggle-diff-comments').click end end step 'I click link "Show inline discussion" of the second file' do within '.files [id^=diff]:nth-child(2)' do - click_link 'Show/Hide comments' + find('.js-toggle-diff-comments').click end end -- GitLab From b908d00f5ba8d290ec0154c9429483979debe8a4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 20:16:36 -0700 Subject: [PATCH 1422/1609] Improve group rename warning --- app/views/shared/_group_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index b34dd53e3b5..c0a9923348e 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -15,7 +15,7 @@ = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false - if @group.persisted? - .alert.alert-danger + .alert.alert-warning.prepend-top-10 %ul %li Changing group path can have unintended side effects. %li Renaming group path will rename directory for all related projects -- GitLab From e25b1107c8cec0b2bfd362dcd5cb6a5cbff90e49 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 20:46:26 -0700 Subject: [PATCH 1423/1609] Refactor select css. Make selects same width --- app/assets/stylesheets/generic/selects.scss | 48 ++----------------- app/assets/stylesheets/pages/groups.scss | 1 - app/assets/stylesheets/pages/issues.scss | 35 +++++++------- .../stylesheets/pages/merge_requests.scss | 4 ++ app/assets/stylesheets/pages/tree.scss | 2 +- .../group_members/_group_member.html.haml | 6 ++- app/views/projects/issues/index.html.haml | 2 +- .../project_members/_project_member.html.haml | 8 ++-- .../protected_branches/index.html.haml | 10 ++-- 9 files changed, 41 insertions(+), 75 deletions(-) diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index c13a685a528..7557f411111 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -46,55 +46,13 @@ } } -select { - &.select2 { - width: 100px; - } - - &.select2-sm { - width: 100px; - } -} - -@media (min-width: $screen-sm-min) { - select { - &.select2 { - width: 150px; - } - &.select2-sm { - width: 120px; - } - } +.select2-container { + width: 100% !important; } -/* Medium devices (desktops, 992px and up) */ -@media (min-width: $screen-md-min) { - select { - &.select2 { - width: 170px; - } - &.select2-sm { - width: 140px; - } - } -} - -/* Large devices (large desktops, 1200px and up) */ -@media (min-width: $screen-lg-min) { - select { - &.select2 { - width: 200px; - } - &.select2-sm { - width: 150px; - } - } -} - - /** Branch/tag selector **/ .project-refs-form .select2-container { - margin-right: 10px; + width: 160px !important; } .ajax-users-dropdown, .ajax-project-users-dropdown { diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index e49fe1a9dd6..2b1b747139a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -1,6 +1,5 @@ .new-group-member-holder { margin-top: 50px; - background: #f9f9f9; padding-top: 20px; } diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 4ea34cc1dac..6c1dd4f7e9f 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -59,33 +59,34 @@ } } -@media (min-width: 800px) { .issues_filters select { width: 160px; } } -@media (min-width: 1200px) { .issues_filters select { width: 220px; } } +@media (min-width: 800px) { + .issues_bulk_update { + select, .select2-container { + width: 120px !important; + display: inline-block; + } + } +} -@media (min-width: 800px) { .issues_bulk_update .select2-container { min-width: 120px; } } -@media (min-width: 1200px) { .issues_bulk_update .select2-container { min-width: 160px; } } +@media (min-width: 1200px) { + .issues_bulk_update { + select, .select2-container { + width: 160px !important; + display: inline-block; + } + } +} .issues_bulk_update { .select2-container .select2-choice { color: #444 !important; - font-weight: 500; } } -#update_status { - width: 100px; -} - .participants { margin-bottom: 20px; } -.issues_bulk_update { - .select2-container { - text-shadow: none; - } -} - .issue-search-form { margin: 0; height: 24px; @@ -177,6 +178,6 @@ h2.issue-title { font-weight: bold; } -.context .select2-container { - width: 100% !important; +.issue-form .select2-container { + width: 250px !important; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index fe5667a587f..394b59b7e4b 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -194,3 +194,7 @@ } } } + +.merge-request-form .select2-container { + width: 250px !important; +} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index b0e6a05fa06..57f63b52aa1 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -112,7 +112,7 @@ .tree-ref-holder { float: left; - margin-right: 6px; + margin-right: 15px; .select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice { padding: 4px 12px; diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 003025221b2..30e5faf822e 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -31,7 +31,9 @@ %i.fa.fa-minus.fa-inverse .edit-member.hide.js-toggle-content + %br = form_for [@group, member], remote: true do |f| - .alert.prepend-top-20 - = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level) + .prepend-top-10 + = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level), {}, class: 'form-control' + .prepend-top-10 = f.submit 'Save', class: 'btn btn-save btn-sm' diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 2cb94d10b6f..54e3009cca2 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -18,7 +18,7 @@ .clearfix .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do - = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status") + = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") = hidden_field_tag 'update[issues_ids]', [] diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 1f31d84dd1d..a07d0762334 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -29,7 +29,9 @@ %i.fa.fa-minus.fa-inverse .edit-member.hide.js-toggle-content + %br = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member.user), remote: true do |f| - .alert.prepend-top-20 - = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level) - = f.submit 'Save', class: 'btn btn-save btn-sm' + .prepend-top-10 + = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control' + .prepend-top-10 + = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index cfe28084170..4db71ce8ff9 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -23,12 +23,12 @@ .col-sm-10 = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"}) .form-group - = f.label :developers_can_push, class: 'control-label' do - Developers can push - .col-sm-10 + .col-sm-offset-2.col-sm-10 .checkbox - = f.check_box :developers_can_push - %span.descr Allow developers to push to this branch + = f.label :developers_can_push do + = f.check_box :developers_can_push + %strong Developers can push + .help-block Allow developers to push to this branch .form-actions = f.submit 'Protect', class: "btn-create btn" = render 'branches_list' -- GitLab From 39484411713fcfbf586015926ccff61d597335fc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 20:48:32 -0700 Subject: [PATCH 1424/1609] Update CHANGELOG with selectbox refactoring --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 22f38024f93..b4c8aeff680 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 7.10.0 (unreleased) - Add a service to support external wikis (Hannes Rosenögger) - List new commits for newly pushed branch in activity view. - Add changelog, license and contribution guide links to project sidebar. + - Identical look of selectboxes in UI v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) -- GitLab From ba39ca9f4ad2f5723795a59004fd7b77a51cca43 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 20:58:20 -0700 Subject: [PATCH 1425/1609] Fix project name overflow on dashboard --- app/assets/stylesheets/pages/dashboard.scss | 17 ++++------------- app/views/dashboard/_projects.html.haml | 6 +++--- app/views/dashboard/projects/starred.html.haml | 6 +++--- app/views/groups/_projects.html.haml | 6 +++--- 4 files changed, 13 insertions(+), 22 deletions(-) diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 5a543a852c2..e408211fc7d 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -28,6 +28,10 @@ font-size: 14px; line-height: 24px; + .str-truncated { + max-width: 75%; + } + a { display: block; padding: 8px 15px; @@ -87,16 +91,3 @@ margin-right: 5px; width: 16px; } - -.dash-new-project { - background: $gl-success; - border: 1px solid $gl-success; - - a { - color: #FFF; - } -} - -.dash-list .str-truncated { - max-width: 72%; -} diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index 3634b2bfd7b..d676576067c 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -3,8 +3,8 @@ .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if current_user.can_create_project? - .input-group-addon.dash-new-project - = link_to new_project_path do - %strong New project + %span.input-group-btn + = link_to new_project_path, class: 'btn btn-success' do + New project = render 'shared/projects_list', projects: @projects, projects_limit: 20 diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 94de6092563..670f5ac7af7 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -8,9 +8,9 @@ .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if current_user.can_create_project? - .input-group-addon.dash-new-project - = link_to new_project_path do - %strong New project + %span.input-group-btn + = link_to new_project_path, class: 'btn btn-success' do + New project = render 'shared/projects_list', projects: @projects, projects_limit: 20, stars: true, avatar: false diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 6f53e125c47..4f8aec1c67e 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -3,8 +3,8 @@ .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if can? current_user, :create_projects, @group - .input-group-addon.dash-new-project - = link_to new_project_path(namespace_id: @group.id) do - %strong New project + %span.input-group-btn + = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-success' do + New project = render 'shared/projects_list', projects: @projects, projects_limit: 20 -- GitLab From 1f835a81a7b23a8e90375867089728a45e168414 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 21:20:43 -0700 Subject: [PATCH 1426/1609] Remove success signin message by devise I find it really annoying every time I login into GitLab it shows me that I successfully signed in. But this makes no sense to me. I already see dashboard and dont see login screen. Its obvious I signed in successfully. Instead it just show annyoing message every time taking part of space on my screen. --- config/locales/devise.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 1cbcde5b3da..ed350d2a065 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -23,7 +23,7 @@ en: timeout: 'Your session expired, please sign in again to continue.' inactive: 'Your account was not activated yet.' sessions: - signed_in: 'Signed in successfully.' + signed_in: '' signed_out: 'Signed out successfully.' users_sessions: user: -- GitLab From 64f5d6ddf06620e4989ffe414602998016f39ac5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Mar 2015 04:26:36 +0000 Subject: [PATCH 1427/1609] Remove signout flash message because it also makes no sense. You get redirected after signuout to gitlab.com and see this message on login page which makes no sense. --- config/locales/devise.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index ed350d2a065..f3db5b7476e 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -24,7 +24,7 @@ en: inactive: 'Your account was not activated yet.' sessions: signed_in: '' - signed_out: 'Signed out successfully.' + signed_out: '' users_sessions: user: signed_in: 'Signed in successfully.' @@ -57,4 +57,4 @@ en: reset_password_instructions: subject: 'Reset password instructions' unlock_instructions: - subject: 'Unlock Instructions' + subject: 'Unlock Instructions' \ No newline at end of file -- GitLab From f3d50461962b1a9696cad234560d4362264ec411 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 18 Mar 2015 21:32:23 -0700 Subject: [PATCH 1428/1609] Return compare code button to top of sidebar --- app/views/projects/show.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index d5a44b7ed51..cfa6cda0466 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -47,6 +47,9 @@ = link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project) - unless @project.empty_repo? + = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do + Compare code + - if version = @repository.version - detail_url = changelog_url(@project) || version_url(@project) = link_to detail_url, class: 'btn btn-block' do @@ -65,9 +68,6 @@ = link_to license_url(@project), class: 'btn btn-block' do View license - = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do - Compare code - .prepend-top-10 %p %span.light Created on -- GitLab From 5aa97ce91ba0e2ef06ea8a03b10d36287fb8768b Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 18 Mar 2015 21:47:17 -0700 Subject: [PATCH 1429/1609] Show test settings as disabled when service cannot be tested. --- app/views/projects/services/_form.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index bb983229b1c..32e97a754cb 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -101,5 +101,6 @@ .form-actions = f.submit 'Save', class: 'btn btn-save'   - - if @service.valid? && @service.activated? && @service.can_test? - = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: 'btn' + - if @service.valid? && @service.activated? + - disabled = @service.can_test? ? '':'disabled' + = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}" -- GitLab From d70126c1fbc65c4feeea6fb45424f5d4a30bb797 Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Thu, 19 Mar 2015 09:29:43 +0100 Subject: [PATCH 1430/1609] Update poltergeist to support phantomjs 2.0 --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 8 +++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c9d04e40109..ed1a85527bf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) - Add a service to support external wikis (Hannes Rosenögger) diff --git a/Gemfile b/Gemfile index 5bbbed9c139..988829015da 100644 --- a/Gemfile +++ b/Gemfile @@ -252,7 +252,7 @@ group :development, :test do gem 'rb-inotify', require: linux_only('rb-inotify') # PhantomJS driver for Capybara - gem 'poltergeist', '~> 1.5.1' + gem 'poltergeist', '~> 1.6.0' gem 'jasmine', '2.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index e728115fa98..53544cda532 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -392,7 +392,7 @@ GEM ast (>= 1.1, < 3.0) pg (0.15.1) phantomjs (1.9.2.0) - poltergeist (1.5.1) + poltergeist (1.6.0) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) @@ -650,7 +650,9 @@ GEM webmock (1.16.0) addressable (>= 2.2.7) crack (>= 0.3.2) - websocket-driver (0.3.3) + websocket-driver (0.5.3) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) wikicloth (0.8.1) builder expression_parser @@ -744,7 +746,7 @@ DEPENDENCIES omniauth-twitter org-ruby (= 0.9.12) pg - poltergeist (~> 1.5.1) + poltergeist (~> 1.6.0) pry-rails quiet_assets (~> 1.0.1) rack-attack -- GitLab From 575ae806477edeb4e4c662560fb4ebca0773edad Mon Sep 17 00:00:00 2001 From: kingcody Date: Thu, 19 Mar 2015 03:51:32 -0400 Subject: [PATCH 1431/1609] fix(style): align navbar-toggle Changes: - change `.navbar-toggle` from `margin: 0 -15px 0 0;` to `margin: 0;` --- CHANGELOG | 1 + app/assets/stylesheets/pages/header.scss | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b6c39ae5f71..f807e506cc9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 7.10.0 (unreleased) - List new commits for newly pushed branch in activity view. - Add changelog, license and contribution guide links to project sidebar. - Improve diff UI + - Fix alignment of navbar toggle button (Cody Mize) - Identical look of selectboxes in UI v 7.9.0 (unreleased) diff --git a/app/assets/stylesheets/pages/header.scss b/app/assets/stylesheets/pages/header.scss index 26b4d04106e..dde19b801f8 100644 --- a/app/assets/stylesheets/pages/header.scss +++ b/app/assets/stylesheets/pages/header.scss @@ -31,7 +31,7 @@ header { .navbar-toggle { color: $style_color; - margin: 0 -15px 0 0; + margin: 0; padding: 10px; border-radius: 0; -- GitLab From a5a5ec970e0e8dc56103decd3a0f5fbf3db46bcb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 19 Mar 2015 10:34:04 +0100 Subject: [PATCH 1432/1609] Fewer constants, more helpers. --- app/services/git_push_service.rb | 2 +- app/workers/irker_worker.rb | 6 +++--- lib/gitlab/force_push_check.rb | 7 ++++--- lib/gitlab/push_data_builder.rb | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 4885e1b2fc5..a0da07f5c90 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -109,7 +109,7 @@ class GitPushService def push_to_existing_branch?(ref, oldrev) # Return if this is not a push to a branch (e.g. new commits) - Gitlab::Git.branch_ref?(ref) && oldrev != Gitlab::Git::BLANK_SHA + Gitlab::Git.branch_ref?(ref) && !Gitlab::Git.blank_ref?(oldrev) end def push_to_new_branch?(ref, oldrev) diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index e1a99d9cad8..8b50f423984 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -57,9 +57,9 @@ class IrkerWorker end def send_branch_updates(push_data, project, repo_name, committer, branch) - if push_data['before'] == Gitlab::Git::BLANK_SHA + if Gitlab::Git.blank_ref?(push_data['before']) send_new_branch project, repo_name, committer, branch - elsif push_data['after'] == Gitlab::Git::BLANK_SHA + elsif Gitlab::Git.blank_ref?(push_data['after']) send_del_branch repo_name, committer, branch end end @@ -83,7 +83,7 @@ class IrkerWorker return if push_data['total_commits_count'] == 0 # Next message is for number of commit pushed, if any - if push_data['before'] == Gitlab::Git::BLANK_SHA + if Gitlab::Git.blank_ref?(push_data['before']) # Tweak on push_data["before"] in order to have a nice compare URL push_data['before'] = before_on_new_branch push_data, project end diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb index eae9773a067..fdb6a35c78d 100644 --- a/lib/gitlab/force_push_check.rb +++ b/lib/gitlab/force_push_check.rb @@ -3,11 +3,12 @@ module Gitlab def self.force_push?(project, oldrev, newrev) return false if project.empty_repo? - if oldrev != Gitlab::Git::BLANK_SHA && newrev != Gitlab::Git::BLANK_SHA + # Created or deleted branch + if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev) + false + else missed_refs, _ = Gitlab::Popen.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})) missed_refs.split("\n").size > 0 - else - false end end end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 694a30db5df..948cf58fd9a 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -71,7 +71,8 @@ module Gitlab end def checkout_sha(repository, newrev, ref) - if newrev != Gitlab::Git::BLANK_SHA && Gitlab::Git.tag_ref?(ref) + # Find sha for tag, except when it was deleted. + if Gitlab::Git.tag_ref?(ref) && !Gitlab::Git.blank_ref?(newrev) tag_name = Gitlab::Git.ref_name(ref) tag = repository.find_tag(tag_name) -- GitLab From 9487c3703757ba013923125c4fe7774b9cea6f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Wed, 21 Jan 2015 22:06:21 +0100 Subject: [PATCH 1433/1609] Only trigger actual search if a search string is present --- app/controllers/search_controller.rb | 46 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 55926a1ed22..a3284c82d3f 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -2,34 +2,34 @@ class SearchController < ApplicationController include SearchHelper def show + return if params[:search].nil? || params[:search].blank? @project = Project.find_by(id: params[:project_id]) if params[:project_id].present? @group = Group.find_by(id: params[:group_id]) if params[:group_id].present? @scope = params[:scope] @show_snippets = params[:snippets].eql? 'true' - @search_results = if @project - return access_denied! unless can?(current_user, :download_code, @project) - - unless %w(blobs notes issues merge_requests wiki_blobs). - include?(@scope) - @scope = 'blobs' - end - - Search::ProjectService.new(@project, current_user, params).execute - elsif @show_snippets - unless %w(snippet_blobs snippet_titles).include?(@scope) - @scope = 'snippet_blobs' - end - - Search::SnippetService.new(current_user, params).execute - else - unless %w(projects issues merge_requests).include?(@scope) - @scope = 'projects' - end - - Search::GlobalService.new(current_user, params).execute - end - + @search_results = + if @project + return access_denied! unless can?(current_user, :download_code, @project) + + unless %w(blobs notes issues merge_requests wiki_blobs). + include?(@scope) + @scope = 'blobs' + end + + Search::ProjectService.new(@project, current_user, params).execute + elsif @show_snippets + unless %w(snippet_blobs snippet_titles).include?(@scope) + @scope = 'snippet_blobs' + end + + Search::SnippetService.new(current_user, params).execute + else + unless %w(projects issues merge_requests).include?(@scope) + @scope = 'projects' + end + Search::GlobalService.new(current_user, params).execute + end @objects = @search_results.objects(@scope, params[:page]) end -- GitLab From 9ffef6ca1e58c69584a316d4fa5f80e4d97930f4 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 19 Mar 2015 09:57:27 +0000 Subject: [PATCH 1434/1609] Changed tis to this --- doc/install/requirements.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/install/requirements.md b/doc/install/requirements.md index f42af65796f..7a3216dd2d2 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -76,7 +76,7 @@ Notice: The 25 workers of Sidekiq will show up as separate processes in your pro ## Unicorn Workers -It's possible to increase the amount of unicorn workers and tis will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests. +It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests. For most instances we recommend using: CPU cores + 1 = unicorn workers. So for a machine with 2 cores, 3 unicorn workers is ideal. @@ -106,4 +106,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/)) - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) -- IE 10+ +- IE 10+ \ No newline at end of file -- GitLab From 0a3b39d95318f7beb9ee5ab6a72ac03fc3840f8b Mon Sep 17 00:00:00 2001 From: Stefan Tatschner Date: Thu, 19 Mar 2015 12:41:37 +0100 Subject: [PATCH 1435/1609] Update rugments, fixes #8976 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a454461ec26..71936584d6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -519,7 +519,7 @@ GEM rubyntlm (0.5.0) rubypants (0.2.0) rugged (0.21.4) - rugments (1.0.0.beta4) + rugments (1.0.0.beta5) safe_yaml (0.9.7) sanitize (2.1.0) nokogiri (>= 1.4.4) -- GitLab From 81c06ee53ce8c872f26d7626ab98b9a8ed7d3a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Samp=C5=82awski?= Date: Thu, 19 Mar 2015 12:59:04 +0100 Subject: [PATCH 1436/1609] Fix link to URL auto-linking section in markdown help --- doc/markdown/markdown.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 64f28d46451..b66583bb363 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -6,7 +6,7 @@ * [Newlines](#newlines) * [Multiple underscores in words](#multiple-underscores-in-words) -* [URL auto-linking](#url-autolinking) +* [URL auto-linking](#url-auto-linking) * [Code and Syntax Highlighting](#code-and-syntax-highlighting) * [Emoji](#emoji) * [Special GitLab references](#special-gitlab-references) -- GitLab From d411a9e4d8063fdc9b6d0f74cad7345245a1fb0b Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 18 Mar 2015 20:40:16 +0200 Subject: [PATCH 1437/1609] backup repo with tar instead of git bundle --- CHANGELOG | 1 + lib/backup/repository.rb | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b6c39ae5f71..e0b15e93c6b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -86,6 +86,7 @@ v 7.9.0 (unreleased) - Ability to unsubscribe/subscribe to issue or merge request - Delete deploy key when last connection to a project is destroyed. - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) + - Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup) v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index e18bc804437..dfb2da9f84e 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -16,7 +16,7 @@ module Backup if project.empty_repo? $progress.puts "[SKIPPED]".cyan else - cmd = %W(git --git-dir=#{path_to_repo(project)} bundle create #{path_to_bundle(project)} --all) + cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .) output, status = Gitlab::Popen.popen(cmd) if status.zero? $progress.puts "[DONE]".green @@ -64,7 +64,8 @@ module Backup project.namespace.ensure_dir_exist if project.namespace if File.exists?(path_to_bundle(project)) - cmd = %W(git clone --bare #{path_to_bundle(project)} #{path_to_repo(project)}) + FileUtils.mkdir_p(path_to_repo(project)) + cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)}) else cmd = %W(git init --bare #{path_to_repo(project)}) end -- GitLab From b298758b4448c65bd1ca71cca0d5b1c4f235e5ac Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 19 Mar 2015 15:06:05 +0100 Subject: [PATCH 1438/1609] Move "Import existing repository by URL" option to button. --- CHANGELOG | 1 + app/views/projects/new.html.haml | 90 ++++++++++++++++---------------- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 22f38024f93..28e812a91a8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 7.10.0 (unreleased) - Add a service to support external wikis (Hannes Rosenögger) - List new commits for newly pushed branch in activity view. - Add changelog, license and contribution guide links to project sidebar. + - Move "Import existing repository by URL" option to button. v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 7fc612c0c7d..46ef51d8128 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -21,17 +21,55 @@ = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} %hr - .js-toggle-container + + .project-import.js-toggle-container .form-group - .col-sm-2 + %label.control-label Import project from .col-sm-10 - = link_to "#", class: 'js-toggle-button' do - %i.fa.fa-upload - %span Import existing repository by URL + - if github_import_enabled? + = link_to status_import_github_path, class: 'btn' do + %i.fa.fa-github + GitHub + - else + = link_to '#', class: 'how_to_import_link light btn' do + %i.fa.fa-github + GitHub + = render 'github_import_modal' + + + - if bitbucket_import_enabled? + = link_to status_import_bitbucket_path, class: 'btn' do + %i.fa.fa-bitbucket + Bitbucket + - else + = link_to '#', class: 'how_to_import_link light btn' do + %i.fa.fa-bitbucket + Bitbucket + = render 'bitbucket_import_modal' + + - unless request.host == 'gitlab.com' + - if gitlab_import_enabled? + = link_to status_import_gitlab_path, class: 'btn' do + %i.fa.fa-heart + GitLab.com + - else + = link_to '#', class: 'how_to_import_link light btn' do + %i.fa.fa-heart + GitLab.com + = render 'gitlab_import_modal' + + = link_to new_import_gitorious_path, class: 'btn' do + %i.icon-gitorious.icon-gitorious-small + Gitorious.org + + = link_to "#", class: 'btn js-toggle-button' do + %i.fa.fa-git + %span Existing repo by URL + .js-toggle-content.hide .form-group.import-url-data = f.label :import_url, class: 'control-label' do - %span Import existing git repo + %span Git repository URL .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' .alert.alert-info.prepend-top-10 @@ -40,46 +78,6 @@ The import will time out after 4 minutes. For big repositories, use a clone/push combination. For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"} - - .project-import.form-group - %label.control-label Import projects from - .col-sm-10 - - if github_import_enabled? - = link_to status_import_github_path, class: 'btn' do - %i.fa.fa-github - GitHub - - else - = link_to '#', class: 'how_to_import_link light btn' do - %i.fa.fa-github - GitHub - = render 'github_import_modal' - - - - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: 'btn' do - %i.fa.fa-bitbucket - Bitbucket - - else - = link_to '#', class: 'how_to_import_link light btn' do - %i.fa.fa-bitbucket - Bitbucket - = render 'bitbucket_import_modal' - - - unless request.host == 'gitlab.com' - - if gitlab_import_enabled? - = link_to status_import_gitlab_path, class: 'btn' do - %i.fa.fa-heart - GitLab.com - - else - = link_to '#', class: 'how_to_import_link light btn' do - %i.fa.fa-heart - GitLab.com - = render 'gitlab_import_modal' - - = link_to new_import_gitorious_path, class: 'btn' do - %i.icon-gitorious.icon-gitorious-small - Gitorious.org - %hr.prepend-botton-10 .form-group -- GitLab From fad71576f91beb4aae8e8482465a696cdfc3b222 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 12 Mar 2015 09:11:59 -0700 Subject: [PATCH 1439/1609] Fix cross references when usernames, milestones, or project names contain underscores. Remove emphasis from system notes to avoid Markdown conflicts in names. --- CHANGELOG | 1 + app/models/note.rb | 25 ++-- app/services/notification_service.rb | 2 +- doc/api/merge_requests.md | 2 +- doc/api/notes.md | 2 +- spec/lib/gitlab/reference_extractor_spec.rb | 14 ++ spec/models/note_spec.rb | 153 ++++++++++++++++++-- 7 files changed, 175 insertions(+), 24 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b6c39ae5f71..0897a712eaf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) - Add a service to support external wikis (Hannes Rosenögger) diff --git a/app/models/note.rb b/app/models/note.rb index 649e9b4e852..27b583a869a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -59,7 +59,7 @@ class Note < ActiveRecord::Base class << self def create_status_change_note(noteable, project, author, status, source) - body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_" + body = "Status changed to #{status}#{' by ' + source.gfm_reference if source}" create( noteable: noteable, @@ -95,9 +95,9 @@ class Note < ActiveRecord::Base def create_milestone_change_note(noteable, project, author, milestone) body = if milestone.nil? - '_Milestone removed_' + 'Milestone removed' else - "_Milestone changed to #{milestone.title}_" + "Milestone changed to #{milestone.title}" end create( @@ -110,7 +110,7 @@ class Note < ActiveRecord::Base end def create_assignee_change_note(noteable, project, author, assignee) - body = assignee.nil? ? '_Assignee removed_' : "_Reassigned to @#{assignee.username}_" + body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}" create({ noteable: noteable, @@ -140,7 +140,7 @@ class Note < ActiveRecord::Base end message << ' ' << 'label'.pluralize(labels_count) - body = "_#{message.capitalize}_" + body = "#{message.capitalize}" create( noteable: noteable, @@ -170,14 +170,14 @@ class Note < ActiveRecord::Base commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit') - branch = + branch = if merge_request.for_fork? "#{merge_request.target_project_namespace}:#{merge_request.target_branch}" else merge_request.target_branch end - message = "* #{commit_ids} - _#{commits_text} from branch `#{branch}`_" + message = "* #{commit_ids} - #{commits_text} from branch `#{branch}`" body << message body << "\n" end @@ -240,7 +240,7 @@ class Note < ActiveRecord::Base where(noteable_id: noteable.id) end - notes.where('note like ?', cross_reference_note_content(gfm_reference)). + notes.where('note like ?', cross_reference_note_pattern(gfm_reference)). system.any? end @@ -249,13 +249,18 @@ class Note < ActiveRecord::Base end def cross_reference_note_prefix - '_mentioned in ' + 'mentioned in ' end private def cross_reference_note_content(gfm_reference) - cross_reference_note_prefix + "#{gfm_reference}_" + cross_reference_note_prefix + "#{gfm_reference}" + end + + def cross_reference_note_pattern(gfm_reference) + # Older cross reference notes contained underscores for emphasis + "%" + cross_reference_note_content(gfm_reference) + "%" end # Prepend the mentioner's namespaced project path to the GFM reference for diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 848ed77ebf8..cc5853144c5 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -120,7 +120,7 @@ class NotificationService return true unless note.noteable_type.present? # ignore gitlab service messages - return true if note.note.start_with?('_Status changed to closed_') + return true if note.note.start_with?('Status changed to closed') return true if note.cross_reference? && note.system == true opts = { noteable_type: note.noteable_type, project_id: note.project_id } diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 1f3fd26a241..6a272539e45 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -375,7 +375,7 @@ Parameters: } }, { - "note": "_Status changed to closed_", + "note": "Status changed to closed", "author": { "id": 11, "username": "admin", diff --git a/doc/api/notes.md b/doc/api/notes.md index c22e493562a..ee2f9fa0eac 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -21,7 +21,7 @@ Parameters: [ { "id": 302, - "body": "_Status changed to closed_", + "body": "Status changed to closed", "attachment": null, "author": { "id": 1, diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 034f8ee7c45..5ebe44f6fb7 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -120,4 +120,18 @@ describe Gitlab::ReferenceExtractor do expect(extracted[0][1].message).to eq(commit.message) end end + + context 'with a project with an underscore' do + let(:project) { create(:project, path: 'test_project') } + let(:issue) { create(:issue, project: project) } + + it 'handles project issue references' do + subject.analyze("this refers issue #{project.path_with_namespace}##{issue.iid}", + project) + extracted = subject.issues_for(project) + expect(extracted.size).to eq(1) + expect(extracted).to eq([issue]) + end + + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 17cb439c90e..a7bf5081d5b 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -182,14 +182,14 @@ describe Note do describe '#note' do subject { super().note } - it { is_expected.to match(/Status changed to #{status}/) } + it { is_expected.to eq("Status changed to #{status}") } end it 'appends a back-reference if a closing mentionable is supplied' do commit = double('commit', gfm_reference: 'commit 123456') n = Note.create_status_change_note(thing, project, author, status, commit) - expect(n.note).to match(/Status changed to #{status} by commit 123456/) + expect(n.note).to eq("Status changed to #{status} by commit 123456") end end @@ -197,7 +197,7 @@ describe Note do let(:project) { create(:project) } let(:thing) { create(:issue, project: project) } let(:author) { create(:user) } - let(:assignee) { create(:user) } + let(:assignee) { create(:user, username: "assigned_user") } subject { Note.create_assignee_change_note(thing, project, author, assignee) } @@ -227,7 +227,7 @@ describe Note do describe '#note' do subject { super().note } - it { is_expected.to match(/Reassigned to @#{assignee.username}/) } + it { is_expected.to eq('Reassigned to @assigned_user') } end context 'assignee is removed' do @@ -235,11 +235,95 @@ describe Note do describe '#note' do subject { super().note } - it { is_expected.to match(/Assignee removed/) } + it { is_expected.to eq('Assignee removed') } end end end + describe '#create_labels_change_note' do + let(:project) { create(:project) } + let(:thing) { create(:issue, project: project) } + let(:author) { create(:user) } + let(:label1) { create(:label) } + let(:label2) { create(:label) } + let(:added_labels) { [label1, label2] } + let(:removed_labels) { [] } + + subject { Note.create_labels_change_note(thing, project, author, added_labels, removed_labels) } + + context 'creates and saves a Note' do + it { is_expected.to be_a Note } + + describe '#id' do + subject { super().id } + it { is_expected.not_to be_nil } + end + end + + describe '#noteable' do + subject { super().noteable } + it { is_expected.to eq(thing) } + end + + describe '#project' do + subject { super().project } + it { is_expected.to eq(thing.project) } + end + + describe '#author' do + subject { super().author } + it { is_expected.to eq(author) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to eq("Added ~#{label1.id} ~#{label2.id} labels") } + end + + context 'label is removed' do + let(:added_labels) { [label1] } + let(:removed_labels) { [label2] } + + describe '#note' do + subject { super().note } + it { is_expected.to eq("Added ~#{label1.id} and removed ~#{label2.id} labels") } + end + end + end + + describe '#create_milestone_change_note' do + let(:project) { create(:project) } + let(:thing) { create(:issue, project: project) } + let(:milestone) { create(:milestone, project: project, title: "first_milestone") } + let(:author) { create(:user) } + + subject { Note.create_milestone_change_note(thing, project, author, milestone) } + + context 'creates and saves a Note' do + it { is_expected.to be_a Note } + + describe '#id' do + subject { super().id } + it { is_expected.not_to be_nil } + end + end + + describe '#project' do + subject { super().project } + it { is_expected.to eq(thing.project) } + end + + describe '#author' do + subject { super().author } + it { is_expected.to eq(author) } + end + + describe '#note' do + subject { super().note } + it { is_expected.to eq("Milestone changed to first_milestone") } + end + end + describe '#create_cross_reference_note' do let(:project) { create(:project) } let(:author) { create(:user) } @@ -272,7 +356,7 @@ describe Note do describe '#note' do subject { super().note } - it { is_expected.to eq("_mentioned in merge request !#{mergereq.iid}_") } + it { is_expected.to eq("mentioned in merge request !#{mergereq.iid}") } end end @@ -288,7 +372,7 @@ describe Note do describe '#note' do subject { super().note } - it { is_expected.to eq("_mentioned in commit #{commit.sha}_") } + it { is_expected.to eq("mentioned in commit #{commit.sha}") } end end @@ -309,7 +393,7 @@ describe Note do describe '#note' do subject { super().note } - it { is_expected.to eq("_mentioned in issue ##{issue.iid}_") } + it { is_expected.to eq("mentioned in issue ##{issue.iid}") } end end @@ -330,7 +414,7 @@ describe Note do describe '#note' do subject { super().note } - it { is_expected.to eq("_mentioned in merge request !#{mergereq.iid}_") } + it { is_expected.to eq("mentioned in merge request !#{mergereq.iid}") } end end @@ -362,7 +446,7 @@ describe Note do describe '#note' do subject { super().note } - it { is_expected.to eq("_mentioned in issue ##{issue.iid}_") } + it { is_expected.to eq("mentioned in issue ##{issue.iid}") } end end @@ -389,7 +473,7 @@ describe Note do describe '#note' do subject { super().note } - it { is_expected.to eq("_mentioned in commit #{parent_commit.id}_") } + it { is_expected.to eq("mentioned in commit #{parent_commit.id}") } end end end @@ -421,6 +505,41 @@ describe Note do it { expect(Note.cross_reference_exists?(commit0, commit1)).to be_truthy } it { expect(Note.cross_reference_exists?(commit1, commit0)).to be_falsey } end + + context 'legacy note with Markdown emphasis' do + let(:issue2) { create :issue, project: project } + let!(:note) do + create :note, system: true, noteable_id: issue2.id, + noteable_type: "Issue", note: "_mentioned in issue " \ + "#{issue.project.path_with_namespace}##{issue.iid}_" + end + + it 'detects if a mentionable with emphasis has been mentioned' do + expect(Note.cross_reference_exists?(issue2, issue)).to be_truthy + end + end + end + + describe '#cross_references_with_underscores?' do + let(:project) { create :project, path: "first_project" } + let(:second_project) { create :project, path: "second_project" } + + let(:author) { create :user } + let(:issue0) { create :issue, project: project } + let(:issue1) { create :issue, project: second_project } + let!(:note) { Note.create_cross_reference_note(issue0, issue1, author, project) } + + it 'detects if a mentionable has already been mentioned' do + expect(Note.cross_reference_exists?(issue0, issue1)).to be_truthy + end + + it 'detects if a mentionable has not already been mentioned' do + expect(Note.cross_reference_exists?(issue1, issue0)).to be_falsey + end + + it 'detects that text has underscores' do + expect(note.note).to eq("mentioned in issue #{second_project.path_with_namespace}##{issue1.iid}") + end end describe '#system?' do @@ -429,6 +548,8 @@ describe Note do let(:other) { create(:issue, project: project) } let(:author) { create(:user) } let(:assignee) { create(:user) } + let(:label) { create(:label) } + let(:milestone) { create(:milestone) } it 'should recognize user-supplied notes as non-system' do @note = create(:note_on_issue) @@ -449,6 +570,16 @@ describe Note do @note = Note.create_assignee_change_note(issue, project, author, assignee) expect(@note).to be_system end + + it 'should identify label-change notes as system notes' do + @note = Note.create_labels_change_note(issue, project, author, [label], []) + expect(@note).to be_system + end + + it 'should identify milestone-change notes as system notes' do + @note = Note.create_milestone_change_note(issue, project, author, milestone) + expect(@note).to be_system + end end describe :authorization do -- GitLab From e23a1e448b834482453f4e5965a8792d70ce3cd3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 19 Mar 2015 15:43:49 +0100 Subject: [PATCH 1440/1609] Clear up import help. --- app/views/projects/new.html.haml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 46ef51d8128..173a3080b31 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -64,19 +64,22 @@ = link_to "#", class: 'btn js-toggle-button' do %i.fa.fa-git - %span Existing repo by URL + %span Any repo by URL .js-toggle-content.hide .form-group.import-url-data = f.label :import_url, class: 'control-label' do %span Git repository URL .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' + = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' .alert.alert-info.prepend-top-10 - This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. - %br - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"} + %ul + %li + The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: https://username:password@gitlab.company.com/group/project.git. + %li + The import will time out after 4 minutes. For big repositories, use a clone/push combination. + %li + To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"}. %hr.prepend-botton-10 -- GitLab From b617455d2d6fe1e0ae26dec4a8db7612cdd1320a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Mar 2015 08:37:21 -0700 Subject: [PATCH 1441/1609] Dont exit from brakeman rake task --- lib/tasks/brakeman.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake index abcb5f0ae46..3a225801ff2 100644 --- a/lib/tasks/brakeman.rake +++ b/lib/tasks/brakeman.rake @@ -1,7 +1,7 @@ desc 'Security check via brakeman' task :brakeman do if system("brakeman --skip-files lib/backup/repository.rb -w3 -z") - exit 0 + puts 'Security check succeed' else puts 'Security check failed' exit 1 -- GitLab From 1fdf4508d4ccfe941651023de9bc6b70222e651d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Mar 2015 09:12:11 -0700 Subject: [PATCH 1442/1609] Use same font for project description and star links --- app/assets/stylesheets/pages/projects.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 74755951670..6d55a5fa66e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -49,16 +49,20 @@ @extend .clearfix; margin-bottom: 15px; + .project-home-desc, + .star-fork-buttons { + font-size: 16px; + line-height: 1.3; + } + .project-home-desc { float: left; color: #666; - font-size: 16px; } .star-fork-buttons { float: right; min-width: 200px; - font-size: 14px; font-weight: bold; .star-buttons, .fork-buttons { -- GitLab From 19032c03198cc393b8977720a28041b06c203e81 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 19 Mar 2015 17:13:20 +0200 Subject: [PATCH 1443/1609] add canceled CI status --- CHANGELOG | 1 + app/assets/javascripts/merge_request.js.coffee | 2 +- app/assets/stylesheets/pages/merge_requests.scss | 6 ++++++ app/views/projects/merge_requests/show/_mr_ci.html.haml | 6 ++++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b6c39ae5f71..d4ddc2aafe4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -86,6 +86,7 @@ v 7.9.0 (unreleased) - Ability to unsubscribe/subscribe to issue or merge request - Delete deploy key when last connection to a project is destroyed. - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) + - Add canceled status for CI v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 1fee9dc1892..10462ac073d 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -110,7 +110,7 @@ class @MergeRequest showCiState: (state) -> $('.ci_widget').hide() - allowed_states = ["failed", "running", "pending", "success"] + allowed_states = ["failed", "canceled", "running", "pending", "success"] if state in allowed_states $('.ci_widget.ci-' + state).show() else diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 394b59b7e4b..d8fe339b7b3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -155,6 +155,12 @@ background-color: #FAF1F1; } + &.ci-canceled { + color: $gl-warning; + border-color: $gl-danger; + background-color: #FAF5F1; + } + &.ci-error { color: $gl-danger; border-color: $gl-danger; diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml index 85a7103f3bc..ffa3f7b0e36 100644 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml @@ -23,6 +23,12 @@ %i.fa.fa-spinner Checking for CI status for #{@merge_request.last_commit_short_sha} + .ci_widget.ci-canceled{style: "display:none"} + %i.fa.fa-times + %span CI build canceled + for #{@merge_request.last_commit_short_sha}. + = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + .ci_widget.ci-error{style: "display:none"} %i.fa.fa-times %span Cannot connect to the CI server. Please check your settings and try again. -- GitLab From 415517918eca22dff524b5ce3ad48cb852887065 Mon Sep 17 00:00:00 2001 From: DJ Mountney Date: Thu, 19 Mar 2015 13:51:16 -0700 Subject: [PATCH 1444/1609] Update merge_status state to allow more transitions Previously you could only transition from the unchecked state to one of the others. This meant that the mark_as_unmerged call in AutoMergeService would rarily be able to actually transition the state. As it would usually have already been set to can_be_merged before it hit that service. --- app/models/merge_request.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4cbdc612297..798306f6dcc 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -95,11 +95,11 @@ class MergeRequest < ActiveRecord::Base end event :mark_as_mergeable do - transition unchecked: :can_be_merged + transition [:unchecked, :cannot_be_merged] => :can_be_merged end event :mark_as_unmergeable do - transition unchecked: :cannot_be_merged + transition [:unchecked, :can_be_merged] => :cannot_be_merged end state :unchecked -- GitLab From 83d552d50d5485950052a8b9fcba384b81f33c43 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 3 Mar 2015 11:06:17 -0800 Subject: [PATCH 1445/1609] Disable reference creation for comments surrounded by code/preformatted blocks --- CHANGELOG | 1 + lib/gitlab/reference_extractor.rb | 8 +++++++- spec/lib/gitlab/reference_extractor_spec.rb | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b6c39ae5f71..2bcecb402a0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu) - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) - Add a service to support external wikis (Hannes Rosenögger) diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 5b9772de168..1058d4c43d9 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -11,7 +11,13 @@ module Gitlab end def analyze(string, project) - parse_references(string.dup, project) + text = string.dup + + # Remove preformatted/code blocks so that references are not included + text.gsub!(%r{
    .*?
    |.*?}m) { |match| '' } + text.gsub!(%r{^```.*?^```}m) { |match| '' } + + parse_references(text, project) end # Given a valid project, resolve the extracted identifiers of the requested type to diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 034f8ee7c45..7e524aa95cf 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -50,6 +50,26 @@ describe Gitlab::ReferenceExtractor do expect(text).to eq('issue #123 is just the worst, @user') end + it 'extracts no references for
    ..
    blocks' do + subject.analyze("
    def puts '#1 issue'\nend\n
    ```", nil) + expect(subject.issues).to be_blank + end + + it 'extracts no references for .. blocks' do + subject.analyze("def puts '!1 request'\nend\n```", nil) + expect(subject.merge_requests).to be_blank + end + + it 'extracts no references for code blocks with language' do + subject.analyze("this code:\n```ruby\ndef puts '#1 issue'\nend\n```", nil) + expect(subject.issues).to be_blank + end + + it 'extracts issue references for invalid code blocks' do + subject.analyze('test: ```this one talks about issue #1234```', nil) + expect(subject.issues).to eq([{ project: nil, id: '1234' }]) + end + it 'handles all possible kinds of references' do accessors = Gitlab::Markdown::TYPES.map { |t| "#{t}s".to_sym } expect(subject).to respond_to(*accessors) -- GitLab From a879d933784269796fd52cfc4aca1dca833dada7 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 19 Mar 2015 17:43:21 -0700 Subject: [PATCH 1446/1609] Remove mention of branch to commit to. --- app/views/projects/_commit_button.html.haml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index fd8320adb8d..35f7e7bb34b 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -2,8 +2,5 @@ .commit-button-annotation = button_tag 'Commit Changes', class: 'btn commit-btn js-commit-button btn-create' - .message - to branch - %strong= ref = link_to 'Cancel', cancel_path, class: 'btn btn-cancel', data: {confirm: leave_edit_message} -- GitLab From abbea9a0daa009fff4efcee0743307ae27e49d65 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Thu, 19 Mar 2015 19:24:25 -0600 Subject: [PATCH 1447/1609] Move backup permission changes to version 7.10 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4787117cbbc..2bdcfa9e609 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Restrict permissions on backup files v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) @@ -34,7 +35,6 @@ v 7.9.0 (unreleased) - Add a service to send updates to an Irker gateway (Romain Coltel) - Add brakeman (security scanner for Ruby on Rails) - Slack username and channel options - - Restrict permissions on backup files - Add grouped milestones from all projects to dashboard. - Web hook sends pusher email as well as commiter - Add Bitbucket omniauth provider. -- GitLab From 479020ec6b3ecef56016c9ad4dd106dde822eda1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Mar 2015 18:27:26 -0700 Subject: [PATCH 1448/1609] Fix project issues and merge requests pages --- app/helpers/gitlab_routing_helper.rb | 1 - app/views/projects/issues/_issue.html.haml | 2 +- app/views/projects/merge_requests/_merge_request.html.haml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index b005cb8e417..3386fac8657 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -47,6 +47,5 @@ module GitlabRoutingHelper def project_snippet_url(entity, *args) namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) - end end diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 89142a13772..7b06fe72882 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -17,7 +17,7 @@ = issue.notes.count .issue-info - = link_to "##{issue.iid}", project_issue_path(issue.project, issue), class: "light" + = link_to "##{issue.iid}", issue_path(issue), class: "light" - if issue.assignee assigned to #{link_to_member(@project, issue.assignee)} - if issue.votes_count > 0 diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 79e2a5795ea..ecbff722b42 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -22,7 +22,7 @@ %i.fa.fa-comments = merge_request.mr_and_commit_notes.count .merge-request-info - = link_to "##{merge_request.iid}", project_merge_request_path(merge_request.target_project, merge_request), class: "light" + = link_to "##{merge_request.iid}", merge_request_path(merge_request), class: "light" - if merge_request.assignee assigned to #{link_to_member(merge_request.source_project, merge_request.assignee)} - else -- GitLab From efd8491d4906d190abc5e190a2111c04b01b729b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Mar 2015 17:57:37 -0700 Subject: [PATCH 1449/1609] Revert "Increase timeout for Git-over-HTTP requests." This reverts commit 516bcabbf42d60db2ac989dce4c7187b2a1e5de9. Conflicts: Gemfile --- Gemfile | 3 --- Gemfile.lock | 8 -------- config/initializers/timeout.rb | 8 -------- config/unicorn.rb.example | 20 ++++++++++++++++---- lib/gitlab/middleware/timeout.rb | 13 ------------- public/503.html | 13 ------------- 6 files changed, 16 insertions(+), 49 deletions(-) delete mode 100644 config/initializers/timeout.rb delete mode 100644 lib/gitlab/middleware/timeout.rb delete mode 100644 public/503.html diff --git a/Gemfile b/Gemfile index 696e5c1d9d3..f30a64c4c0e 100644 --- a/Gemfile +++ b/Gemfile @@ -180,9 +180,6 @@ gem 'mousetrap-rails' # Detect and convert string character encoding gem 'charlock_holmes' -# Shutting down requests that take too long -gem "slowpoke" - gem "sass-rails", '~> 4.0.2' gem "coffee-rails" gem "uglifier" diff --git a/Gemfile.lock b/Gemfile.lock index 8ab1e3d7683..d0762a7ff5f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -147,7 +147,6 @@ GEM enumerize (0.7.0) activesupport (>= 3.2) equalizer (0.0.8) - errbase (0.0.2) erubis (2.7.0) escape_utils (0.2.4) eventmachine (1.0.4) @@ -429,7 +428,6 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rack-timeout (0.2.0) rails (4.1.9) actionmailer (= 4.1.9) actionpack (= 4.1.9) @@ -482,8 +480,6 @@ GEM rest-client (1.6.7) mime-types (>= 1.16) rinku (1.7.3) - robustly (0.0.3) - errbase rouge (1.7.4) rspec (2.99.0) rspec-core (~> 2.99.0) @@ -566,9 +562,6 @@ GEM temple (~> 0.6.6) tilt (>= 1.3.3, < 2.1) slop (3.6.0) - slowpoke (0.0.5) - rack-timeout (>= 0.1.0) - robustly spinach (0.8.7) colorize (= 0.5.8) gherkin-ruby (>= 0.3.1) @@ -778,7 +771,6 @@ DEPENDENCIES six slack-notifier (~> 1.0.0) slim - slowpoke spinach-rails spring (~> 1.3.1) spring-commands-rspec (= 1.0.4) diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb deleted file mode 100644 index bc88595cf26..00000000000 --- a/config/initializers/timeout.rb +++ /dev/null @@ -1,8 +0,0 @@ -# Slowpoke extends Rack::Timeout to gracefully kill Unicorn workers so they can clean up state. -Slowpoke.timeout = 60 - -# The `Rack::Timeout` middleware kills requests after 60 seconds (as set above). -# We're replacing it with our `Gitlab::Middleware::Timeout` that does the same, -# except ignoring Git-over-HTTP requests, letting those take as long as they need. - -Rails.application.config.middleware.swap(Rack::Timeout, Gitlab::Middleware::Timeout) diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index 3aee718097f..86a5512e761 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -35,10 +35,22 @@ working_directory "/home/git/gitlab" # available in 0.94.0+ listen "/home/git/gitlab/tmp/sockets/gitlab.socket", :backlog => 1024 listen "127.0.0.1:8080", :tcp_nopush => true -# Kill workers after 1 hour. -# A shorter timeout of 60 seconds is enforced by rack-timeout for web requests. -# Git-over-HTTP only has the below timeout since large pulls/pushes can take a long time. -timeout 60 * 60 +# nuke workers after 30 seconds instead of 60 seconds (the default) +# +# NOTICE: git push over http depends on this value. +# If you want be able to push huge amount of data to git repository over http +# you will have to increase this value too. +# +# Example of output if you try to push 1GB repo to GitLab over http. +# -> git push http://gitlab.... master +# +# error: RPC failed; result=18, HTTP code = 200 +# fatal: The remote end hung up unexpectedly +# fatal: The remote end hung up unexpectedly +# +# For more information see http://stackoverflow.com/a/21682112/752049 +# +timeout 60 # feel free to point this anywhere accessible on the filesystem pid "/home/git/gitlab/tmp/pids/unicorn.pid" diff --git a/lib/gitlab/middleware/timeout.rb b/lib/gitlab/middleware/timeout.rb deleted file mode 100644 index 015600392b9..00000000000 --- a/lib/gitlab/middleware/timeout.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Gitlab - module Middleware - class Timeout < Rack::Timeout - GRACK_REGEX = /[-\/\w\.]+\.git\//.freeze - - def call(env) - return @app.call(env) if env['PATH_INFO'] =~ GRACK_REGEX - - super - end - end - end -end diff --git a/public/503.html b/public/503.html deleted file mode 100644 index efdae0f512d..00000000000 --- a/public/503.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - Page took too long to load (503) - - - -

    503

    -

    Page took too long to load.

    -
    -

    Please contact your GitLab administrator if this problem persists.

    - - -- GitLab From d5a259d43ecf6713b443da25b2d765580b8f7e42 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 19 Mar 2015 18:56:05 -0700 Subject: [PATCH 1450/1609] Revert "Update poltergeist to support phantomjs 2.0" This reverts commit d70126c1fbc65c4feeea6fb45424f5d4a30bb797. --- Gemfile | 2 +- Gemfile.lock | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 696e5c1d9d3..ad33116db3e 100644 --- a/Gemfile +++ b/Gemfile @@ -252,7 +252,7 @@ group :development, :test do gem 'rb-inotify', require: linux_only('rb-inotify') # PhantomJS driver for Capybara - gem 'poltergeist', '~> 1.6.0' + gem 'poltergeist', '~> 1.5.1' gem 'jasmine', '2.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index 8ab1e3d7683..71936584d6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -392,7 +392,7 @@ GEM ast (>= 1.1, < 3.0) pg (0.15.1) phantomjs (1.9.2.0) - poltergeist (1.6.0) + poltergeist (1.5.1) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) @@ -650,9 +650,7 @@ GEM webmock (1.16.0) addressable (>= 2.2.7) crack (>= 0.3.2) - websocket-driver (0.5.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) + websocket-driver (0.3.3) wikicloth (0.8.1) builder expression_parser @@ -746,7 +744,7 @@ DEPENDENCIES omniauth-twitter org-ruby (= 0.9.12) pg - poltergeist (~> 1.6.0) + poltergeist (~> 1.5.1) pry-rails quiet_assets (~> 1.0.1) rack-attack -- GitLab From 06aafb73640da21a4277961c5c6da61496f0e8db Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Thu, 19 Mar 2015 19:24:57 -0600 Subject: [PATCH 1451/1609] Call chdir() with a block --- lib/backup/manager.rb | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 1a4f28d106d..c6087830b40 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -11,28 +11,27 @@ module Backup s[:tar_version] = tar_version tar_file = "#{s[:backup_created_at].to_i}_gitlab_backup.tar" - orig_pwd = Dir.pwd - Dir.chdir(Gitlab.config.backup.path) + Dir.chdir(Gitlab.config.backup.path) do + File.open("#{Gitlab.config.backup.path}/backup_information.yml", + "w+") do |file| + file << s.to_yaml.gsub(/^---\n/,'') + end - File.open("#{Gitlab.config.backup.path}/backup_information.yml", "w+") do |file| - file << s.to_yaml.gsub(/^---\n/,'') - end + FileUtils.chmod_R(0700, %w{db uploads repositories}) - FileUtils.chmod_R(0700, %w{db uploads repositories}) + # create archive + $progress.print "Creating backup archive: #{tar_file} ... " + orig_umask = File.umask(0077) + if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) + $progress.puts "done".green + else + puts "creating archive #{tar_file} failed".red + abort 'Backup failed' + end + File.umask(orig_umask) - # create archive - $progress.print "Creating backup archive: #{tar_file} ... " - orig_umask = File.umask(0077) - if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) - $progress.puts "done".green - else - puts "creating archive #{tar_file} failed".red - abort 'Backup failed' + upload(tar_file) end - File.umask(orig_umask) - - upload(tar_file) - Dir.chdir(orig_pwd) end def upload(tar_file) -- GitLab From 243547616789619b807c8b97f7e534f24d1a268c Mon Sep 17 00:00:00 2001 From: Carlos Ribeiro Date: Wed, 18 Mar 2015 23:18:35 -0300 Subject: [PATCH 1452/1609] Add error message when have error on profile screen --- CHANGELOG | 1 + app/controllers/profiles_controller.rb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c96ecc5726e..f38a075fff5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 7.10.0 (unreleased) - Fix alignment of navbar toggle button (Cody Mize) - Identical look of selectboxes in UI - Move "Import existing repository by URL" option to button. + - Improve error message when save profile has error. v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 1b9a86ee42c..3c7f45d559b 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -25,7 +25,8 @@ class ProfilesController < ApplicationController if @user.update_attributes(user_params) flash[:notice] = "Profile was successfully updated" else - flash[:alert] = "Failed to update profile" + messages = @user.errors.full_messages.uniq.join('. ') + flash[:alert] = "Failed to update profile. #{messages}" end respond_to do |format| -- GitLab From 52bf95ae380dc06243d0c4e5c8eb80f8be15a4f3 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Tue, 17 Mar 2015 21:17:00 -0600 Subject: [PATCH 1453/1609] Change HTML sanitization Use the `SanitizationFilter` class from the html-pipeline gem for inline HTML instead of calling the Rails `sanitize` method. --- app/helpers/gitlab_markdown_helper.rb | 2 +- doc/markdown/markdown.md | 59 +-------------------------- lib/gitlab/markdown.rb | 36 ++++++++++------ 3 files changed, 25 insertions(+), 72 deletions(-) diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 7bafbbd5f3f..6df506e835d 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -49,7 +49,7 @@ module GitlabMarkdownHelper space_after_headers: true, superscript: true) end - @markdown.render(sanitize_html(text)).html_safe + @markdown.render(text).html_safe end # Return the first line of +text+, up to +max_chars+, after parsing the line diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index ddf1bbc6ee4..4ab73df8af9 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -440,64 +440,7 @@ Note that inline HTML is disabled in the default Gitlab configuration, although
    Does *not* work **very** well. Use HTML tags.
    -The following tags can be used: - -* `` -* `` -* `` -* `
    ` -* `` -* `` -* `
    ` -* `
    ` -* `` -* `` -* `
    ` -* `` -* `` -* `
    ` -* `
    ` -* `
    ` -* `` -* `

    ` -* `

    ` -* `

    ` -* `

    ` -* `

    ` -* `
    ` -* `
    ` -* `` -* `` -* `` -* `` -* `
  • ` -* `
      ` -* `

      ` -* `

      `
      -* ``
      -* ``
      -* ``
      -* ``
      -* ``
      -* ``
      -* ``
      -* `
        ` -* `` - -You can also use the following HTML attributes in your inline tags: - -* `abbr` -* `alt` -* `cite` -* `class` -* `datetime` -* `height` -* `href` -* `name` -* `src` -* `title` -* `width` -* `xml:lang` +See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows the `class`, `id`, and `style` attributes. ## Horizontal Rule diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 32f04c866e3..cd70fd5e85b 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -79,15 +79,34 @@ module Gitlab # Used markdown pipelines in GitLab: # GitlabEmojiFilter - performs emoji replacement. + # SanitizationFilter - remove unsafe HTML tags and attributes # # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters filters = [ - HTML::Pipeline::Gitlab::GitlabEmojiFilter + HTML::Pipeline::Gitlab::GitlabEmojiFilter, + HTML::Pipeline::SanitizationFilter ] + whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST + whitelist[:attributes][:all].push('class', 'id', 'style') + + # Remove the rel attribute that the sanitize gem adds, and remove the + # href attribute if it contains inline javascript + fix_anchors = lambda do |env| + name, node = env[:node_name], env[:node] + if name == 'a' + node.remove_attribute('rel') + if node['href'] && node['href'].match('javascript:') + node.remove_attribute('href') + end + end + end + whitelist[:transformers].push(fix_anchors) + markdown_context = { asset_root: Gitlab.config.gitlab.url, - asset_host: Gitlab::Application.config.asset_host + asset_host: Gitlab::Application.config.asset_host, + whitelist: whitelist } markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline @@ -97,22 +116,13 @@ module Gitlab if options[:xhtml] saveoptions |= Nokogiri::XML::Node::SaveOptions::AS_XHTML end - text = result[:output].to_html(save_with: saveoptions) - sanitize_html(text) - end - - # Remove HTML tags and attributes that are not whitelisted - def sanitize_html(text) - allowed_attributes = ActionView::Base.sanitized_allowed_attributes - allowed_tags = ActionView::Base.sanitized_allowed_tags + text = result[:output].to_html(save_with: saveoptions) - text = sanitize text.html_safe, - attributes: allowed_attributes + %w(id class style), - tags: allowed_tags + %w(table tr td th) if options[:parse_tasks] text = parse_tasks(text) end + text end -- GitLab From 189dc4dc814ec56b8184cc5ab3a71b679dd16fdd Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 19 Mar 2015 23:49:49 -0700 Subject: [PATCH 1454/1609] REaadme. Issue #1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9476a811c8..526fcedab0d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ![logo](https://about.gitlab.com/images/gitlab_logo.png) GitLab sdfsd ## Open source software to collaborate on code - +#### ![Animated screenshots](https://about.gitlab.com/images/animated/compiled.gif) - Manage Git repositories with fine grained access controls that keep your code secure -- GitLab From c6dd117c71a326a09a1e6d546d6d0c98a21e3f1d Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 19 Mar 2015 23:52:33 -0700 Subject: [PATCH 1455/1609] Revert "Merge branch 'new_branch' into 'master'" This reverts commit 00607d4f960631f7396ea9afd39859d17fa9f12c, reversing changes made to edfc7d0d268b6e73a30f501c3897965c111c262a. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 526fcedab0d..0563ceca409 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ![logo](https://about.gitlab.com/images/gitlab_logo.png) GitLab -sdfsd + ## Open source software to collaborate on code -#### + ![Animated screenshots](https://about.gitlab.com/images/animated/compiled.gif) - Manage Git repositories with fine grained access controls that keep your code secure -- GitLab From 5df1609c82483f6477b2174c216e540417a465cb Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 20 Mar 2015 14:47:19 +0200 Subject: [PATCH 1456/1609] update doorkeeper to 2.1.3 --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 128c0b4526b..87d765b095e 100644 --- a/Gemfile +++ b/Gemfile @@ -31,7 +31,7 @@ gem 'omniauth-shibboleth' gem 'omniauth-kerberos' gem 'omniauth-gitlab' gem 'omniauth-bitbucket' -gem 'doorkeeper', '2.1.0' +gem 'doorkeeper', '2.1.3' gem "rack-oauth2", "~> 1.0.5" # Browser detection diff --git a/Gemfile.lock b/Gemfile.lock index 54753f5e016..044788dee6c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -136,8 +136,8 @@ GEM diff-lcs (1.2.5) diffy (3.0.3) docile (1.1.5) - doorkeeper (2.1.0) - railties (>= 3.1) + doorkeeper (2.1.3) + railties (>= 3.2) dotenv (0.9.0) dropzonejs-rails (0.4.14) rails (> 3.1) @@ -683,7 +683,7 @@ DEPENDENCIES devise (= 3.2.4) devise-async (= 0.9.0) diffy (~> 3.0.3) - doorkeeper (= 2.1.0) + doorkeeper (= 2.1.3) dropzonejs-rails email_spec enumerize -- GitLab From 484524e0968e168183a8e22599e062d29d1d81fe Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Fri, 20 Mar 2015 15:15:56 +0200 Subject: [PATCH 1457/1609] gollum-lib update --- Gemfile | 2 +- Gemfile.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 87d765b095e..285ccf32b66 100644 --- a/Gemfile +++ b/Gemfile @@ -48,7 +48,7 @@ gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap" # Git Wiki -gem 'gollum-lib', '~> 4.0.0' +gem 'gollum-lib', '~> 4.0.2' # Language detection gem "gitlab-linguist", "~> 3.0.1", require: "linguist" diff --git a/Gemfile.lock b/Gemfile.lock index 044788dee6c..80eebc16e4c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -223,11 +223,11 @@ GEM omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.1) rubyntlm (~> 0.3) - gollum-grit_adapter (0.1.0) - gitlab-grit (~> 2.7.1) - gollum-lib (4.0.0) + gollum-grit_adapter (0.1.3) + gitlab-grit (~> 2.7, >= 2.7.1) + gollum-lib (4.0.2) github-markup (~> 1.3.1) - gollum-grit_adapter (~> 0.1.0) + gollum-grit_adapter (~> 0.1, >= 0.1.1) nokogiri (~> 1.6.4) rouge (~> 1.7.4) sanitize (~> 2.1.0) @@ -480,7 +480,7 @@ GEM rest-client (1.6.7) mime-types (>= 1.16) rinku (1.7.3) - rouge (1.7.4) + rouge (1.7.7) rspec (2.99.0) rspec-core (~> 2.99.0) rspec-expectations (~> 2.99.0) @@ -701,7 +701,7 @@ DEPENDENCIES gitlab_git (~> 7.1.2) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) - gollum-lib (~> 4.0.0) + gollum-lib (~> 4.0.2) gon (~> 5.0.0) grape (~> 0.6.1) grape-entity (~> 0.4.2) -- GitLab From e5fe14b2fb6590e71c7853d38ff74f32bc6b4ed4 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 5 Mar 2015 16:58:04 +0200 Subject: [PATCH 1458/1609] Link to CI with ref --- CHANGELOG | 1 + .../projects/merge_requests_controller.rb | 4 ++-- app/helpers/merge_requests_helper.rb | 2 +- app/models/project_services/bamboo_service.rb | 4 ++-- .../project_services/buildbox_service.rb | 4 ++-- app/models/project_services/ci_service.rb | 4 ++-- .../project_services/gitlab_ci_service.rb | 20 +++++++++---------- .../project_services/teamcity_service.rb | 4 ++-- .../project_services/buildbox_service_spec.rb | 2 +- .../gitlab_ci_service_spec.rb | 4 ++-- 10 files changed, 25 insertions(+), 24 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f38a075fff5..07d0b5920bd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ v 7.10.0 (unreleased) - Identical look of selectboxes in UI - Move "Import existing repository by URL" option to button. - Improve error message when save profile has error. + - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index c63a9b0cd44..e9b7d7e0083 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -160,10 +160,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController def ci_status ci_service = @merge_request.source_project.ci_service - status = ci_service.commit_status(merge_request.last_commit.sha) + status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.last_commit.sha) + coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) end response = { diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 3b1589da57f..51b60770e0b 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -35,7 +35,7 @@ module MergeRequestsHelper end def ci_build_details_path(merge_request) - merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha) + merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) end def merge_path_description(merge_request, separator) diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 0100f1e4a10..f968afe9fa8 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -93,7 +93,7 @@ class BambooService < CiService end end - def build_page(sha) + def build_page(sha, ref) build_info(sha) if @response.nil? || !@response.code if @response.code != 200 || @response['results']['results']['size'] == '0' @@ -106,7 +106,7 @@ class BambooService < CiService end end - def commit_status(sha) + def commit_status(sha, ref) build_info(sha) if @response.nil? || !@response.code return :error unless @response.code == 200 || @response.code == 404 diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb index 270863c1576..fef1c9b7349 100644 --- a/app/models/project_services/buildbox_service.rb +++ b/app/models/project_services/buildbox_service.rb @@ -48,7 +48,7 @@ class BuildboxService < CiService service_hook.execute(data) end - def commit_status(sha) + def commit_status(sha, ref) response = HTTParty.get(commit_status_path(sha), verify: false) if response.code == 200 && response['status'] @@ -62,7 +62,7 @@ class BuildboxService < CiService "#{buildbox_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}" end - def build_page(sha) + def build_page(sha, ref) "#{project_url}/builds?commit=#{sha}" end diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index c6f6b4952c9..1a36e439245 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -34,7 +34,7 @@ class CiService < Service # Ex. # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c # - def build_page(sha) + def build_page(sha, ref) # implement inside child end @@ -51,7 +51,7 @@ class CiService < Service # # => 'running' # # - def commit_status(sha) + def commit_status(sha, ref) # implement inside child end end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index d81623625c9..edaeeffc228 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -40,17 +40,17 @@ class GitlabCiService < CiService service_hook.execute(data) end - def commit_status_path(sha) - project_url + "/commits/#{sha}/status.json?token=#{token}" + def commit_status_path(sha, ref) + project_url + "/refs/#{ref}/commits/#{sha}/status.json?token=#{token}" end - def get_ci_build(sha) + def get_ci_build(sha, ref) @ci_builds ||= {} - @ci_builds[sha] ||= HTTParty.get(commit_status_path(sha), verify: false) + @ci_builds[sha] ||= HTTParty.get(commit_status_path(sha, ref), verify: false) end - def commit_status(sha) - response = get_ci_build(sha) + def commit_status(sha, ref) + response = get_ci_build(sha, ref) if response.code == 200 and response["status"] response["status"] @@ -59,16 +59,16 @@ class GitlabCiService < CiService end end - def commit_coverage(sha) - response = get_ci_build(sha) + def commit_coverage(sha, ref) + response = get_ci_build(sha, ref) if response.code == 200 and response["coverage"] response["coverage"] end end - def build_page(sha) - project_url + "/commits/#{sha}" + def build_page(sha, ref) + project_url + "/refs/#{ref}/commits/#{sha}" end def builds_path diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 7403e19da9a..c26bc551352 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -88,7 +88,7 @@ class TeamcityService < CiService @response = HTTParty.get("#{url}", verify: false, basic_auth: auth) end - def build_page(sha) + def build_page(sha, ref) build_info(sha) if @response.nil? || !@response.code if @response.code != 200 @@ -103,7 +103,7 @@ class TeamcityService < CiService end end - def commit_status(sha) + def commit_status(sha, ref) build_info(sha) if @response.nil? || !@response.code return :error unless @response.code == 200 || @response.code == 404 diff --git a/spec/models/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildbox_service_spec.rb index 39d7df54cf0..fcbf3e45b9a 100644 --- a/spec/models/project_services/buildbox_service_spec.rb +++ b/spec/models/project_services/buildbox_service_spec.rb @@ -59,7 +59,7 @@ describe BuildboxService do describe :build_page do it 'returns the correct build page' do - expect(@service.build_page('2ab7834c')).to eq( + expect(@service.build_page('2ab7834c', nil)).to eq( 'https://buildbox.io/account-name/example-project/builds?commit=2ab7834c' ) end diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb index 8bfb19e524b..610f33c5823 100644 --- a/spec/models/project_services/gitlab_ci_service_spec.rb +++ b/spec/models/project_services/gitlab_ci_service_spec.rb @@ -39,11 +39,11 @@ describe GitlabCiService do end describe :commit_status_path do - it { expect(@service.commit_status_path("2ab7834c")).to eq("http://ci.gitlab.org/projects/2/commits/2ab7834c/status.json?token=verySecret")} + it { expect(@service.commit_status_path("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c/status.json?token=verySecret")} end describe :build_page do - it { expect(@service.build_page("2ab7834c")).to eq("http://ci.gitlab.org/projects/2/commits/2ab7834c")} + it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c")} end end end -- GitLab From 6c8b1192636ffff94556014c059c9b7f9bed4b18 Mon Sep 17 00:00:00 2001 From: Andrew Tomaka Date: Fri, 20 Mar 2015 13:43:52 -0400 Subject: [PATCH 1459/1609] Fix newline spacing after authorized_keys rebuild --- lib/tasks/gitlab/shell.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 9af93300e08..e835d6cb9b7 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -112,6 +112,7 @@ namespace :gitlab do print '.' end end + puts "" unless $?.success? puts "Failed to add keys...".red -- GitLab From d735321ebdee53b84ea45090227cf2a571a534e0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 11:32:44 -0700 Subject: [PATCH 1460/1609] Improve project name truncation on dashboard to work well with sm screen --- app/assets/stylesheets/pages/dashboard.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index e408211fc7d..af9c83e5dc8 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -29,7 +29,7 @@ line-height: 24px; .str-truncated { - max-width: 75%; + max-width: 72%; } a { -- GitLab From 71e6871737bebbb7164f1c0853120cd209428259 Mon Sep 17 00:00:00 2001 From: Jozef Vaclavik Date: Fri, 20 Mar 2015 20:03:14 +0100 Subject: [PATCH 1461/1609] Separate Dockerfile for Data and Application --- docker/Dockerfile | 4 ---- docker/README.md | 38 +++++++++++++++++++++++------- docker/data/Dockerfile | 8 +++++++ docker/{ => data}/assets/gitlab.rb | 0 4 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 docker/data/Dockerfile rename docker/{ => data}/assets/gitlab.rb (100%) diff --git a/docker/Dockerfile b/docker/Dockerfile index 4eb280f9554..6c351f727ff 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,11 +26,7 @@ RUN mkdir -p /opt/gitlab/sv/sshd/supervise \ # Expose web & ssh EXPOSE 80 22 -# Declare volumes -VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"] - # Copy assets -COPY assets/gitlab.rb /etc/gitlab/ COPY assets/wrapper /usr/local/bin/ # Wrapper to handle signal, trigger runit and reconfigure GitLab diff --git a/docker/README.md b/docker/README.md index 58982a238a8..b7e8b0db7e7 100644 --- a/docker/README.md +++ b/docker/README.md @@ -8,14 +8,15 @@ GitLab offers git repository management, code reviews, issue tracking, activity ![GitLab Logo](https://gitlab.com/uploads/appearance/logo/1/brand_logo-c37eb221b456bb4b472cc1084480991f.png) -How to use this image +How to use these images ====================== -At this moment GitLab doesn't have official Docker images. -Build your own based on the Omnibus packages with the following command (it assumes you're in the GitLab repo root directory): +At this moment GitLab doesn't have official Docker images. For convinience we will use suffix _xy where xy is current version of GitLab. +Build your own based on the Omnibus packages with the following commands (it assumes you're in the GitLab repo root directory): ```bash -sudo docker build --tag gitlab_image docker/ +sudo docker build --tag gitlab_data_image docker/data/ +sudo docker build --tag gitlab_app_image_xy docker/ ``` We assume using a data volume container, this will simplify migrations and backups. @@ -30,16 +31,16 @@ The directories on data container are: Create the data container with: ```bash -sudo docker run --name gitlab_data gitlab_image /bin/true +sudo docker run --name gitlab_data gitlab_data_image /bin/true ``` -After creating this run GitLab: +After creating data container run GitLab container: ```bash -sudo docker run --detach --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image +sudo docker run --detach --name gitlab_app_xy --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_app_image_xy ``` -It might take a while before the docker container is responding to queries. You can follow the configuration process with `docker logs -f gitlab_app`. +It might take a while before the docker container is responding to queries. You can follow the configuration process with `sudo docker logs -f gitlab_app_xy`. You can then go to `http://localhost:8080/` (or `http://192.168.59.103:8080/` if you use boot2docker). You can login with username `root` and password `5iveL!fe`. @@ -54,7 +55,7 @@ This container uses the official Omnibus GitLab distribution, so all configurati To access GitLab configuration, you can start an interactive command line in a new container using the shared data volume container, you will be able to browse the 3 directories and use your favorite text editor: ```bash -docker run -ti -e TERM=linux --rm --volumes-from gitlab_data ubuntu +sudo docker run -ti -e TERM=linux --rm --volumes-from gitlab_data ubuntu vi /etc/gitlab/gitlab.rb ``` @@ -62,6 +63,25 @@ vi /etc/gitlab/gitlab.rb You can find all available options in [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration). +How to upgrade GitLab +======================== + +To updgrade GitLab to new versions, stop running container, create new docker image and container from that image. + +It Assumes that you're upgrading from 7.8 to 7.9 and you're in the updated GitLab repo root directory: + +```bash +sudo docker stop gitlab_app_78 +sudo docker build --tag gitlab_app_image_79 docker/ +sudo docker run --detach --name gitlab_app_79 --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_app_image_79 +``` + +On the first run GitLab will reconfigure and update itself. If everything runs OK don't forget to cleanup old container and image: + +```bash +sudo docker rm gitlab_app_78 +sudo docker rmi gitlab_app_image_78 +``` Troubleshooting ========================= diff --git a/docker/data/Dockerfile b/docker/data/Dockerfile new file mode 100644 index 00000000000..ea0175c4aa2 --- /dev/null +++ b/docker/data/Dockerfile @@ -0,0 +1,8 @@ +FROM busybox + +# Declare volumes +VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"] +# Copy assets +COPY assets/gitlab.rb /etc/gitlab/ + +CMD /bin/sh diff --git a/docker/assets/gitlab.rb b/docker/data/assets/gitlab.rb similarity index 100% rename from docker/assets/gitlab.rb rename to docker/data/assets/gitlab.rb -- GitLab From 2852d0b6207fd1b14227a659a90d80fffa544e4c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 12:03:48 -0700 Subject: [PATCH 1462/1609] Fix commits routing for branches with slash --- config/routes.rb | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 0950bed3cf1..b1aeb686752 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -242,7 +242,7 @@ Gitlab::Application.routes.draw do resources :group_members, only: [:index, :create, :update, :destroy] do delete :leave, on: :collection end - + resource :avatar, only: [:destroy] resources :milestones, only: [:index, :show, :update] end @@ -318,14 +318,6 @@ Gitlab::Application.routes.draw do as: :tree ) end - resource :avatar, only: [:show, :destroy] - - resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do - get :branches, on: :member - end - - resources :commits, only: [:show], constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } - resources :compare, only: [:index, :create] scope do get( @@ -336,8 +328,25 @@ Gitlab::Application.routes.draw do ) end - resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } - resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do + scope do + get( + '/commits/*id', + to: 'commits#show', + constraints: { id: /.+/, format: /(html|js)/ }, + as: :commits + ) + end + + resource :avatar, only: [:show, :destroy] + resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do + get :branches, on: :member + end + + resources :commits, only: [:show], constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } + resources :compare, only: [:index, :create] + resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } + + resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do member do get :commits end -- GitLab From 1f711fd93d9e8f9955d59946469df2b6c7aa9c42 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 12:34:21 -0700 Subject: [PATCH 1463/1609] Remove upvotes js logic since it does not work as expected anyway --- app/assets/javascripts/notes.js.coffee | 2 +- app/assets/javascripts/notes_votes.js.coffee | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 app/assets/javascripts/notes_votes.js.coffee diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 90e6fd6d154..c366c98cf54 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -425,7 +425,7 @@ class @Notes @removeDiscussionNoteForm(form) updateVotes: -> - (new NotesVotes).updateVotes() + true ### Called after an attachment file has been selected. diff --git a/app/assets/javascripts/notes_votes.js.coffee b/app/assets/javascripts/notes_votes.js.coffee deleted file mode 100644 index 65c149b7886..00000000000 --- a/app/assets/javascripts/notes_votes.js.coffee +++ /dev/null @@ -1,20 +0,0 @@ -class @NotesVotes - updateVotes: -> - votes = $("#votes .votes") - notes = $("#notes-list .note .vote") - - # only update if there is a vote display - if votes.size() - upvotes = notes.filter(".upvote").size() - downvotes = notes.filter(".downvote").size() - votesCount = upvotes + downvotes - upvotesPercent = (if votesCount then (100.0 / votesCount * upvotes) else 0) - downvotesPercent = (if votesCount then (100.0 - upvotesPercent) else 0) - - # change vote bar lengths - votes.find(".bar-success").css "width", upvotesPercent + "%" - votes.find(".bar-danger").css "width", downvotesPercent + "%" - - # replace vote numbers - votes.find(".upvotes").text votes.find(".upvotes").text().replace(/\d+/, upvotes) - votes.find(".downvotes").text votes.find(".downvotes").text().replace(/\d+/, downvotes) -- GitLab From 085e5084a8bd5034b94bc7f4e0dc8302cd71c323 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 12:45:45 -0700 Subject: [PATCH 1464/1609] Make issue and merge request sidebar more compact * move votes block to participants * make smaller font in sidebar --- app/assets/stylesheets/pages/issuable.scss | 8 ++++- app/assets/stylesheets/pages/votes.scss | 35 ------------------- .../projects/issues/_discussion.html.haml | 12 +++---- .../projects/issues/_issue_context.html.haml | 4 +-- .../merge_requests/_discussion.html.haml | 10 +++--- app/views/votes/_votes_block.html.haml | 14 +++++--- 6 files changed, 26 insertions(+), 57 deletions(-) diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index d8d12338859..a640a4e2051 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -25,7 +25,7 @@ } .issuable-context-title { - font-size: 15px; + font-size: 14px; line-height: 1.4; margin-bottom: 5px; @@ -39,3 +39,9 @@ margin-right: 4px; } } + +.issuable-affix .context { + font-size: 13px; + + .btn { font-size: 13px; } +} diff --git a/app/assets/stylesheets/pages/votes.scss b/app/assets/stylesheets/pages/votes.scss index ba0a519dca6..dc9a7d71e8b 100644 --- a/app/assets/stylesheets/pages/votes.scss +++ b/app/assets/stylesheets/pages/votes.scss @@ -1,39 +1,4 @@ -.votes { - font-size: 13px; - line-height: 15px; - .progress { - height: 4px; - margin: 0; - .bar { - float: left; - height: 100%; - } - .bar-success { - @include linear-gradient(#62C462, #51A351); - background-color: #468847; - } - .bar-danger { - @include linear-gradient(#EE5F5B, #BD362F); - background-color: #B94A48; - } - } - .upvotes { - display: inline-block; - color: #468847; - } - .downvotes { - display: inline-block; - color: #B94A48; - } -} -.votes-block { - margin: 6px; - .downvotes { - float: right; - } -} .votes-inline { display: inline-block; margin: 0 8px; } - diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index fc3e35640dc..0d3028d50b4 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -6,11 +6,12 @@ = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue" .row %section.col-md-9 + .votes-holder.pull-right + #votes= render 'votes/votes_block', votable: @issue .participants %span= pluralize(@issue.participants.count, 'participant') - @issue.participants.each do |participant| = link_to_member(@project, participant, name: false, size: 24) - .voting_notes#notes= render "projects/notes/notes_with_form" %aside.col-md-3 .issuable-affix @@ -20,15 +21,10 @@ %hr .context = render partial: 'issue_context', locals: { issue: @issue } - %hr - .clearfix - .votes-holder - %h6 Votes - #votes= render 'votes/votes_block', votable: @issue - if @issue.labels.any? - %hr - %h6 Labels + .issuable-context-title + %label Labels .issue-show-labels - @issue.labels.each do |label| = link_to namespace_project_issues_path(@project.namespace, @project, label_name: label.name) do diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index d43ce0aa293..91fe0b68371 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -45,5 +45,5 @@ :coffeescript $ -> new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") - - \ No newline at end of file + + diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 79a093dc775..eb72eaabd8b 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -7,6 +7,8 @@ .row %section.col-md-9 + .votes-holder.pull-right + #votes= render 'votes/votes_block', votable: @merge_request = render "projects/merge_requests/show/participants" = render "projects/notes/notes_with_form" %aside.col-md-3 @@ -17,14 +19,10 @@ %hr .context = render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request } - %hr - .votes-holder - %h6 Votes - #votes= render 'votes/votes_block', votable: @merge_request - if @merge_request.labels.any? - %hr - %h6 Labels + .issuable-context-title + %label Labels .merge-request-show-labels - @merge_request.labels.each do |label| = link_to namespace_project_merge_requests_path(@project.namespace, @project, label_name: label.name) do diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 788d9065a7b..36ea6742064 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,6 +1,10 @@ .votes.votes-block - .progress - .progress-bar.progress-bar-success{style: "width: #{votable.upvotes_in_percent}%;"} - .progress-bar.progress-bar-danger{style: "width: #{votable.downvotes_in_percent}%;"} - .upvotes= "#{votable.upvotes} up" - .downvotes= "#{votable.downvotes} down" + .btn-group + - unless votable.upvotes.zero? + .btn.btn-sm.disabled.cgreen + %i.fa.fa-thumbs-up + = votable.upvotes + - unless votable.downvotes.zero? + .btn.btn-sm.disabled.cred + %i.fa.fa-thumbs-down + = votable.downvotes -- GitLab From 651397513ae302ffd36e10cf5f9ba43c1d673bdc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 13:01:43 -0700 Subject: [PATCH 1465/1609] Fix commits routing --- config/routes.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index b1aeb686752..03b4a32a9dd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -332,7 +332,7 @@ Gitlab::Application.routes.draw do get( '/commits/*id', to: 'commits#show', - constraints: { id: /.+/, format: /(html|js)/ }, + constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }, as: :commits ) end @@ -342,7 +342,6 @@ Gitlab::Application.routes.draw do get :branches, on: :member end - resources :commits, only: [:show], constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } resources :compare, only: [:index, :create] resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } -- GitLab From b0e24413308555ab4a5970e66f7458384771efd2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 15:33:13 -0700 Subject: [PATCH 1466/1609] Improve issue sidebar position --- app/assets/javascripts/issue.js.coffee | 2 +- app/assets/javascripts/merge_request.js.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index f2753170478..bf71c144eaf 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -19,6 +19,6 @@ class @Issue $('.issue-details').waitForImages -> $('.issuable-affix').affix offset: top: -> - @top = $('.issue-details').outerHeight(true) + 25 + @top = ($('.issuable-affix').offset().top - 70) bottom: -> @bottom = $('.footer').outerHeight(true) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 10462ac073d..09c202e42a5 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -23,7 +23,7 @@ class @MergeRequest $('.merge-request-details').waitForImages -> $('.issuable-affix').affix offset: top: -> - @top = $('.merge-request-details').outerHeight(true) + 91 + @top = ($('.issuable-affix').offset().top - 70) bottom: -> @bottom = $('.footer').outerHeight(true) -- GitLab From 9f9fed4fa2446edfdc2295fa1e910e9ed09ca677 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 15:34:29 -0700 Subject: [PATCH 1467/1609] Fix votes inline rendering --- app/views/votes/_votes_inline.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/votes/_votes_inline.html.haml b/app/views/votes/_votes_inline.html.haml index ee805474830..2cb3ae04e1a 100644 --- a/app/views/votes/_votes_inline.html.haml +++ b/app/views/votes/_votes_inline.html.haml @@ -1,9 +1,9 @@ .votes.votes-inline - unless votable.upvotes.zero? - .upvotes + %span.upvotes.cgreen + #{votable.upvotes} - unless votable.downvotes.zero? \/ - unless votable.downvotes.zero? - .downvotes + %span.downvotes.cred \- #{votable.downvotes} -- GitLab From b7229356d57b13996538c583cb2044748a32eb6e Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 20 Mar 2015 15:54:11 -0700 Subject: [PATCH 1468/1609] Change the name of the key used for bitbucket importer. --- config/initializers/public_key.rb | 2 +- doc/integration/bitbucket.md | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb index 75d74e3625d..b70c665f80d 100644 --- a/config/initializers/public_key.rb +++ b/config/initializers/public_key.rb @@ -1,2 +1,2 @@ -path = File.expand_path("~/.ssh/id_rsa.pub") +path = File.expand_path("~/.ssh/bitbucket_isa.pub") Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path) diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index cc6389f5aaf..d82e1f8b41b 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -2,7 +2,7 @@ Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account. -To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket. +To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket. Bitbucket will generate an application ID and secret key for you to use. 1. Sign in to Bitbucket. @@ -19,8 +19,8 @@ Bitbucket will generate an application ID and secret key for you to use. - URL: The URL to your GitLab installation. 'https://gitlab.company.com' 1. Select "Save". -1. You should now see a Key and Secret in the list of OAuth customers. - Keep this page open as you continue configuration. +1. You should now see a Key and Secret in the list of OAuth customers. + Keep this page open as you continue configuration. 1. On your GitLab server, open the configuration file. @@ -70,13 +70,13 @@ Bitbucket will generate an application ID and secret key for you to use. 1. Restart GitLab for the changes to take effect. -On the sign in page there should now be a Bitbucket icon below the regular sign in form. -Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application. +On the sign in page there should now be a Bitbucket icon below the regular sign in form. +Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. ## Bitbucket project import -To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com. +To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com. Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key. @@ -95,7 +95,7 @@ To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org ```sh The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established. RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40. - Are you sure you want to continue connecting (yes/no)? + Are you sure you want to continue connecting (yes/no)? ``` 1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts. @@ -104,7 +104,7 @@ To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org ### Step 2: Public key -To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/id_rsa.pub`, which will expand to `/home/git/.ssh/id_rsa.pub` in most configurations. +To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations. If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following: @@ -114,6 +114,7 @@ If you have that file in place, you're all set and should see the "Import projec sudo -u git -H ssh-keygen ``` + When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`. Make sure to use an **empty passphrase**. 2. Restart GitLab to allow it to find the new public key. -- GitLab From 603b88ab5c79d64e7425ca386a1f3faab89bfdc2 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 20 Mar 2015 16:13:03 -0700 Subject: [PATCH 1469/1609] Fix typo. --- config/initializers/public_key.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb index b70c665f80d..e4f09a2d020 100644 --- a/config/initializers/public_key.rb +++ b/config/initializers/public_key.rb @@ -1,2 +1,2 @@ -path = File.expand_path("~/.ssh/bitbucket_isa.pub") +path = File.expand_path("~/.ssh/bitbucket_rsa.pub") Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path) -- GitLab From 6ef20926ee4cd7cefc6aaff9346a964a34f64a89 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 16:52:23 -0700 Subject: [PATCH 1470/1609] Add location to user profile --- CHANGELOG | 1 + app/controllers/profiles_controller.rb | 2 +- app/views/profiles/show.html.haml | 3 +++ db/migrate/20150320234437_add_location_to_user.rb | 5 +++++ db/schema.rb | 3 ++- features/steps/profile/profile.rb | 2 ++ 6 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20150320234437_add_location_to_user.rb diff --git a/CHANGELOG b/CHANGELOG index e0676b30ce8..b33e2565522 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 7.10.0 (unreleased) - Improve diff UI - Fix alignment of navbar toggle button (Cody Mize) - Identical look of selectboxes in UI + - Add location field to user profile v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 1b9a86ee42c..fcdd5b8b1d1 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -68,7 +68,7 @@ class ProfilesController < ApplicationController params.require(:user).permit( :email, :password, :password_confirmation, :bio, :name, :username, :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, - :avatar, :hide_no_ssh_key, :hide_no_password + :avatar, :hide_no_ssh_key, :hide_no_password, :location ) end end diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 409b6b5a193..5a501e43149 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -53,6 +53,9 @@ .form-group = f.label :website_url, 'Website', class: "control-label" .col-sm-10= f.text_field :website_url, class: "form-control" + .form-group + = f.label :location, 'Location', class: "control-label" + .col-sm-10= f.text_field :location, class: "form-control" .form-group = f.label :bio, class: "control-label" .col-sm-10 diff --git a/db/migrate/20150320234437_add_location_to_user.rb b/db/migrate/20150320234437_add_location_to_user.rb new file mode 100644 index 00000000000..32731d37d75 --- /dev/null +++ b/db/migrate/20150320234437_add_location_to_user.rb @@ -0,0 +1,5 @@ +class AddLocationToUser < ActiveRecord::Migration + def change + add_column :users, :location, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 1be3782dcb3..e1a5b70532a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150313012111) do +ActiveRecord::Schema.define(version: 20150320234437) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -473,6 +473,7 @@ ActiveRecord::Schema.define(version: 20150313012111) do t.boolean "password_automatically_set", default: false t.string "bitbucket_access_token" t.string "bitbucket_access_token_secret" + t.string "location" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index bfbfe7af199..791982d16c3 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -11,6 +11,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps fill_in "user_linkedin", with: "testlinkedin" fill_in "user_twitter", with: "testtwitter" fill_in "user_website_url", with: "testurl" + fill_in "user_location", with: "Ukraine" click_button "Save changes" @user.reload end @@ -20,6 +21,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps @user.linkedin.should == 'testlinkedin' @user.twitter.should == 'testtwitter' @user.website_url.should == 'testurl' + find("#user_location").value.should == "Ukraine" end step 'I change my avatar' do -- GitLab From d85f396fe55e0c8d6593bbd2dcb06990da48d9b7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 16:55:20 -0700 Subject: [PATCH 1471/1609] Add location to user page --- app/views/users/_profile.html.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml index 0a70b738071..a073e5048e7 100644 --- a/app/views/users/_profile.html.haml +++ b/app/views/users/_profile.html.haml @@ -21,6 +21,10 @@ %li %span.light Website: %strong= link_to user.short_website_url, user.full_website_url + - unless user.location.blank? + %li + %span.light Location: + %strong= user.location - unless user.bio.blank? %li %span.light Bio: -- GitLab From beece83af1788e587dc6e14c6feb1dea5c8f9b97 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 20:10:05 -0700 Subject: [PATCH 1472/1609] Improve header with avatar for group and user pages --- app/assets/stylesheets/generic/common.scss | 19 +++++++++++++ app/assets/stylesheets/pages/profile.scss | 5 ---- app/views/groups/show.html.haml | 15 +++++----- app/views/users/_profile.html.haml | 4 --- app/views/users/show.html.haml | 32 ++++++++++++---------- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 876eea72e8a..db393e08819 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -355,3 +355,22 @@ table { bottom: 20px !important; left: 20px !important; } + +.header-with-avatar { + h3 { + margin: 0; + font-weight: bold; + } + + .username { + font-size: 18px; + color: #666; + margin-top: 8px; + } + + .description { + font-size: 16px; + color: #666; + margin-top: 8px; + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 81afe05162f..fbe71a5b5ad 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -41,11 +41,6 @@ } } -.user-show-username { - font-weight: 200; - color: #666; -} - /* * Appearance settings * diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 25efe973d4f..8df9366ecbe 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,12 +1,13 @@ .dashboard - %div + .header-with-avatar.clearfix = image_tag group_icon(@group), class: "avatar group-avatar s90" - .clearfix - %h2 - = @group.name - - if @group.description.present? - %p - = escaped_autolink(@group.description) + %h3 + = @group.name + .username + @#{@group.path} + - if @group.description.present? + .description + = escaped_autolink(@group.description) %hr .row %section.activities.col-md-8 diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml index a073e5048e7..bca71444956 100644 --- a/app/views/users/_profile.html.haml +++ b/app/views/users/_profile.html.haml @@ -25,7 +25,3 @@ %li %span.light Location: %strong= user.location - - unless user.bio.blank? - %li - %span.light Bio: - %span= user.bio diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 6d6beb58711..fd96020d129 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -2,24 +2,28 @@ = link_to '#aside', class: 'show-aside' do %i.fa.fa-angle-left %section.col-md-8 - %h3.page-title + .header-with-avatar = image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: '' - = @user.name - - if @user == current_user - .pull-right - = link_to profile_path, class: 'btn' do - %i.fa.fa-pencil-square-o - Edit Profile settings - %br - %span.user-show-username #{@user.username} - %br - %small member since #{@user.created_at.stamp("Nov 12, 2031")} + %h3 + = @user.name + - if @user == current_user + .pull-right + = link_to profile_path, class: 'btn btn-sm' do + %i.fa.fa-pencil-square-o + Edit Profile settings + .username + @#{@user.username} + .description + - if @user.bio.present? + = @user.bio + .clearfix - if @groups.any? - %h4 Groups - = render 'groups', groups: @groups - %hr + .prepend-top-20 + %h4 Groups + = render 'groups', groups: @groups + %hr .hidden-xs .user-calendar -- GitLab From 222e53a026fc4470737d03c358db97cf11c5e029 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 20 Mar 2015 20:46:04 -0700 Subject: [PATCH 1473/1609] Prevent diff header overflow --- app/assets/stylesheets/pages/diff.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 83f65913ee6..7b7bb88bc20 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -11,8 +11,10 @@ z-index: 10; > span { - @include str-truncated(65%); font-family: $monospace_font; + word-break: break-all; + margin-right: 200px; + display: block; } .diff-btn-group { -- GitLab From 5b432e76710eb70cc41c59af1ea9a294202a49fc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 5 Dec 2014 22:56:43 +0100 Subject: [PATCH 1474/1609] Extend push_tag event to include tag message and last commit --- CHANGELOG | 1 + app/services/create_tag_service.rb | 3 +- app/services/git_push_service.rb | 60 +++++++++++----------- app/services/git_tag_push_service.rb | 18 ++++++- lib/gitlab/push_data_builder.rb | 3 +- spec/services/git_push_service_spec.rb | 5 -- spec/services/git_tag_push_service_spec.rb | 52 +++++++++++++++++-- 7 files changed, 97 insertions(+), 45 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4c644960088..176a3c833b7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 7.10.0 (unreleased) - Improve error message when save profile has error. - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) - Add location field to user profile + - Add tag message and last commit to tag hook (Kamil Trzciński) v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index af4b537cb93..4115d689925 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -40,7 +40,8 @@ class CreateTagService < BaseService end def create_push_data(project, user, tag) + commits = [project.repository.commit(tag.target)].compact Gitlab::PushDataBuilder. - build(project, user, Gitlab::Git::BLANK_SHA, tag.target, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) + build(project, user, Gitlab::Git::BLANK_SHA, tag.target, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", commits, tag.message) end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index a0da07f5c90..1f0b29dff5e 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -23,42 +23,40 @@ class GitPushService project.repository.expire_cache project.update_repository_size - if push_to_branch?(ref) - if push_remove_branch?(ref, newrev) - @push_commits = [] - elsif push_to_new_branch?(ref, oldrev) - # Re-find the pushed commits. - if is_default_branch?(ref) - # Initial push to the default branch. Take the full history of that branch as "newly pushed". - @push_commits = project.repository.commits(newrev) - - # Set protection on the default branch if configured - if (current_application_settings.default_branch_protection != PROTECTION_NONE) - developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false - project.protected_branches.create({ name: project.default_branch, developers_can_push: developers_can_push }) - end - else - # Use the pushed commits that aren't reachable by the default branch - # as a heuristic. This may include more commits than are actually pushed, but - # that shouldn't matter because we check for existing cross-references later. - @push_commits = project.repository.commits_between(project.default_branch, newrev) - - # don't process commits for the initial push to the default branch - process_commit_messages(ref) + if push_remove_branch?(ref, newrev) + @push_commits = [] + elsif push_to_new_branch?(ref, oldrev) + # Re-find the pushed commits. + if is_default_branch?(ref) + # Initial push to the default branch. Take the full history of that branch as "newly pushed". + @push_commits = project.repository.commits(newrev) + + # Set protection on the default branch if configured + if (current_application_settings.default_branch_protection != PROTECTION_NONE) + developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false + project.protected_branches.create({ name: project.default_branch, developers_can_push: developers_can_push }) end - elsif push_to_existing_branch?(ref, oldrev) - # Collect data for this git push - @push_commits = project.repository.commits_between(oldrev, newrev) - project.update_merge_requests(oldrev, newrev, ref, @user) + else + # Use the pushed commits that aren't reachable by the default branch + # as a heuristic. This may include more commits than are actually pushed, but + # that shouldn't matter because we check for existing cross-references later. + @push_commits = project.repository.commits_between(project.default_branch, newrev) + + # don't process commits for the initial push to the default branch process_commit_messages(ref) end + elsif push_to_existing_branch?(ref, oldrev) + # Collect data for this git push + @push_commits = project.repository.commits_between(oldrev, newrev) + project.update_merge_requests(oldrev, newrev, ref, @user) + process_commit_messages(ref) + end - @push_data = build_push_data(oldrev, newrev, ref) + @push_data = build_push_data(oldrev, newrev, ref) - EventCreateService.new.push(project, user, @push_data) - project.execute_hooks(@push_data.dup, :push_hooks) - project.execute_services(@push_data.dup, :push_hooks) - end + EventCreateService.new.push(project, user, @push_data) + project.execute_hooks(@push_data.dup, :push_hooks) + project.execute_services(@push_data.dup, :push_hooks) end protected diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 0d8e6e85e47..bf203bbd692 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -3,7 +3,7 @@ class GitTagPushService def execute(project, user, oldrev, newrev, ref) @project, @user = project, user - + @push_data = build_push_data(oldrev, newrev, ref) EventCreateService.new.push(project, user, @push_data) @@ -18,6 +18,20 @@ class GitTagPushService private def build_push_data(oldrev, newrev, ref) - Gitlab::PushDataBuilder.build(project, user, oldrev, newrev, ref, []) + commits = [] + message = nil + + if !Gitlab::Git.blank_ref?(newrev) + tag_name = Gitlab::Git.ref_name(ref) + tag = project.repository.find_tag(tag_name) + if tag && tag.target == newrev + commit = project.repository.commit(tag.target) + commits = [commit].compact + message = tag.message + end + end + + Gitlab::PushDataBuilder. + build(project, user, oldrev, newrev, ref, commits, message) end end diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 948cf58fd9a..f8da452e4c0 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -21,7 +21,7 @@ module Gitlab # total_commits_count: Fixnum # } # - def build(project, user, oldrev, newrev, ref, commits = []) + def build(project, user, oldrev, newrev, ref, commits = [], message = nil) # Total commits count commits_count = commits.size @@ -42,6 +42,7 @@ module Gitlab after: newrev, ref: ref, checkout_sha: checkout_sha(project.repository, newrev, ref), + message: message, user_id: user.id, user_name: user.name, user_email: user.email, diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 1b1e3ca5f8b..aa9b15dd9ec 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -145,11 +145,6 @@ describe GitPushService do expect(project).to receive(:execute_hooks) service.execute(project, user, 'oldrev', 'newrev', 'refs/heads/master') end - - it "when pushing tags" do - expect(project).not_to receive(:execute_hooks) - service.execute(project, user, 'newrev', 'newrev', 'refs/tags/v1.0.0') - end end end diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index fcf462edbfc..a050fdf6c0e 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -1,32 +1,39 @@ require 'spec_helper' describe GitTagPushService do + include RepoHelpers + let (:user) { create :user } let (:project) { create :project } let (:service) { GitTagPushService.new } before do - @ref = 'refs/tags/super-tag' - @oldrev = 'b98a310def241a6fd9c9a9a3e7934c48e498fe81' - @newrev = 'b19a04f53caeebf4fe5ec2327cb83e9253dc91bb' + @oldrev = Gitlab::Git::BLANK_SHA + @newrev = "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" # gitlab-test: git rev-parse refs/tags/v1.1.0 + @ref = 'refs/tags/v1.1.0' end - describe 'Git Tag Push Data' do + describe "Git Tag Push Data" do before do service.execute(project, user, @oldrev, @newrev, @ref) @push_data = service.push_data + @tag_name = Gitlab::Git.ref_name(@ref) + @tag = project.repository.find_tag(@tag_name) + @commit = project.repository.commit(@tag.target) end subject { @push_data } + it { is_expected.to include(object_kind: 'tag_push') } it { is_expected.to include(ref: @ref) } it { is_expected.to include(before: @oldrev) } it { is_expected.to include(after: @newrev) } + it { is_expected.to include(message: @tag.message) } it { is_expected.to include(user_id: user.id) } it { is_expected.to include(user_name: user.name) } it { is_expected.to include(project_id: project.id) } - context 'With repository data' do + context "with repository data" do subject { @push_data[:repository] } it { is_expected.to include(name: project.name) } @@ -34,6 +41,41 @@ describe GitTagPushService do it { is_expected.to include(description: project.description) } it { is_expected.to include(homepage: project.web_url) } end + + context "with commits" do + subject { @push_data[:commits] } + + it { is_expected.to be_an(Array) } + it 'has 1 element' do + expect(subject.size).to eq(1) + end + + context "the commit" do + subject { @push_data[:commits].first } + + it { is_expected.to include(id: @commit.id) } + it { is_expected.to include(message: @commit.safe_message) } + it { is_expected.to include(timestamp: @commit.date.xmlschema) } + it do + is_expected.to include( + url: [ + Gitlab.config.gitlab.url, + project.namespace.to_param, + project.to_param, + 'commit', + @commit.id + ].join('/') + ) + end + + context "with a author" do + subject { @push_data[:commits].first[:author] } + + it { is_expected.to include(name: @commit.author_name) } + it { is_expected.to include(email: @commit.author_email) } + end + end + end end describe "Web Hooks" do -- GitLab From cc29ce491786d631586c3b0d0da310b8b790a673 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 21 Mar 2015 08:39:54 -0600 Subject: [PATCH 1475/1609] Don't allow style attributes in inline HTML --- CHANGELOG | 1 + lib/gitlab/markdown.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c4e47346fd8..0046b73ba75 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Allow HTML tags in Markdown input v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index cd70fd5e85b..65dce9291e6 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -88,7 +88,7 @@ module Gitlab ] whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST - whitelist[:attributes][:all].push('class', 'id', 'style') + whitelist[:attributes][:all].push('class', 'id') # Remove the rel attribute that the sanitize gem adds, and remove the # href attribute if it contains inline javascript -- GitLab From 1dc90fc455249fb95a47c4abe37c41caeb2ec7da Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 21 Mar 2015 08:45:28 -0600 Subject: [PATCH 1476/1609] Fix nested task lists When nesting task list items, the parent item is wrapped in a `

        ` tag. Update the task list parser to handle these paragraph wrappers. --- app/models/concerns/taskable.rb | 2 +- lib/gitlab/markdown.rb | 5 +++-- spec/helpers/gitlab_markdown_helper_spec.rb | 11 +++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index 410e8dc820b..bbb3b301a9f 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -5,7 +5,7 @@ # Used by MergeRequest and Issue module Taskable TASK_PATTERN_MD = /^(? *[*-] *)\[(?[ xX])\]/.freeze - TASK_PATTERN_HTML = /^

      • \[(?[ xX])\]/.freeze + TASK_PATTERN_HTML = /^
      • (?\s*

        )?\[(?[ xX])\]/.freeze # Change the state of a task list item for this Taskable. Edit the object's # description by finding the nth task item and changing its checkbox diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index e02e5b9fc3d..f5e8267031c 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -352,11 +352,12 @@ module Gitlab # ActiveSupport::SafeBuffer, hence the `String.new` String.new(text).gsub(Taskable::TASK_PATTERN_HTML) do checked = $LAST_MATCH_INFO[:checked].downcase == 'x' + p_tag = $LAST_MATCH_INFO[:p_tag] if checked - "#{li_tag}#{checked_box}" + "#{li_tag}#{p_tag}#{checked_box}" else - "#{li_tag}#{unchecked_box}" + "#{li_tag}#{p_tag}#{unchecked_box}" end end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 6ba27b536e4..ddbb4467f10 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -817,6 +817,17 @@ EOT ) end + it 'should render checkboxes for nested tasks' do + rendered_text = markdown(@source_text_asterisk, parse_tasks: true) + + expect(rendered_text).to match( + / Date: Sat, 21 Mar 2015 09:34:33 -0600 Subject: [PATCH 1477/1609] Add sidetiq dependency Add the sidetiq gem to the Gemfile to match EE. --- CHANGELOG | 1 + Gemfile | 1 + Gemfile.lock | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 4c644960088..25d18501bc8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 7.10.0 (unreleased) - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) - Add a service to support external wikis (Hannes Rosenögger) - List new commits for newly pushed branch in activity view. + - Add sidetiq gem dependency to match EE - Add changelog, license and contribution guide links to project sidebar. - Improve diff UI - Fix alignment of navbar toggle button (Cody Mize) diff --git a/Gemfile b/Gemfile index 285ccf32b66..e7f75055f3f 100644 --- a/Gemfile +++ b/Gemfile @@ -121,6 +121,7 @@ gem "acts-as-taggable-on" gem 'slim' gem 'sinatra', require: nil gem 'sidekiq', '~> 3.3' +gem 'sidetiq', '0.6.3' # HTTP requests gem "httparty" diff --git a/Gemfile.lock b/Gemfile.lock index 80eebc16e4c..4f1cab43dd5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -290,6 +290,7 @@ GEM httpauth (0.2.1) httpclient (2.5.3.3) i18n (0.7.0) + ice_cube (0.11.1) ice_nine (0.10.0) jasmine (2.0.2) jasmine-core (~> 2.0.0) @@ -546,6 +547,10 @@ GEM json redis (>= 3.0.6) redis-namespace (>= 1.3.1) + sidetiq (0.6.3) + celluloid (>= 0.14.1) + ice_cube (= 0.11.1) + sidekiq (>= 3.0.0) simple_oauth (0.1.9) simplecov (0.9.0) docile (~> 1.1.0) @@ -764,6 +769,7 @@ DEPENDENCIES settingslogic shoulda-matchers (~> 2.7.0) sidekiq (~> 3.3) + sidetiq (= 0.6.3) simplecov sinatra six -- GitLab From 5f86c08b2a2a33ad9e6059035a249986f59e995f Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 21 Mar 2015 11:20:30 -0600 Subject: [PATCH 1478/1609] Fix link in patch update guide --- CHANGELOG | 1 + doc/update/patch_versions.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4c644960088..74c2aba631d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ v 7.10.0 (unreleased) - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) + - Fix a link in the patch update guide - Add a service to support external wikis (Hannes Rosenögger) - List new commits for newly pushed branch in activity view. - Add changelog, license and contribution guide links to project sidebar. diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index ad302492556..e29ee2a7b3d 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -1,5 +1,5 @@ # Universal update guide for patch versions -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/patch_versions.md) for the most up to date instructions.* +*Make sure you view this [upgrade guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/patch_versions.md) from the `master` branch for the most up to date instructions.* For example from 6.2.0 to 6.2.1, also see the [semantic versioning specification](http://semver.org/). -- GitLab From b624b775aae83b120603e6fda68e3ffae485cd34 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Mar 2015 12:13:47 -0700 Subject: [PATCH 1479/1609] Hide UI elements when print md files or wiki pages --- app/assets/stylesheets/print.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 42dbf4d6ef3..1be0551ad3b 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -11,3 +11,7 @@ header, nav, nav.main-nav, nav.navbar-collapse, nav.navbar-collapse.collapse {di .wiki h1 {font-size: 30px;} .wiki h2 {font-size: 22px;} .wiki h3 {font-size: 18px; font-weight: bold; } + +.sidebar-wrapper { display: none; } +.nav { display: none; } +.btn { display: none; } -- GitLab From 2c796a1785242a6940ef3b7ef8a06fa8617ec245 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Mar 2015 13:44:11 -0700 Subject: [PATCH 1480/1609] Cache project branches and tags into variables --- CHANGELOG | 1 + app/models/repository.rb | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 4c644960088..e547085043d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 7.10.0 (unreleased) - Improve error message when save profile has error. - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) - Add location field to user profile + - Improve GitLab performance when working with git repositories v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/models/repository.rb b/app/models/repository.rb index c6eaa485b8a..2e3cfb85594 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -62,24 +62,28 @@ class Repository def add_branch(branch_name, ref) cache.expire(:branch_names) + @branches = nil gitlab_shell.add_branch(path_with_namespace, branch_name, ref) end def add_tag(tag_name, ref, message = nil) cache.expire(:tag_names) + @tags = nil gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) end def rm_branch(branch_name) cache.expire(:branch_names) + @branches = nil gitlab_shell.rm_branch(path_with_namespace, branch_name) end def rm_tag(tag_name) cache.expire(:tag_names) + @tags = nil gitlab_shell.rm_tag(path_with_namespace, tag_name) end @@ -368,6 +372,18 @@ class Repository end end + def branches + @branches ||= raw_repository.branches + end + + def tags + @tags ||= raw_repository.tags + end + + def root_ref + @root_ref ||= raw_repository.root_ref + end + private def cache -- GitLab From c378d20c8440aff83a8bd54c5aed9f06a9c205f8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Mar 2015 13:44:51 -0700 Subject: [PATCH 1481/1609] Cache lookup results into hash to prevent repeating same requests to git repo --- app/models/repository.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 2e3cfb85594..7b8a34eba2c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -184,8 +184,17 @@ class Repository end end + def lookup_cache + @lookup_cache ||= {} + end + def method_missing(m, *args, &block) - raw_repository.send(m, *args, &block) + if m == :lookup && !block_given? + lookup_cache[m] ||= {} + lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block) + else + raw_repository.send(m, *args, &block) + end end def respond_to?(method) -- GitLab From d96098e966f70afcf6f3bfa1ed1bc20be2672fc8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Mar 2015 13:45:08 -0700 Subject: [PATCH 1482/1609] Cache head commit and head tree --- app/models/repository.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 7b8a34eba2c..082ad7a0c6a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -248,12 +248,20 @@ class Repository end def head_commit - commit(self.root_ref) + @head_commit ||= commit(self.root_ref) + end + + def head_tree + @head_tree ||= Tree.new(self, head_commit.sha, nil) end def tree(sha = :head, path = nil) if sha == :head - sha = head_commit.sha + if path.nil? + return head_tree + else + sha = head_commit.sha + end end Tree.new(self, sha, path) -- GitLab From 3b5df555f213e3a2be804b56455fcbaad3d93be0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Mar 2015 13:53:11 -0700 Subject: [PATCH 1483/1609] Add CHANGELOG item with print view fix --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 74c2aba631d..9a1c4f41dbb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ v 7.10.0 (unreleased) - Improve error message when save profile has error. - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) - Add location field to user profile + - Fix print view for markdown files and wiki pages v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) -- GitLab From 1502fed795979b19ca5366b688f07ffc22c79c99 Mon Sep 17 00:00:00 2001 From: Michael Alt Date: Sat, 21 Mar 2015 22:57:55 +0100 Subject: [PATCH 1484/1609] Faulty LDAP DN name escaping removed The Net::LDAP::Filter.escape function can not be used to escape the DN name because the backslash is required to escape special chars in the DN name. This leads to the error message "Access denied for your LDAP account." and prevents the user from logging in to gitlab. Example DN: CN=Test\, User,OU=Organization,DC=Company CN=Test User,OU=Organization,DC=Company http://www.ietf.org/rfc/rfc4514.txt --- lib/gitlab/ldap/person.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 3c426179375..b81f3e8e8f5 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -14,7 +14,6 @@ module Gitlab end def self.find_by_dn(dn, adapter) - dn = Net::LDAP::Filter.escape(dn) adapter.user('dn', dn) end -- GitLab From 9f1c284408e1ab724111830ba0d96f874725991a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Mar 2015 17:41:58 -0700 Subject: [PATCH 1485/1609] Remove 32px oauth images --- .../images/authbuttons/bitbucket_32.png | Bin 2713 -> 0 bytes app/assets/images/authbuttons/github_32.png | Bin 1822 -> 0 bytes app/assets/images/authbuttons/gitlab_32.png | Bin 1039 -> 0 bytes app/assets/images/authbuttons/gitlab_64.png | Bin 3013 -> 6559 bytes .../images/authbuttons/gitlab_64.png_old | Bin 0 -> 2878 bytes app/assets/images/authbuttons/google_32.png | Bin 1501 -> 0 bytes app/assets/images/authbuttons/twitter_32.png | Bin 1311 -> 0 bytes 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/assets/images/authbuttons/bitbucket_32.png delete mode 100644 app/assets/images/authbuttons/github_32.png delete mode 100644 app/assets/images/authbuttons/gitlab_32.png create mode 100644 app/assets/images/authbuttons/gitlab_64.png_old delete mode 100644 app/assets/images/authbuttons/google_32.png delete mode 100644 app/assets/images/authbuttons/twitter_32.png diff --git a/app/assets/images/authbuttons/bitbucket_32.png b/app/assets/images/authbuttons/bitbucket_32.png deleted file mode 100644 index 27702eb973d1c3c0b9a39eee082ca72d38339cc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2713 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 Bjl2K= diff --git a/app/assets/images/authbuttons/gitlab_32.png b/app/assets/images/authbuttons/gitlab_32.png deleted file mode 100644 index f3b78cb6efba1e8cff625fd262b36705dd707c93..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1039 zcmeAS@N?(olHy`uVBq!ia0y~yU{GLSV36ftV_;x7Juk1Ffq{W7$=lt9;Xep2*t>i( z0|NtRfk$L90|U1(2s1Lwnj^u$z`$PO>FdgVpHYsP#o*CPx9toJ%yT?l978;g@4Xh^ zaWzzc{lob`-`=fz_iMUoaj|J|&l`TOgUfE(Yh2m1LFsh+krfkov|gwxD+O&*;8x}J z^l3WbWT3o3EaZIOO={`B3i@9r_te?G^u_}Ls|WKICmsuUNx))AL|^g`Dld zX`h~j#mstMym9@5S5{Te9$kE}_(6ez_Gj~?@DB`W*LMDS`QUFtN!-dei3jIZ zIc{PW)+%`POmzWKDGo)<#T+xXTL?${-)neoI5Y|25~QUnQZ*OPV=L#9(V4MMdw78+CF!<)z#Fg zD!wL*VeZ|X7I$ZI$KLg@n{)i#!leEG+Rq1sA9Q(naW}h_>_L;inm@RDG9`a(J9=={ zpWiE+W9+2Dr>)AaK5kjJ$;QZ!TkrUb75^J@53cm+c(FFUZ<+|V@uy$8XY#Jhd95to z)0EA=@$1){+h2X5AHp?>hX5A>S~Rm z>ox*skNtSy@kDQTrbtZ3`U}xlIQJ~jW`8$bLhx)}DwC{cZvN4>qT-k(YGvUYcIhuS z**ksJOND0-0}N~<1Y}+u{{7$6b%SNavzjS?xVa|H)UDsWIGTZhfx*+&&t;ucLK6Tb CIO~1@ diff --git a/app/assets/images/authbuttons/gitlab_64.png b/app/assets/images/authbuttons/gitlab_64.png index ff2945fe89eddf03e2e1fc8deb39a6a7bc5bc088..31281a194441d4e23bb691de89110c6097021ac9 100644 GIT binary patch delta 6548 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 A&j0`b delta 2975 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 Qfs28`)78&qol`;+04k=RkN^Mx diff --git a/app/assets/images/authbuttons/gitlab_64.png_old b/app/assets/images/authbuttons/gitlab_64.png_old new file mode 100644 index 0000000000000000000000000000000000000000..8390c81a08b310df07fe53a487368f6716745be7 GIT binary patch literal 2878 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 SL>U+u7(8A5T-G@yGywoaCak3Z diff --git a/app/assets/images/authbuttons/twitter_32.png b/app/assets/images/authbuttons/twitter_32.png deleted file mode 100644 index a3d4964f40fe5c7f48dd4bd56c1e51697f0959bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1311 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANMU&pcfmLn;`bPW8@+y()gJ zKKp)g{=2jG)3=#z)i+qnws2yQ#;5iDj#K%rbaq$;u?Vuiaw)jCQ8~TKLc3|PlXi&9 zYS*JltxaonzfR1$z2(v}{mE*zDwgT*pGM9(`)tCo*#R;fb<_Lv=RW^+ZeRA=v-NV9 ze&4d^iVd2?9)HVnN6y`2(>J$8EBXnrgdWvUQsL!VCKZ$uaMeX%gM`D38FKQsSz{Km zX$lJ+eA9dK>T9=cwHM!QG2eb$cazTHom!d~>p2)rzcL!Xs9kZcRV!0trQ(C;rd6@s z&qdyMKYpyvXUiC)ptRexlJjt9t^AJT_p1I+^{Y=W-hSJE_thm=PZcN`ZdYBg&*DhR zGDqz&w^c1}?`;oiyFT3b_)vAjS?xBrINKlZX6G}rI)thDrOjyzU7O~%d%b79XSb`v zoaRYfp-I&%x3A|}kt8))TTHy{Y(&Kz;WI6c&6loU-!59XLi;^R|{Jiq(rA8r-s7792Z;_1R-Xt>nrb^YmoOW4FsbpyM4 zJ9If`U(;FM*W1kOaX2t_+LLWcoEfX+l3KF2J^NS2I;Z&MvlQnDh9(oK38@@%oO~Ap zRw-TNSe2n_yUf98LKM%Id#44&D#|LCAMNwa4vgO_(UqCwY*Af0>+t^AmOTzD&ou{D zNeifJ7KU)o=og)4u&5;*0AWgLCKW`Tr013M1Au6aH4i~;L7!BKI?6H z7&41Oo!Q(g#QlDA+^{Ba zN%_%gaqUAUyFWT!_Bzw9TL1ob;GdYl-WcED3u+2SV*-uTXEa)92gL5-p2M-~hIhc# z6pxk7ujcJpFk|M{!r7B%&sw->$@$fVvb&o@&f9ihm|-?~Qegf$(JO`F_1z3^H8E?u z6eT@7ofT5!*Kl|6w|+6bnpgJe(v@7cxIh2*math}$a0Do&b&G!^?^!g-Gu=CV{xt= znc>~1HCqc(-e{fA+u3dpSg!0#8jn!eVwW>^Fj!7t7u4Ik=gUd@_xl-kJ!AAwjIoSyZ-tW z&Th#LU5W4mlb*8{Z!V6}4o`eO@9-O2t7AbM-k*@Hw)^|v`&Q4Eu$FHd7#=;E?ArK* zb@%??Q~mqrCBD9(#kTwHt!duBP6el4l>7BlM)CY*nE(fI!wJh0ID26oC3pQMSFVk^S$(BFqhTqpQv8$Pl^lX10`uqGH}iY`|F~C=Zl=`BfPB08 z=VRHI45uun-5=J>u z{?DF=G0S)Pe)bVyoMUnP$jW5#zPEMHdwKiqKF5?lo%QgW!}q)pRsqwcOO!e^xg7-q z4P8YXSpx+xo=#TZZ(m#Re%{N^%g=QddvW+V^0-9IS(z2Spxfhgr-|#5#OI5pWFIW@ z+FR$?yh<>B)y9rl+OwF050r%ZurL%pir{c;cG%J} Date: Sat, 21 Mar 2015 17:44:40 -0700 Subject: [PATCH 1486/1609] Restyle oauth accounts at profile page and add ability to unlink account --- CHANGELOG | 2 + app/assets/stylesheets/generic/forms.scss | 4 -- app/assets/stylesheets/pages/login.scss | 9 +++++ app/assets/stylesheets/pages/profile.scss | 40 ++++++++----------- .../profiles/accounts_controller.rb | 6 +++ app/helpers/application_helper.rb | 6 --- app/helpers/oauth_helper.rb | 11 ++++- app/helpers/profile_helper.rb | 6 --- .../devise/shared/_omniauth_box.html.haml | 2 +- app/views/profiles/accounts/show.html.haml | 21 +++++----- config/routes.rb | 6 ++- 11 files changed, 59 insertions(+), 54 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6e40fa9aef8..cf00780d332 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,8 @@ v 7.10.0 (unreleased) - Improve GitLab performance when working with git repositories - Add tag message and last commit to tag hook (Kamil Trzciński) - Restrict permissions on backup files + - Improve oauth accounts UI in profile page + - Add ability to unlink connected accounts v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/assets/stylesheets/generic/forms.scss b/app/assets/stylesheets/generic/forms.scss index 31fe5a03f37..266041403e0 100644 --- a/app/assets/stylesheets/generic/forms.scss +++ b/app/assets/stylesheets/generic/forms.scss @@ -15,10 +15,6 @@ input[type='text'].danger { text-shadow: 0 1px 1px #fff } -fieldset legend { - font-size: 16px; -} - .datetime-controls { select { width: 100px; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index d366300511e..83b866c3a64 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -113,3 +113,12 @@ } } } + +.oauth-image-link { + margin-right: 10px; + + img { + width: 32px; + height: 32px; + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index fbe71a5b5ad..65655d4bfa3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -1,31 +1,7 @@ .account-page { fieldset { margin-bottom: 15px; - border-bottom: 1px dashed #ddd; padding-bottom: 15px; - - &:last-child { - border: none; - } - - legend { - border: none; - margin-bottom: 10px; - } - } -} - -.oauth_select_holder { - img { - padding: 2px; - margin-right: 10px; - } - .active { - img { - border: 1px solid #4BD; - background: $hover; - @include border-radius(5px); - } } } @@ -101,3 +77,19 @@ } } } + +.oauth-buttons { + .btn-group { + margin-right: 10px; + } + + .btn { + line-height: 36px; + height: 56px; + + img { + width: 32px; + height: 32px; + } + } +} diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index fe121691a10..9bd34fe2261 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -4,4 +4,10 @@ class Profiles::AccountsController < ApplicationController def show @user = current_user end + + def unlink + provider = params[:provider] + current_user.identities.find_by(provider: provider).destroy + redirect_to profile_account_path + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8ed6d59c20d..38b5fc4a011 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -174,16 +174,10 @@ module ApplicationHelper Digest::SHA1.hexdigest string end - def authbutton(provider, size = 64) - file_name = "#{provider.to_s.split('_').first}_#{size}.png" - image_tag(image_path("authbuttons/#{file_name}"), alt: "Sign in with #{provider.to_s.titleize}") - end - def simple_sanitize(str) sanitize(str, tags: %w(a span)) end - def body_data_page path = controller.controller_path.split('/') namespace = path.first if path.second diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb index 1a0ad17b607..997b91de077 100644 --- a/app/helpers/oauth_helper.rb +++ b/app/helpers/oauth_helper.rb @@ -20,6 +20,15 @@ module OauthHelper def additional_providers enabled_oauth_providers.reject{|provider| provider.to_s.starts_with?('ldap')} end - + + def oauth_image_tag(provider, size = 64) + file_name = "#{provider.to_s.split('_').first}_#{size}.png" + image_tag(image_path("authbuttons/#{file_name}"), alt: "Sign in with #{provider.to_s.titleize}") + end + + def oauth_active?(provider) + current_user.identities.exists?(provider: provider.to_s) + end + extend self end diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb index 9e37e44732a..780c7cd5133 100644 --- a/app/helpers/profile_helper.rb +++ b/app/helpers/profile_helper.rb @@ -1,10 +1,4 @@ module ProfileHelper - def oauth_active_class(provider) - if current_user.identities.exists?(provider: provider.to_s) - 'active' - end - end - def show_profile_username_tab? current_user.can_change_username? end diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 4cd1c303b22..b647b906b71 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -5,6 +5,6 @@ - providers.each do |provider| %span.light - if default_providers.include?(provider) - = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider) + = link_to oauth_image_tag(provider), omniauth_authorize_path(resource_name, provider), class: 'oauth-image-link' - else = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn" diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 6bafcb56551..5bffb4acc1d 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,11 +1,6 @@ -%h3.page-title - Account Settings -%p.light - You can change your username and private token here. - - if current_user.ldap_user? +- if current_user.ldap_user? + .alert.alert-info Some options are unavailable for LDAP accounts -%hr - .account-page %fieldset.update-token @@ -33,12 +28,16 @@ - if show_profile_social_tab? %fieldset - %legend Social Accounts - .oauth_select_holder.append-bottom-10 + %legend Connected Accounts + .oauth-buttons.append-bottom-10 %p Click on icon to activate signin with one of the following services - enabled_social_providers.each do |provider| - %span{class: oauth_active_class(provider) } - = link_to authbutton(provider, 32), omniauth_authorize_path(User, provider) + .btn-group + = link_to oauth_image_tag(provider), omniauth_authorize_path(User, provider), + class: "btn btn-lg #{'active' if oauth_active?(provider)}" + - if oauth_active?(provider) + = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do + %i.fa.fa-close - if show_profile_username_tab? %fieldset.update-username diff --git a/config/routes.rb b/config/routes.rb index 03b4a32a9dd..c30cd768572 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -184,7 +184,11 @@ Gitlab::Application.routes.draw do end scope module: :profiles do - resource :account, only: [:show, :update] + resource :account, only: [:show, :update] do + member do + delete :unlink + end + end resource :notifications, only: [:show, :update] resource :password, only: [:new, :create, :edit, :update] do member do -- GitLab From 59d5c779758a696233d2f2adcf145c0a93a8fb2f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 21 Mar 2015 18:04:29 -0700 Subject: [PATCH 1487/1609] Fix dots in Wiki slug causing errors Closes #1263, #431 --- CHANGELOG | 1 + app/models/project_wiki.rb | 2 +- app/models/wiki_page.rb | 3 ++- spec/models/wiki_page_spec.rb | 41 +++++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6e40fa9aef8..7b22c6b36f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Fix dots in Wiki slugs causing errors (Stan Hu) - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 55438bee245..772c868d9cd 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -104,7 +104,7 @@ class ProjectWiki def page_title_and_dir(title) title_array = title.split("/") title = title_array.pop - [title.gsub(/\.[^.]*$/, ""), title_array.join("/")] + [title, title_array.join("/")] end def search_files(query) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 32981a0e664..e9413c34bae 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -179,7 +179,8 @@ class WikiPage if valid? && project_wiki.send(method, *args) page_details = if method == :update_page - @page.path + # Use url_path instead of path to omit format extension + @page.url_path else title end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index f3fd805783f..fceb7668cac 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -78,6 +78,47 @@ describe WikiPage do end end + describe "dot in the title" do + let(:title) { 'Index v1.2.3' } + + before do + @wiki_attr = {title: title, content: "Home Page", format: "markdown"} + end + + describe "#create" do + after do + destroy_page(title) + end + + context "with valid attributes" do + it "saves the wiki page" do + subject.create(@wiki_attr) + expect(wiki.find_page(title)).not_to be_nil + end + + it "returns true" do + expect(subject.create(@wiki_attr)).to eq(true) + end + end + end + + describe "#update" do + before do + create_page(title, "content") + @page = wiki.find_page(title) + end + + it "updates the content of the page" do + @page.update("new content") + @page = wiki.find_page(title) + end + + it "returns true" do + expect(@page.update("more content")).to be_truthy + end + end + end + describe "#update" do before do create_page("Update", "content") -- GitLab From 64891c6c40c6b670c2b50aab8ba56e3d47e30076 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 21 Mar 2015 23:48:08 -0700 Subject: [PATCH 1488/1609] Replace commits calendar with contributions calendar * count opening of issues and merge requests * dont trigger git repository - use events from database * much-much faster since does not affected by repository size --- app/assets/javascripts/calendar.js.coffee | 2 +- app/controllers/users_controller.rb | 29 +++++---- app/models/project_contributions.rb | 32 --------- app/models/repository.rb | 35 ---------- app/views/users/calendar.html.haml | 2 +- app/views/users/calendar_activities.html.haml | 53 +++++++-------- lib/gitlab/commits_calendar.rb | 41 ------------ lib/gitlab/contributions_calendar.rb | 65 +++++++++++++++++++ 8 files changed, 104 insertions(+), 155 deletions(-) delete mode 100644 app/models/project_contributions.rb delete mode 100644 lib/gitlab/commits_calendar.rb create mode 100644 lib/gitlab/contributions_calendar.rb diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 2891a48e249..99e7d3a029f 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -7,7 +7,7 @@ class @calendar constructor: (timestamps, starting_year, starting_month, calendar_activities_path) -> cal = new CalHeatMap() cal.init - itemName: ["commit"] + itemName: ["contribution"] data: timestamps start: new Date(starting_year, starting_month) domainLabelFormat: "%b" diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 68130eb128c..f39c820626f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -31,9 +31,7 @@ class UsersController < ApplicationController end def calendar - projects = Project.where(id: authorized_projects_ids & @user.contributed_projects_ids) - - calendar = Gitlab::CommitsCalendar.new(projects, @user) + calendar = contributions_calendar @timestamps = calendar.timestamps @starting_year = calendar.starting_year @starting_month = calendar.starting_month @@ -42,20 +40,13 @@ class UsersController < ApplicationController end def calendar_activities - projects = Project.where(id: authorized_projects_ids & @user.contributed_projects_ids) + @calendar_date = Date.parse(params[:date]) rescue nil + @events = [] - date = Date.parse(params[:date]) rescue nil - if date - @calendar_activities = Gitlab::CommitsCalendar.get_commits_for_date(projects, @user, date) - else - @calendar_activities = {} + if @calendar_date + @events = contributions_calendar.events_by_date(@calendar_date) end - # get the total number of unique commits - @commit_count = @calendar_activities.values.flatten.map(&:id).uniq.count - - @calendar_date = date - render 'calendar_activities', layout: false end @@ -82,4 +73,14 @@ class UsersController < ApplicationController @authorized_projects_ids ||= ProjectsFinder.new.execute(current_user).pluck(:id) end + + def contributed_projects + @contributed_projects = Project. + where(id: authorized_projects_ids & @user.contributed_projects_ids).reject(&:forked?) + end + + def contributions_calendar + @contributions_calendar ||= Gitlab::ContributionsCalendar. + new(contributed_projects, @user) + end end diff --git a/app/models/project_contributions.rb b/app/models/project_contributions.rb deleted file mode 100644 index bfe9928b158..00000000000 --- a/app/models/project_contributions.rb +++ /dev/null @@ -1,32 +0,0 @@ -class ProjectContributions - attr_reader :project, :user - - def initialize(project, user) - @project, @user = project, user - end - - def commits_log - repository = project.repository - - if !repository.exists? || repository.empty? - return {} - end - - Rails.cache.fetch(cache_key) do - repository.commits_per_day_for_user(user) - end - end - - def user_commits_on_date(date) - repository = @project.repository - - if !repository.exists? || repository.empty? - return [] - end - commits = repository.commits_by_user_on_date_log(@user, date) - end - - def cache_key - "#{Date.today.to_s}-commits-log-#{project.id}-#{user.email}" - end -end diff --git a/app/models/repository.rb b/app/models/repository.rb index 082ad7a0c6a..77765cae1a0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -149,41 +149,6 @@ class Repository end end - def timestamps_by_user_log(user) - author_emails = '(' + user.all_emails.map{ |e| Regexp.escape(e) }.join('|') + ')' - args = %W(git log -E --author=#{author_emails} --since=#{(Date.today - 1.year).to_s} --branches --pretty=format:%cd --date=short) - dates = Gitlab::Popen.popen(args, path_to_repo).first.split("\n") - - if dates.present? - dates - else - [] - end - end - - def commits_by_user_on_date_log(user, date) - # format the date string for git - start_date = date.strftime("%Y-%m-%d 00:00:00") - end_date = date.strftime("%Y-%m-%d 23:59:59") - - author_emails = '(' + user.all_emails.map{ |e| Regexp.escape(e) }.join('|') + ')' - args = %W(git log -E --author=#{author_emails} --after=#{start_date.to_s} --until=#{end_date.to_s} --branches --pretty=format:%h) - commits = Gitlab::Popen.popen(args, path_to_repo).first.split("\n") - - commits.map! do |commit_id| - commit(commit_id) - end - end - - def commits_per_day_for_user(user) - timestamps_by_user_log(user). - group_by { |commit_date| commit_date }. - inject({}) do |hash, (timestamp_date, commits)| - hash[timestamp_date] = commits.count - hash - end - end - def lookup_cache @lookup_cache ||= {} end diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index d113ceeb753..12446a209f8 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -1,4 +1,4 @@ -%h4 Commits calendar +%h4 Contributions calendar #cal-heatmap.calendar :javascript new calendar( diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 7c0cecfadb5..4200233dc4b 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,33 +1,24 @@ .calendar_commit_activity - %hr - %h4 - Commit Activity - %strong - - if @commit_count == 0 - no - - else - = @commit_count - %span.calendar_commit_date - unique - = 'commit'.pluralize(@commit_count) - on - = @calendar_date.strftime("%b %d, %Y") rescue '' - -unless @commit_count == 0 - %hr - - @calendar_activities.each do |project, commits| - - next if commits.empty? - %div.js-toggle-container +%h4.prepend-top-20 + %span.light Contributions for + %strong #{@calendar_date.to_s(:short)} + +%ul.bordered-list + - @events.sort_by(&:created_at).each do |event| + %li + %span.light + %i.fa.fa-clock-o + = event.created_at.to_s(:time) + - if event.push? && event.commits_count > 0 + pushed #{event.commits_count} commits to + - else + = event_action_name(event) + - if event.target + %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] + at + %strong - = pluralize(commits.count, 'commit') - in project - = link_to project.name_with_namespace, project_path(project) - %a.text-expander.js-toggle-button … - %hr - %div.js-toggle-content - - commits.each do |commit| - %span.monospace - = commit.committed_date.strftime("%H:%M") - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" - = link_to commit.message, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message str-truncated" - %br - %hr + - if event.project + = link_to_project event.project + - else + = event.project_name diff --git a/lib/gitlab/commits_calendar.rb b/lib/gitlab/commits_calendar.rb deleted file mode 100644 index 8963d346b6f..00000000000 --- a/lib/gitlab/commits_calendar.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Gitlab - class CommitsCalendar - attr_reader :timestamps - - def initialize(projects, user) - @timestamps = {} - date_timestamps = [] - - projects.reject(&:forked?).each do |project| - date_timestamps << ProjectContributions.new(project, user).commits_log - end - - # Sumarrize commits from all projects per days - date_timestamps = date_timestamps.inject do |collection, date| - collection.merge(date) { |k, old_v, new_v| old_v + new_v } - end - - date_timestamps ||= [] - date_timestamps.each do |date, commits| - timestamp = Date.parse(date).to_time.to_i.to_s rescue nil - @timestamps[timestamp] = commits if timestamp - end - end - - def self.get_commits_for_date(projects, user, date) - user_commits = {} - projects.reject(&:forked?).each do |project| - user_commits[project] = ProjectContributions.new(project, user).user_commits_on_date(date) - end - user_commits - end - - def starting_year - (Time.now - 1.year).strftime("%Y") - end - - def starting_month - Date.today.strftime("%m").to_i - end - end -end diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb new file mode 100644 index 00000000000..8ca5115d43a --- /dev/null +++ b/lib/gitlab/contributions_calendar.rb @@ -0,0 +1,65 @@ +module Gitlab + class ContributionsCalendar + attr_reader :timestamps, :projects, :user + + def initialize(projects, user) + @projects = projects + @user = user + end + + def timestamps + return @timestamps if @timestamps.present? + + @timestamps = {} + date_from = 1.year.ago + date_to = Date.today + + events = Event.where(author_id: user.id).where(action: event_type). + where("created_at > ?", date_from).where(project_id: projects) + + grouped_events = events.to_a.group_by { |event| event.created_at.to_date.to_s } + dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a + + dates.each do |date| + date_id = date.to_time.to_i.to_s + @timestamps[date_id] = 0 + + if grouped_events.has_key?(date.to_s) + grouped_events[date.to_s].each do |event| + if event.created_at.to_date == date + if event.issue? || event.merge_request? + @timestamps[date_id] += 1 + elsif event.push? + @timestamps[date_id] += event.commits_count + end + end + end + end + end + + @timestamps + end + + def events_by_date(date) + events = Event.where(author_id: user.id).where(action: event_type). + where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day). + where(project_id: projects) + + events.select do |event| + event.push? || event.issue? || event.merge_request? + end + end + + def starting_year + (Time.now - 1.year).strftime("%Y") + end + + def starting_month + Date.today.strftime("%m").to_i + end + + def event_type + [Event::PUSHED, Event::CREATED, Event::CLOSED, Event::MERGED] + end + end +end -- GitLab From 38ba8b45ad9823d4cff6de98eb0bc78171e67cfc Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Sun, 22 Mar 2015 09:05:13 +0100 Subject: [PATCH 1489/1609] Remove old GitLab auth image --- app/assets/images/authbuttons/gitlab_64.png_old | Bin 2878 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/assets/images/authbuttons/gitlab_64.png_old diff --git a/app/assets/images/authbuttons/gitlab_64.png_old b/app/assets/images/authbuttons/gitlab_64.png_old deleted file mode 100644 index 8390c81a08b310df07fe53a487368f6716745be7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2878 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hElEaktaqI2u@+qND zMUU41zW03@!(ok%#Vn>BE(f~!x>zr0bjg}(2`L}+yRt*aFeBoaSai<}j~-REu45Xn zx;8G(;Bo7nv_gqJudPW~S-vV_CuXKrmt91G|L!gA51NoAw&<9@yKozuiok;WlU9 z=fG;26?w>!{)r-=kFF zU%>W^iD8P=xtK2B7qd?K{CKW)P>6>?TdDeE@P2c<7W1%!g@SW7GqG`<>wYlT=D{!J z1o;KQEU_uqPo1dwaFt`-kpt60!>-Rfpx>~Xl{?(IcY&EvOqEHKc+vN{_kZWuOCLOI zH)G{&(fEUM2Z9b7C1hTC@K)%7(3x-{#vYyp!E7dPng2K&$VyCHsA$dQ#{8afKI7xY zc&a)qR!)LH&jK6g-KT0< z;}~qzZrEHFkW;S^zHu$ENcMsD)`@I8IA^?PZcC6p*zB-3=h?as-%mHD%viXYwe5va zPCymcvBZD79SRCF7QW4Kocmy@g7*9T%=0^bOFS z-TGVjb&_lLtpv&X2$r6UyCyD{l6Y59y!OFPmXZ^p3HAw61(TGsD--I4qa-BtX1rH< zxo7>%W7=(^-7Ofo+e{=(KMfc?eu2-7Wwt-Qi# znEE`WOy}wSkhqb_d?Igs;k^RhW7c*8Hi15g<<^J%1bvQn9BE7XzT^CLhWzdRk6jn% z&fz~KD?RB%V4g_5(1)M=UCN2sA08AQTrIug#^$}kOFz#RO15Wb%RA6@UEb!m_p|P$ zGxPR{9u!INnCG>N)5<;Uno)Y&SKSLU{np7%b=e?wcz-N|#F{W3kjb-Uz&#pmP$K5E$9xqLn9V33aRnyvU79cs90UTS`f`EPe~^6I2FY~g*+xSufuv*ughUj5nX zwT4}PLgtl+pYQ0u#+{e%a{P8x z@^tQbhuJoZRk%#>QMc^l#()YG9?+NxlZ%AE7jJ+)aTu)&idr{cE)y<$#3E&CA274FtiA+;qZ=VX-ru4 zPXE&s$(>H2>K4hDb@J{CUS@h#+v)pZj`j4|x2FsE41_QL5SXKVdiU$*14hRtso178 zPtjMK&$ouhdG<=jy_|a=Zf4p4QS!C=kN)%jk`7+{`ILE8?2AYp3C|h*)rk(o6sqRzjxy#N4g~gw@pT^2~`1iN1 zl6*%M61*ff%(gFI?9;cJSx$S&%uWAlU%bD%TPI9z{hpu?<~h%Hn>fvU+0T0@>EQ8q zPArdp2=GOJjyaGSQ4*nGq3dkvIdk9kE9qt1lHRxPo2XbgZMB1UgZRn)UXR0A|9BfX z7U*nb4Vh(A3-&0&i5fQ&(nebSYRaRnT(4;~dMS z7jfD%RyKT&dHr60$BoOkSex37w07TYIQm;Dcr_YVxLgYw_T4)%NgWEeRw}Du3GrbHeqvwY9c&_yISvA_h{ZWicargQ9r$5iDk&Mvxd6Cc8pi$9=T7x z$H%itt@+%Mup>%NnrvmhnK9`z&)ic!{Wt4D*Cv^t?u}uczHwQ>%a@;FmCMR;T+f)C z)ExBCaL*dfs@O#a{hGHXF}@L6=D6t&rt>#WSlTb~A8Vz*_!Sr(^a zoqZ)KOmoNi;QRA7-oMSbJmKH()#$2aKKC;j}Uxs&CpT7>n3DkM(|0{eS=_(;+Nq5_+Znzcb$C;7b6d4i|E6l5m$v`4ZnnLRnN@MjLg(92 z{a$IGDVmPSDVB#^&fiqFU-;2&+RQhvIzz3OTJN)&EuJohRDuq{`;kvmR|* zyHo1ln=6$nVynJoEtwI-_~yChyZYMAUzg{2JlpN?+2G!NQ;j*gc}l^{`@d}V%->Oc zu_fo@>x~y$j;kBAeox7HZaIH$%isHlat_X&z0~wC@9NKvQ6ZYsQ#0OIosbHdwbR&h zW}5v?)285`Z8rnwExyiCckMwO`$74+k@qetod5A`hEC+Jd*%Jdl6wkfypfl)E7%r( z(EN^=C9w9zTM3e^JQRQVDNPH Kb6Mw<&;$SxT} Date: Sun, 22 Mar 2015 08:24:23 -0700 Subject: [PATCH 1490/1609] Fix "Import projects from" button to show the correct instructions Closes #1267 --- CHANGELOG | 1 + app/views/projects/new.html.haml | 6 +++--- features/dashboard/new_project.feature | 13 ++++++++++++ features/steps/dashboard/new_project.rb | 27 +++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 features/dashboard/new_project.feature create mode 100644 features/steps/dashboard/new_project.rb diff --git a/CHANGELOG b/CHANGELOG index cf00780d332..854f5c88bc3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Fix "Import projects from" button to show the correct instructions (Stan Hu) - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 173a3080b31..9687c8ad87c 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -74,9 +74,9 @@ = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' .alert.alert-info.prepend-top-10 %ul - %li + %li The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: https://username:password@gitlab.company.com/group/project.git. - %li + %li The import will time out after 4 minutes. For big repositories, use a clone/push combination. %li To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"}. @@ -112,6 +112,6 @@ $ -> $('.how_to_import_link').bind 'click', (e) -> e.preventDefault() - import_modal = $(this).parent().find(".modal").show() + import_modal = $(this).next(".modal").show() $('.modal-header .close').bind 'click', -> $(".modal").hide() diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature new file mode 100644 index 00000000000..431dc4ccfcb --- /dev/null +++ b/features/dashboard/new_project.feature @@ -0,0 +1,13 @@ +@dashboard +Feature: New Project +Background: + Given I sign in as a user + And I own project "Shop" + And I visit dashboard page + + @javascript + Scenario: I should see New projects page + Given I click "New project" link + Then I see "New project" page + When I click on "Import project from GitHub" + Then I see instructions on how to import from GitHub diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb new file mode 100644 index 00000000000..5e588ceb780 --- /dev/null +++ b/features/steps/dashboard/new_project.rb @@ -0,0 +1,27 @@ +class Spinach::Features::NewProject < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + + step 'I click "New project" link' do + click_link "New project" + end + + step 'I see "New project" page' do + page.should have_content("Project path") + end + + step 'I click on "Import project from GitHub"' do + first('.how_to_import_link').click + end + + step 'I see instructions on how to import from GitHub' do + github_modal = first('.modal-body') + github_modal.should be_visible + github_modal.should have_content "To enable importing projects from GitHub" + + all('.modal-body').each do |element| + element.should_not be_visible unless element == github_modal + end + end +end -- GitLab From 9c6086bc956a3fa57cb71eab744847b0206109e2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 11:01:45 -0700 Subject: [PATCH 1491/1609] Refactor repository specs --- spec/models/repository_spec.rb | 45 ++++++---------------------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 0e3e0b167d7..f41e5a97ca3 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -13,47 +13,16 @@ describe Repository do it { is_expected.not_to include('fix') } end - describe :last_commit_for_path do - subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } - - it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } - end - - context :timestamps_by_user_log do - before do - Date.stub(:today).and_return(Date.new(2015, 03, 01)) - end - - describe 'single e-mail for user' do - let(:user) { create(:user, email: sample_commit.author_email) } - - subject { repository.timestamps_by_user_log(user) } + describe :tag_names_contains do + subject { repository.tag_names_contains(sample_commit.id) } - it { is_expected.to eq(['2014-08-06', '2014-07-31', '2014-07-31']) } - end - - describe 'multiple emails for user' do - let(:email_alias) { create(:email, email: another_sample_commit.author_email) } - let(:user) { create(:user, email: sample_commit.author_email, emails: [email_alias]) } - - subject { repository.timestamps_by_user_log(user) } - - it { is_expected.to eq(['2015-01-10', '2014-08-06', '2014-07-31', '2014-07-31']) } - end + it { is_expected.to include('v1.1.0') } + it { is_expected.not_to include('v1.0.0') } end - context :commits_by_user_on_date_log do - - describe 'single e-mail for user' do - let(:user) { create(:user, email: sample_commit.author_email) } - let(:commit1) { '0ed8c6c6752e8c6ea63e7b92a517bf5ac1209c80' } - let(:commit2) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } - - subject { repository.commits_by_user_on_date_log(user,Date.new(2014, 07, 31)) } + describe :last_commit_for_path do + subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } - it 'contains the exepected commits' do - expect(subject.flatten.map(&:id)).to eq([commit1, commit2]) - end - end + it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } end end -- GitLab From 20a12438ab470afe1a5736fc46091a6ef4c30073 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 11:14:42 -0700 Subject: [PATCH 1492/1609] Fix user controller specs --- spec/controllers/users_controller_spec.rb | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 7962bcdde71..d47a37914df 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -25,34 +25,21 @@ describe UsersController do end describe 'GET #calendar_activities' do - include RepoHelpers - let(:project) { create(:project) } - let(:calendar_user) { create(:user, email: sample_commit.author_email) } - let(:commit1) { '0ed8c6c6752e8c6ea63e7b92a517bf5ac1209c80' } - let(:commit2) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } + let!(:project) { create(:project) } + let!(:user) { create(:user) } before do allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id]) project.team << [user, :developer] end - it 'assigns @commit_count' do - get :calendar_activities, username: calendar_user.username, date: '2014-07-31' - expect(assigns(:commit_count)).to eq(2) - end - it 'assigns @calendar_date' do - get :calendar_activities, username: calendar_user.username, date: '2014-07-31' + get :calendar_activities, username: user.username, date: '2014-07-31' expect(assigns(:calendar_date)).to eq(Date.parse('2014-07-31')) end - it 'assigns @calendar_activities' do - get :calendar_activities, username: calendar_user.username, date: '2014-07-31' - expect(assigns(:calendar_activities).values.flatten.map(&:id)).to eq([commit1, commit2]) - end - it 'renders calendar_activities' do - get :calendar_activities, username: calendar_user.username + get :calendar_activities, username: user.username expect(response).to render_template('calendar_activities') end end -- GitLab From 497fd75dbd19920d5621fa3b5b1720b6d5cfff66 Mon Sep 17 00:00:00 2001 From: KahWee Teng Date: Mon, 23 Mar 2015 03:09:23 +0800 Subject: [PATCH 1493/1609] Remove the "unreleased" word from v7.9.0 changelog --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index cf00780d332..517efae9f31 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,7 +24,7 @@ v 7.10.0 (unreleased) - Improve oauth accounts UI in profile page - Add ability to unlink connected accounts -v 7.9.0 (unreleased) +v 7.9.0 - Add HipChat integration documentation (Stan Hu) - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) - Fix broken email images (Hannes Rosenögger) -- GitLab From 43afe46bbd19b1edf60abf3f104fb2b0d29af564 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 13:55:00 -0700 Subject: [PATCH 1494/1609] Refactor contributions events and write tests for calendar --- app/controllers/users_controller.rb | 10 ++++---- app/models/event.rb | 6 +++++ app/models/user.rb | 7 ++---- app/views/users/_projects.html.haml | 2 +- features/steps/user.rb | 35 ++++++++++++++++++++++++++++ features/user.feature | 9 +++++++ lib/gitlab/contributions_calendar.rb | 8 ++----- 7 files changed, 59 insertions(+), 18 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f39c820626f..f9b568b8af9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -4,10 +4,7 @@ class UsersController < ApplicationController layout :determine_layout def show - @contributed_projects = Project. - where(id: authorized_projects_ids & @user.contributed_projects_ids). - in_group_namespace. - includes(:namespace). + @contributed_projects = contributed_projects.joined(@user). reject(&:forked?) @projects = @user.personal_projects. @@ -76,11 +73,12 @@ class UsersController < ApplicationController def contributed_projects @contributed_projects = Project. - where(id: authorized_projects_ids & @user.contributed_projects_ids).reject(&:forked?) + where(id: authorized_projects_ids & @user.contributed_projects_ids). + includes(:namespace) end def contributions_calendar @contributions_calendar ||= Gitlab::ContributionsCalendar. - new(contributed_projects, @user) + new(contributed_projects.reject(&:forked?), @user) end end diff --git a/app/models/event.rb b/app/models/event.rb index 2103a48a71b..57f6d5cd4e0 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -55,6 +55,12 @@ class Event < ActiveRecord::Base order('id DESC').limit(100). update_all(updated_at: Time.now) end + + def contributions + where("action = ? OR (target_type in (?) AND action in (?))", + Event::PUSHED, ["MergeRequest", "Issue"], + [Event::CREATED, Event::CLOSED, Event::MERGED]) + end end def proper? diff --git a/app/models/user.rb b/app/models/user.rb index ba325132df8..50f664a09a3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -603,13 +603,10 @@ class User < ActiveRecord::Base end def contributed_projects_ids - Event.where(author_id: self). + Event.contributions.where(author_id: self). where("created_at > ?", Time.now - 1.year). - where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)", - pushed: Event::PUSHED, created: Event::CREATED). reorder(project_id: :desc). select(:project_id). - uniq - .map(&:project_id) + uniq.map(&:project_id) end end diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml index 6c7779be30e..b7383d5594e 100644 --- a/app/views/users/_projects.html.haml +++ b/app/views/users/_projects.html.haml @@ -1,5 +1,5 @@ - if @contributed_projects.present? - .panel.panel-default + .panel.panel-default.contributed-projects .panel-heading Projects contributed to = render 'shared/projects_list', projects: @contributed_projects.sort_by(&:star_count).reverse, diff --git a/features/steps/user.rb b/features/steps/user.rb index d6f05ecb2c7..e6086bfc1c9 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -7,4 +7,39 @@ class Spinach::Features::User < Spinach::FeatureSteps step 'I should see user "John Doe" page' do expect(title).to match(/^\s*John Doe/) end + + step '"John Doe" has contributions' do + user = User.find_by(name: 'John Doe') + project = contributed_project + + # Issue controbution + issue_params = { title: 'Bug in old browser' } + Issues::CreateService.new(project, user, issue_params).execute + + # Push code contribution + push_params = { + project: project, + action: Event::PUSHED, + author_id: user.id, + data: { commit_count: 3 } + } + + Event.create(push_params) + end + + step 'I should see contributed projects' do + within '.contributed-projects' do + page.should have_content(@contributed_project.name) + end + end + + step 'I should see contributions calendar' do + within '.calendar' do + page.should have_css('.graph-rect.r2.q2') + end + end + + def contributed_project + @contributed_project ||= create(:project, :public) + end end diff --git a/features/user.feature b/features/user.feature index a2167935fd2..69618e929c4 100644 --- a/features/user.feature +++ b/features/user.feature @@ -67,3 +67,12 @@ Feature: User And I should see project "Enterprise" And I should not see project "Internal" And I should not see project "Community" + + @javascript + Scenario: "John Doe" contribution profile + Given I sign in as a user + And "John Doe" has contributions + When I visit user "John Doe" page + Then I should see user "John Doe" page + And I should see contributed projects + And I should see contributions calendar diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 8ca5115d43a..ea41644811f 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -14,7 +14,7 @@ module Gitlab date_from = 1.year.ago date_to = Date.today - events = Event.where(author_id: user.id).where(action: event_type). + events = Event.contributions.where(author_id: user.id). where("created_at > ?", date_from).where(project_id: projects) grouped_events = events.to_a.group_by { |event| event.created_at.to_date.to_s } @@ -41,7 +41,7 @@ module Gitlab end def events_by_date(date) - events = Event.where(author_id: user.id).where(action: event_type). + events = Event.contributions.where(author_id: user.id). where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day). where(project_id: projects) @@ -57,9 +57,5 @@ module Gitlab def starting_month Date.today.strftime("%m").to_i end - - def event_type - [Event::PUSHED, Event::CREATED, Event::CLOSED, Event::MERGED] - end end end -- GitLab From 54aca18cf856a18bdd121a3db25d2a64b9e0844d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 14:35:27 -0700 Subject: [PATCH 1495/1609] Contribution calendar will use events instead of commits to count contributions --- app/views/users/calendar.html.haml | 5 ++++- features/steps/user.rb | 2 +- lib/gitlab/contributions_calendar.rb | 19 +++++++------------ 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 12446a209f8..488f49267c7 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -1,4 +1,7 @@ -%h4 Contributions calendar +%h4 + Contributions calendar + .pull-right + %small Issues, merge requests and push events #cal-heatmap.calendar :javascript new calendar( diff --git a/features/steps/user.rb b/features/steps/user.rb index e6086bfc1c9..5939c28a000 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -35,7 +35,7 @@ class Spinach::Features::User < Spinach::FeatureSteps step 'I should see contributions calendar' do within '.calendar' do - page.should have_css('.graph-rect.r2.q2') + page.should have_css('.graph-rect.r3.q3') end end diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index ea41644811f..79e0a514f9e 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -15,25 +15,20 @@ module Gitlab date_to = Date.today events = Event.contributions.where(author_id: user.id). - where("created_at > ?", date_from).where(project_id: projects) + where("created_at > ?", date_from).where(project_id: projects). + group('date(created_at)'). + select('date(created_at), count(id) as total_amount'). + reorder(nil).map(&:attributes) - grouped_events = events.to_a.group_by { |event| event.created_at.to_date.to_s } dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a dates.each do |date| date_id = date.to_time.to_i.to_s @timestamps[date_id] = 0 + day_events = events.find { |day_events| day_events["date"] == date } - if grouped_events.has_key?(date.to_s) - grouped_events[date.to_s].each do |event| - if event.created_at.to_date == date - if event.issue? || event.merge_request? - @timestamps[date_id] += 1 - elsif event.push? - @timestamps[date_id] += event.commits_count - end - end - end + if day_events + @timestamps[date_id] = day_events["total_amount"] end end -- GitLab From 7d84252e052de03431edf39d1f3eeebad34758be Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 14:37:14 -0700 Subject: [PATCH 1496/1609] Update CHANGELOG with contribution calendar --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index cf00780d332..ff2ac6a095f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 7.10.0 (unreleased) - Restrict permissions on backup files - Improve oauth accounts UI in profile page - Add ability to unlink connected accounts + - Replace commits calendar with faster contribution calendar that includes issues and merge requests v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) -- GitLab From 8494170550063c2d0308963cbd14cb46a292c401 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 14:52:44 -0700 Subject: [PATCH 1497/1609] Improve contribution calendar per day info --- app/assets/javascripts/calendar.js.coffee | 3 --- app/assets/stylesheets/generic/calendar.scss | 21 ------------------- app/views/users/calendar_activities.html.haml | 7 +++---- lib/gitlab/contributions_calendar.rb | 4 ++-- 4 files changed, 5 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 99e7d3a029f..d08ef9361a6 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -27,7 +27,6 @@ class @calendar legendCellPadding: 3 onClick: (date, count) -> formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate() - $(".calendar_commit_activity").fadeOut 400 $.ajax url: calendar_activities_path data: @@ -36,6 +35,4 @@ class @calendar dataType: "html" success: (data) -> $(".user-calendar-activities").html data - $(".calendar_commit_activity").find(".js-toggle-content").hide() - $(".calendar_commit_activity").fadeIn 400 diff --git a/app/assets/stylesheets/generic/calendar.scss b/app/assets/stylesheets/generic/calendar.scss index e2ab7fc51a5..a36fefe22c5 100644 --- a/app/assets/stylesheets/generic/calendar.scss +++ b/app/assets/stylesheets/generic/calendar.scss @@ -1,21 +1,8 @@ .user-calendar-activities { - - .calendar_commit_activity { - padding: 5px 0 0; - } - .calendar_onclick_hr { padding: 0; margin: 10px 0; } - - .calendar_commit_date { - color: #999; - } - - .calendar_activity_summary { - font-size: 14px; - } .str-truncated { max-width: 70%; @@ -31,14 +18,6 @@ background-color: #ddd; } } - - .commit-row-message { - color: #333; - &:hover { - color: #444; - text-decoration: underline; - } - } } /** * This overwrites the default values of the cal-heatmap gem diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 4200233dc4b..027a93a75fc 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,4 +1,3 @@ -.calendar_commit_activity %h4.prepend-top-20 %span.light Contributions for %strong #{@calendar_date.to_s(:short)} @@ -9,14 +8,14 @@ %span.light %i.fa.fa-clock-o = event.created_at.to_s(:time) - - if event.push? && event.commits_count > 0 - pushed #{event.commits_count} commits to + - if event.push? + #{event.action_name} #{event.ref_type} #{event.ref_name} - else = event_action_name(event) - if event.target %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target] - at + at %strong - if event.project = link_to_project event.project diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb index 79e0a514f9e..3fd0823df06 100644 --- a/lib/gitlab/contributions_calendar.rb +++ b/lib/gitlab/contributions_calendar.rb @@ -14,11 +14,11 @@ module Gitlab date_from = 1.year.ago date_to = Date.today - events = Event.contributions.where(author_id: user.id). + events = Event.reorder(nil).contributions.where(author_id: user.id). where("created_at > ?", date_from).where(project_id: projects). group('date(created_at)'). select('date(created_at), count(id) as total_amount'). - reorder(nil).map(&:attributes) + map(&:attributes) dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a -- GitLab From 99682502119fa67f229b3e8d3c3ed0ccb68b29f9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 15:40:26 -0700 Subject: [PATCH 1498/1609] Add inifinite scroll to user activity on user page --- CHANGELOG | 1 + app/assets/javascripts/dispatcher.js.coffee | 1 + app/controllers/users_controller.rb | 18 +++++++++++++----- app/views/users/show.html.haml | 3 ++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cf00780d332..42848bcb317 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ v 7.10.0 (unreleased) - Restrict permissions on backup files - Improve oauth accounts UI in profile page - Add ability to unlink connected accounts + - Add inifinite scroll to user page activity v 7.9.0 (unreleased) - Add HipChat integration documentation (Stan Hu) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index edf482f33d8..deabaf8a784 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -97,6 +97,7 @@ class Dispatcher new ProjectFork() when 'users:show' new User() + new Activities() switch path.first() when 'admin' diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 68130eb128c..95ee9e83dbd 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -16,17 +16,16 @@ class UsersController < ApplicationController # Collect only groups common for both users @groups = @user.groups & GroupsFinder.new.execute(current_user) - # Get user activity feed for projects common for both users - @events = @user.recent_events. - where(project_id: authorized_projects_ids). - with_associations.limit(30) - @title = @user.name @title_url = user_path(@user) respond_to do |format| format.html format.atom { render layout: false } + format.json do + load_events + pager_json("events/_events", @events.count) + end end end @@ -82,4 +81,13 @@ class UsersController < ApplicationController @authorized_projects_ids ||= ProjectsFinder.new.execute(current_user).pluck(:id) end + + def load_events + # Get user activity feed for projects common for both users + @events = @user.recent_events. + where(project_id: authorized_projects_ids). + with_associations + + @events = @events.limit(20).offset(params[:offset] || 0) + end end diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index fd96020d129..0653fb871ae 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -40,7 +40,8 @@ %strong %i.fa.fa-rss - = render @events + .content_list + = spinner %aside.col-md-4 = render 'profile', user: @user = render 'projects' -- GitLab From e15d154b1c30d8ab8ebd5c9b94e9cca30c2cf7e3 Mon Sep 17 00:00:00 2001 From: Paul Beattie Date: Sun, 22 Mar 2015 22:41:21 +0000 Subject: [PATCH 1499/1609] Updated upgrader to highlight nodejs as required package [ci skip] --- doc/update/upgrader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md index d8476fb3457..f62a53d3340 100644 --- a/doc/update/upgrader.md +++ b/doc/update/upgrader.md @@ -24,7 +24,7 @@ If you have local changes to your GitLab repository the script will stash them a ## 2. Run GitLab upgrade tool -Note: GitLab 7.9 adds nodejs as a dependency. GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX). GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +Note: GitLab 7.9 adds `nodejs` as a dependency. GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX). GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) # Starting with GitLab version 7.0 upgrader script has been moved to bin directory cd /home/git/gitlab -- GitLab From 5cce0645b07265e3c2b991bcbff351a9acbc90d6 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 22 Mar 2015 18:07:33 -0700 Subject: [PATCH 1500/1609] Fix OAuth2 issue importing a new project from GitHub and GitLab Closes #1268 --- CHANGELOG | 1 + lib/gitlab/bitbucket_import/client.rb | 4 ++-- lib/gitlab/github_import/client.rb | 2 +- lib/gitlab/gitlab_import/client.rb | 2 +- spec/lib/gitlab/bitbucket_import/client_spec.rb | 17 +++++++++++++++++ spec/lib/gitlab/github_import/client_spec.rb | 16 ++++++++++++++++ spec/lib/gitlab/gitlab_import/client_spec.rb | 16 ++++++++++++++++ 7 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 spec/lib/gitlab/bitbucket_import/client_spec.rb create mode 100644 spec/lib/gitlab/github_import/client_spec.rb create mode 100644 spec/lib/gitlab/gitlab_import/client_spec.rb diff --git a/CHANGELOG b/CHANGELOG index f5a53747881..ea325d4892a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) - Fix "Import projects from" button to show the correct instructions (Stan Hu) - Fix dots in Wiki slugs causing errors (Stan Hu) + - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu) diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 1e4906c9e31..5b1952b9675 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -62,7 +62,7 @@ module Gitlab end def find_deploy_key(project_identifier, key) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| + JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| deploy_key["key"].chomp == key.chomp end end @@ -92,7 +92,7 @@ module Gitlab end def bitbucket_options - OmniAuth::Strategies::Bitbucket.default_options[:client_options].dup + OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys end end end diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 7fe076b333b..270cbcd9ccd 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -46,7 +46,7 @@ module Gitlab end def github_options - OmniAuth::Strategies::GitHub.default_options[:client_options].dup + OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys end end end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index 2236439c6ce..f48ede9d067 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -71,7 +71,7 @@ module Gitlab end def gitlab_options - OmniAuth::Strategies::GitLab.default_options[:client_options].dup + OmniAuth::Strategies::GitLab.default_options[:client_options].symbolize_keys end end end diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb new file mode 100644 index 00000000000..dd450e9967b --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::BitbucketImport::Client do + let(:token) { '123456' } + let(:secret) { 'secret' } + let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) } + + before do + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") + end + + it 'all OAuth client options are symbols' do + client.consumer.options.keys.each do |key| + expect(key).to be_kind_of(Symbol) + end + end +end diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb new file mode 100644 index 00000000000..26618120316 --- /dev/null +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::Client do + let(:token) { '123456' } + let(:client) { Gitlab::GithubImport::Client.new(token) } + + before do + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "github") + end + + it 'all OAuth2 client options are symbols' do + client.client.options.keys.each do |key| + expect(key).to be_kind_of(Symbol) + end + end +end diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb new file mode 100644 index 00000000000..c511c515474 --- /dev/null +++ b/spec/lib/gitlab/gitlab_import/client_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::GitlabImport::Client do + let(:token) { '123456' } + let(:client) { Gitlab::GitlabImport::Client.new(token) } + + before do + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "gitlab") + end + + it 'all OAuth2 client options are symbols' do + client.client.options.keys.each do |key| + expect(key).to be_kind_of(Symbol) + end + end +end -- GitLab From a7afc0634240f5cddb6c6e1bf1f9fcf4374b852e Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 22 Mar 2015 09:02:32 -0600 Subject: [PATCH 1501/1609] Fix SanitizationFilter bugs Return a `SafeBuffer` instead of a `String` from the `#gfm_with_options` method so that Rails doesn't escape our markup. Also add `` to the sanitization whitelist to avoid breaking syntax highlighting in code blocks. --- lib/gitlab/markdown.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 65dce9291e6..11da4be4022 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -89,6 +89,7 @@ module Gitlab whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST whitelist[:attributes][:all].push('class', 'id') + whitelist[:elements].push('span') # Remove the rel attribute that the sanitize gem adds, and remove the # href attribute if it contains inline javascript @@ -123,7 +124,7 @@ module Gitlab text = parse_tasks(text) end - text + text.html_safe end private -- GitLab From fee1f2e0ab0336e6746aae8e9ed76549c6ac3353 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 19:41:12 -0700 Subject: [PATCH 1502/1609] Fix atom feed for user page --- app/controllers/users_controller.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 95ee9e83dbd..5c618001fdf 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -21,7 +21,12 @@ class UsersController < ApplicationController respond_to do |format| format.html - format.atom { render layout: false } + + format.atom do + load_events + render layout: false + end + format.json do load_events pager_json("events/_events", @events.count) -- GitLab From b449bc5b77aa658db67ebcb25f709fe51c29b129 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 19:47:23 -0700 Subject: [PATCH 1503/1609] Improve user calendar test --- features/steps/user.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/features/steps/user.rb b/features/steps/user.rb index 5939c28a000..10cae692a88 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -34,9 +34,7 @@ class Spinach::Features::User < Spinach::FeatureSteps end step 'I should see contributions calendar' do - within '.calendar' do - page.should have_css('.graph-rect.r3.q3') - end + page.should have_css('.cal-heatmap-container') end def contributed_project -- GitLab From 9e554a5223d5ed7e08e5db58617c9bc1b9564402 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 20:10:12 -0700 Subject: [PATCH 1504/1609] Update views to single form of address. Change "my" to "your" --- app/helpers/search_helper.rb | 6 +++--- app/views/devise/mailer/confirmation_instructions.html.erb | 2 +- .../devise/mailer/reset_password_instructions.html.erb | 2 +- app/views/devise/mailer/unlock_instructions.html.erb | 2 +- app/views/devise/passwords/edit.html.haml | 2 +- app/views/devise/registrations/edit.html.erb | 4 ++-- app/views/help/ui.html.haml | 2 +- app/views/layouts/_head_panel.html.haml | 2 +- app/views/profiles/history.html.haml | 2 +- app/views/profiles/keys/index.html.haml | 2 -- app/views/projects/_home_panel.html.haml | 2 +- app/views/snippets/current_user_index.html.haml | 2 +- app/views/snippets/index.html.haml | 4 ++-- app/views/snippets/show.html.haml | 2 +- 14 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 7d3fcfa7037..c31a556ff7b 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -23,9 +23,9 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { label: "My Profile settings", url: profile_path }, - { label: "My SSH Keys", url: profile_keys_path }, - { label: "My Dashboard", url: root_path }, + { label: "Profile settings", url: profile_path }, + { label: "SSH Keys", url: profile_keys_path }, + { label: "Dashboard", url: root_path }, { label: "Admin Section", url: admin_root_path }, ] end diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb index cb1291cf3bf..c6fa8f0ee36 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -6,4 +6,4 @@

        You can confirm your account through the link below:

        <% end %> -

        <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

        +

        <%= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) %>

        diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index 7913e88beb6..23b31da92d8 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -2,7 +2,7 @@

        Someone has requested a link to change your password, and you can do this through the link below.

        -

        <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

        +

        <%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %>

        If you didn't request this, please ignore this email.

        Your password won't change until you access the link above and create a new one.

        diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb index 8c2a4f0c2d9..79d6c761d8f 100644 --- a/app/views/devise/mailer/unlock_instructions.html.erb +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -4,4 +4,4 @@

        Click the link below to unlock your account:

        -

        <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

        +

        <%= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) %>

        diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 0640739b5d7..56048e99c17 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -11,7 +11,7 @@ %div = f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true .clearfix - = f.submit "Change my password", class: "btn btn-primary" + = f.submit "Change your password", class: "btn btn-primary" .clearfix.prepend-top-20 %p diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index b11817af95d..f379e71ae5b 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -21,8 +21,8 @@
        <%= f.submit "Update", class: "input_button" %>
        <% end %> -

        Cancel my account

        +

        Cancel your account

        -

        Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>.

        +

        Unhappy? <%= link_to "Cancel your account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>.

        <%= link_to "Back", :back %> diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index ed03f885dcd..246a6c1bdfd 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -53,7 +53,7 @@ %code .panel .well-list .panel.panel-default - .panel-heading My list + .panel-heading Your list %ul.well-list %li One item diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml index fc8a487ece7..b1c2e1a7b19 100644 --- a/app/views/layouts/_head_panel.html.haml +++ b/app/views/layouts/_head_panel.html.haml @@ -25,7 +25,7 @@ = link_to explore_root_path, title: "Explore", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do %i.fa.fa-globe %li - = link_to user_snippets_path(current_user), title: "My snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'My snippets' do + = link_to user_snippets_path(current_user), title: "Your snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'Your snippets' do %i.fa.fa-clipboard - if current_user.is_admin? %li diff --git a/app/views/profiles/history.html.haml b/app/views/profiles/history.html.haml index 9cafe03b8b3..b1ab433f48f 100644 --- a/app/views/profiles/history.html.haml +++ b/app/views/profiles/history.html.haml @@ -1,5 +1,5 @@ %h3.page-title - My Account History + Your Account History %p.light All events created by your account are listed below. %hr diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml index 965d5e032f9..0904c50c88b 100644 --- a/app/views/profiles/keys/index.html.haml +++ b/app/views/profiles/keys/index.html.haml @@ -3,8 +3,6 @@ .pull-right = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new" %p.light - My SSH keys: #{@keys.count} - %br Before you can add an SSH key you need to = link_to "generate it.", help_page_path("ssh", "README") %hr diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index c0e13e67be3..a295a0d6cdc 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -19,7 +19,7 @@ .fork-buttons - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to my fork' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork' do = link_to_toggle_fork - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project" do diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml index b2b7ea4df0e..0df5ade500d 100644 --- a/app/views/snippets/current_user_index.html.haml +++ b/app/views/snippets/current_user_index.html.haml @@ -1,5 +1,5 @@ %h3.page-title - My Snippets + Your Snippets .pull-right = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do Add new snippet diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index 0d71c41e2e7..5cd8ae26cf9 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -2,12 +2,12 @@ Public snippets .pull-right - + - if current_user = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do Add new snippet = link_to user_snippets_path(current_user), class: "btn btn-grouped" do - My snippets + Your snippets %p.light Public snippets created by you and other users are listed here diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index edfa2092df9..55a990c94ed 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -23,7 +23,7 @@ .back-link - if @snippet.author == current_user = link_to user_snippets_path(current_user) do - ← my snippets + ← your snippets - else = link_to snippets_path do ← discover snippets -- GitLab From bc4e25189805879490555ef2782193470f4fe295 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 22 Mar 2015 20:16:25 -0700 Subject: [PATCH 1505/1609] Make panel heading font bold --- app/assets/stylesheets/base/gl_bootstrap.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index 82c51cf4852..e176cce5c69 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -152,6 +152,8 @@ */ .panel { .panel-heading { + font-weight: bold; + .panel-head-actions { position: relative; top: -5px; -- GitLab From b7ed7d05a0b2aab9123c101a1a0558ae0f9bf7cc Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sun, 22 Mar 2015 21:36:56 -0600 Subject: [PATCH 1506/1609] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index f38a075fff5..6d77b5df95c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ v 7.10.0 (unreleased) - Add changelog, license and contribution guide links to project sidebar. - Improve diff UI - Fix alignment of navbar toggle button (Cody Mize) + - Fix checkbox rendering for nested task lists - Identical look of selectboxes in UI - Move "Import existing repository by URL" option to button. - Improve error message when save profile has error. -- GitLab From a793c06532240c1f82b13fd45203c232904deb7d Mon Sep 17 00:00:00 2001 From: vichak Date: Mon, 23 Mar 2015 10:38:33 +0100 Subject: [PATCH 1507/1609] Fix #8966 Remove Milestones ans Labels from project navbar when Issues disabled --- app/views/layouts/nav/_project.html.haml | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 91cae2b572c..ce1971c7bb4 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -44,11 +44,12 @@ %span Graphs - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - %i.fa.fa-clock-o - %span - Milestones + - if project_nav_tab? :issues + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + %i.fa.fa-clock-o + %span + Milestones - if project_nav_tab? :issues = nav_link(controller: :issues) do @@ -67,11 +68,12 @@ Merge Requests %span.count.merge_counter= @project.merge_requests.opened.count - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - %i.fa.fa-tags - %span - Labels + - if project_nav_tab? :issues + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + %i.fa.fa-tags + %span + Labels - if project_nav_tab? :wiki = nav_link(controller: :wikis) do -- GitLab From a2e161cb35eb8b755ebdff92ab95f0fe4ca5d87d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 23 Mar 2015 12:11:38 +0000 Subject: [PATCH 1508/1609] Bump Docker build to GitLab v7.9.0 --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 4eb280f9554..f34cbc38a54 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update -q \ # If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.8.3-omnibus-1_amd64.deb \ + wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.0-omnibus.2-1_amd64.deb \ && dpkg -i $TMP_FILE \ && rm -f $TMP_FILE -- GitLab From a3daead1b00c28a16684cf11970ebd6da27511e2 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 23 Mar 2015 05:51:38 -0700 Subject: [PATCH 1509/1609] Include missing events and fix save functionality in admin service template settings form Closes #1275 --- CHANGELOG | 1 + app/controllers/admin/services_controller.rb | 4 ++- app/views/admin/services/_form.html.haml | 13 +++++++++ features/admin/settings.feature | 7 +++++ features/steps/admin/settings.rb | 29 ++++++++++++++++++++ 5 files changed, 53 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f5a53747881..84f4446d017 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Include missing events and fix save functionality in admin service template settings form (Stan Hu) - Fix "Import projects from" button to show the correct instructions (Stan Hu) - Fix dots in Wiki slugs causing errors (Stan Hu) - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 44a3f1379d8..76a938c5fe4 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -46,7 +46,9 @@ class Admin::ServicesController < Admin::ApplicationController :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :build_key, :server, :teamcity_url, :build_type, :description, :issues_url, :new_issue_url, :restrict_to_branch, - :send_from_committer_email, :disable_diffs + :send_from_committer_email, :disable_diffs, + :push_events, :tag_push_events, :note_events, :issues_events, + :merge_requests_events ]) end end diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index a953833b37c..18b7e8ba270 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -14,6 +14,11 @@ = preserve do = markdown @service.help + .form-group + = f.label :active, "Active", class: "control-label" + .col-sm-10 + = f.check_box :active + - if @service.supported_events.length > 1 .form-group = f.label :url, "Trigger", class: 'control-label' @@ -34,6 +39,14 @@ %strong Tag push events %p.light This url will be triggered when a new tag is pushed to the repository + - if @service.supported_events.include?("note") + %div + = f.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = f.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This url will be triggered when someone adds a comment - if @service.supported_events.include?("issue") %div = f.check_box :issues_events, class: 'pull-left' diff --git a/features/admin/settings.feature b/features/admin/settings.feature index 8fdf0575c2c..52e47307b23 100644 --- a/features/admin/settings.feature +++ b/features/admin/settings.feature @@ -7,3 +7,10 @@ Feature: Admin Settings Scenario: Change application settings When I modify settings and save form Then I should see application settings saved + + Scenario: Change Slack Service Template settings + When I click on "Service Templates" + And I click on "Slack" service + Then I check all events and submit form + And I should see service template settings saved + And I should see all checkboxes checked diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb index c2d0d2a3fa3..87d4e969ff5 100644 --- a/features/steps/admin/settings.rb +++ b/features/steps/admin/settings.rb @@ -15,4 +15,33 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps current_application_settings.home_page_url.should == 'https://about.gitlab.com/' page.should have_content 'Application settings saved successfully' end + + step 'I click on "Service Templates"' do + click_link 'Service Templates' + end + + step 'I click on "Slack" service' do + click_link 'Slack' + end + + step 'I check all events and submit form' do + page.check('Active') + page.check('Push events') + page.check('Tag push events') + page.check('Comments') + page.check('Issues events') + page.check('Merge Request events') + fill_in 'Webhook', with: "http://localhost" + click_on 'Save' + end + + step 'I should see service template settings saved' do + page.should have_content 'Application settings saved successfully' + end + + step 'I should see all checkboxes checked' do + all('input[type=checkbox]').each do |checkbox| + checkbox.should be_checked + end + end end -- GitLab From b13bed62eaa047560370692f22041993635f83ee Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 23 Mar 2015 15:00:12 +0100 Subject: [PATCH 1510/1609] Clean up code by using keyword arguments. --- app/mailers/emails/projects.rb | 8 +++++++- .../project_services/emails_on_push_service.rb | 8 +++++++- app/workers/emails_on_push_worker.rb | 16 ++++++++-------- spec/mailers/notify_spec.rb | 12 ++++++------ 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index d2165c9f764..48458baa674 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -16,7 +16,13 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, author_id, ref, action, compare, reverse_compare = false, send_from_committer_email = false, disable_diffs = false) + def repository_push_email(project_id, recipient, author_id:, + ref:, + action:, + compare: nil, + reverse_compare: false, + send_from_committer_email: false, + disable_diffs: false) @project = Project.find(project_id) @author = User.find(author_id) @reverse_compare = reverse_compare diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 3e465ab1a38..6f6e5950aab 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -42,7 +42,13 @@ class EmailsOnPushService < Service def execute(push_data) return unless supported_events.include?(push_data[:object_kind]) - EmailsOnPushWorker.perform_async(project_id, recipients, push_data, send_from_committer_email?, disable_diffs?) + EmailsOnPushWorker.perform_async( + project_id, + recipients, + push_data, + send_from_committer_email: send_from_committer_email?, + disable_diffs: disable_diffs? + ) end def send_from_committer_email? diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 429b29525dc..89fa2117dd2 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,7 +1,7 @@ class EmailsOnPushWorker include Sidekiq::Worker - def perform(project_id, recipients, push_data, send_from_committer_email = false, disable_diffs = false) + def perform(project_id, recipients, push_data, send_from_committer_email: false, disable_diffs: false) project = Project.find(project_id) before_sha = push_data["before"] after_sha = push_data["after"] @@ -37,13 +37,13 @@ class EmailsOnPushWorker Notify.repository_push_email( project_id, recipient, - author_id, - ref, - action, - compare, - reverse_compare, - send_from_committer_email, - disable_diffs + author_id: author_id, + ref: ref, + action: action, + compare: compare, + reverse_compare: reverse_compare, + send_from_committer_email: send_from_committer_email, + disable_diffs: disable_diffs ).deliver end ensure diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index aeb0e14d836..ba42f9e5c70 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -645,7 +645,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/heads/master', :create, nil) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -671,7 +671,7 @@ describe Notify do let(:user) { create(:user) } let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/tags/v1.0', :create, nil) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -696,7 +696,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/heads/master', :delete, nil) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -717,7 +717,7 @@ describe Notify do let(:example_site_path) { root_path } let(:user) { create(:user) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/tags/v1.0', :delete, nil) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -742,7 +742,7 @@ describe Notify do let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base), to: Commit.new(compare.head)) } let(:send_from_committer_email) { false } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/heads/master', :push, compare, false, send_from_committer_email) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -830,7 +830,7 @@ describe Notify do let(:commits) { Commit.decorate(compare.commits) } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } - subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'refs/heads/master', :push, compare) } + subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } it 'is sent as the author' do sender = subject.header[:from].addrs[0] -- GitLab From a30645c68f31d848ebad8c7ca395b55b9449db6d Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 23 Mar 2015 16:17:10 +0100 Subject: [PATCH 1511/1609] Don't show commit comment button when user is not signed in. --- CHANGELOG | 1 + app/views/projects/diffs/_text_file.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c8b2dd2ba77..09ef479be57 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 7.10.0 (unreleased) - Restrict permissions on backup files - Improve oauth accounts UI in profile page - Add ability to unlink connected accounts + - Don't show commit comment button when user is not signed in. v 7.9.0 - Add HipChat integration documentation (Stan Hu) diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index b1c987563f1..e691db9c08e 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -16,7 +16,7 @@ - else %td.old_line = link_to raw(type == "new" ? " " : line_old), "##{line_code}", id: line_code - - if @comments_allowed + - if @comments_allowed && can?(current_user, :write_note, @project) = link_to_new_diff_note(line_code) %td.new_line{data: {linenumber: line.new_pos}} = link_to raw(type == "old" ? " " : line.new_pos) , "##{line_code}", id: line_code -- GitLab From 6b92236eeb74fa9854165c498a80f52e25e60e18 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 23 Mar 2015 16:43:08 +0100 Subject: [PATCH 1512/1609] Don't include system notes in issue/MR comment count. --- CHANGELOG | 1 + app/models/note.rb | 1 + app/views/projects/commits/_commit.html.haml | 2 +- app/views/projects/issues/_issue.html.haml | 5 +++-- app/views/projects/merge_requests/_merge_request.html.haml | 5 +++-- app/views/projects/merge_requests/_show.html.haml | 2 +- lib/gitlab/project_search_results.rb | 2 +- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c8b2dd2ba77..d824e871bc4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 7.10.0 (unreleased) - Restrict permissions on backup files - Improve oauth accounts UI in profile page - Add ability to unlink connected accounts + - Don't include system notes in issue/MR comment count. v 7.9.0 - Add HipChat integration documentation (Stan Hu) diff --git a/app/models/note.rb b/app/models/note.rb index 27b583a869a..e86160e7cd9 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -48,6 +48,7 @@ class Note < ActiveRecord::Base scope :inline, ->{ where("line_code IS NOT NULL") } scope :not_inline, ->{ where(line_code: [nil, '']) } scope :system, ->{ where(system: true) } + scope :user, ->{ where(system: false) } scope :common, ->{ where(noteable_type: ["", nil]) } scope :fresh, ->{ order(created_at: :asc, id: :asc) } scope :inc_author_project, ->{ includes(:project, :author) } diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 4c853f577e9..c6026f96804 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -13,7 +13,7 @@ - note_count = @note_counts.fetch(commit.id, 0) - else - notes = project.notes.for_commit_id(commit.id) - - note_count = notes.count + - note_count = notes.user.count - if note_count > 0 %span.light diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 7b06fe72882..998e74d12cf 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -10,11 +10,12 @@ - if issue.closed? %span CLOSED - - if issue.notes.any? + - note_count = issue.notes.user.count + - if note_count > 0   %span %i.fa.fa-comments - = issue.notes.count + = note_count .issue-info = link_to "##{issue.iid}", issue_path(issue), class: "light" diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index ecbff722b42..4f30d1e69f7 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -16,11 +16,12 @@ %span.label-branch< %i.fa.fa-code-fork %span= merge_request.target_branch - - if merge_request.notes.any? + - note_count = merge_request.mr_and_commit_notes.user.count + - if note_count > 0   %span %i.fa.fa-comments - = merge_request.mr_and_commit_notes.count + = note_count .merge-request-info = link_to "##{merge_request.iid}", merge_request_path(merge_request), class: "light" - if merge_request.assignee diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index ca4ceecb225..a74aede4e6b 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -40,7 +40,7 @@ = link_to merge_request_path(@merge_request) do %i.fa.fa-comments Discussion - %span.badge= @merge_request.mr_and_commit_notes.count + %span.badge= @merge_request.mr_and_commit_notes.user.count %li.commits-tab{data: {action: 'commits'}} = link_to merge_request_path(@merge_request), title: 'Commits' do %i.fa.fa-history diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 8b85f3da83f..0dab7bcfa4d 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -67,7 +67,7 @@ module Gitlab end def notes - Note.where(project_id: limit_project_ids).search(query).order('updated_at DESC') + Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC') end def limit_project_ids -- GitLab From e5b8cf0a8faad52c3c08a42a7a628318e11a3f60 Mon Sep 17 00:00:00 2001 From: vichak Date: Mon, 23 Mar 2015 16:48:50 +0100 Subject: [PATCH 1513/1609] Fix #8966 Remove Milestones/Labels from project navbar when Isses disabled --- app/helpers/projects_helper.rb | 4 ++++ app/views/layouts/nav/_project.html.haml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a14277180c7..77ad7a40fab 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -146,6 +146,10 @@ module ProjectsHelper nav_tabs << feature if project.send :"#{feature}_enabled" end + if project.issues_enabled + nav_tabs << [:milestones, :labels] + end + nav_tabs.flatten end diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index ce1971c7bb4..52681865d64 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -44,7 +44,7 @@ %span Graphs - - if project_nav_tab? :issues + - if project_nav_tab? :milestones = nav_link(controller: :milestones) do = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do %i.fa.fa-clock-o @@ -68,7 +68,7 @@ Merge Requests %span.count.merge_counter= @project.merge_requests.opened.count - - if project_nav_tab? :issues + - if project_nav_tab? :labels = nav_link(controller: :labels) do = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do %i.fa.fa-tags -- GitLab From 70fa9d5629f6a368da5d649450e3e575ca1c252a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 23 Mar 2015 17:30:19 +0100 Subject: [PATCH 1514/1609] Don't mark merge request as updated when merge status relative to target branch changes. --- CHANGELOG | 1 + app/models/merge_request.rb | 9 +++++++++ app/services/merge_requests/refresh_service.rb | 12 +++--------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c8b2dd2ba77..83f6c16281d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ v 7.10.0 (unreleased) - Restrict permissions on backup files - Improve oauth accounts UI in profile page - Add ability to unlink connected accounts + - Don't mark merge request as updated when merge status relative to target branch changes. v 7.9.0 - Add HipChat integration documentation (Stan Hu) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 798306f6dcc..5634f9a686e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -105,6 +105,15 @@ class MergeRequest < ActiveRecord::Base state :unchecked state :can_be_merged state :cannot_be_merged + + around_transition do |merge_request, transition, block| + merge_request.record_timestamps = false + begin + block.call + ensure + merge_request.record_timestamps = true + end + end end validates :source_project, presence: true, unless: :allow_broken diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 7eef2c2d6a5..e9b526d1fb7 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -53,7 +53,7 @@ module MergeRequests if merge_request.source_branch == @branch_name || force_push? merge_request.reload_code - update_merge_request(merge_request) + merge_request.mark_as_unchecked else mr_commit_ids = merge_request.commits.map(&:id) push_commit_ids = @commits.map(&:id) @@ -61,20 +61,14 @@ module MergeRequests if matches.any? merge_request.reload_code - update_merge_request(merge_request) + merge_request.mark_as_unchecked else - update_merge_request(merge_request) + merge_request.mark_as_unchecked end end end end - def update_merge_request(merge_request) - MergeRequests::UpdateService.new( - merge_request.target_project, - @current_user, merge_status: 'unchecked').execute(merge_request) - end - # Add comment about pushing new commits to merge requests def comment_mr_with_commits merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a -- GitLab From 61700f619c61fd2b7b56259097c03e1cfe66444a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 23 Mar 2015 09:38:52 -0700 Subject: [PATCH 1515/1609] Fix file mode going to next line in diff header --- app/assets/stylesheets/pages/diff.scss | 10 +++++----- app/views/projects/diffs/_file.html.haml | 13 +++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 7b7bb88bc20..af6ea58382f 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -15,6 +15,11 @@ word-break: break-all; margin-right: 200px; display: block; + + .file-mode { + margin-left: 10px; + color: #777; + } } .diff-btn-group { @@ -34,11 +39,6 @@ font-family: $monospace_font; font-size: smaller; } - - .file-mode { - font-family: $monospace_font; - margin-left: 10px; - } } .diff-content { overflow: auto; diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 860ab096341..672a6635321 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -13,12 +13,13 @@ - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) = submodule_link(submodule_item, @commit.id) - else - - if diff_file.renamed_file - %span= "#{diff_file.old_path} renamed to #{diff_file.new_path}" - - else - %span= diff_file.new_path - - if diff_file.mode_changed? - %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" + %span + - if diff_file.renamed_file + = "#{diff_file.old_path} renamed to #{diff_file.new_path}" + - else + = diff_file.new_path + - if diff_file.mode_changed? + %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" .diff-btn-group - if blob.text? -- GitLab From 97dd1046e76baf97b371d6939f7008eda4a83577 Mon Sep 17 00:00:00 2001 From: hebbet Date: Mon, 23 Mar 2015 20:07:13 +0100 Subject: [PATCH 1516/1609] Change comment in blue ui to match other scss Change comment in blue ui to match other scss files --- app/assets/stylesheets/themes/ui_blue.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/themes/ui_blue.scss b/app/assets/stylesheets/themes/ui_blue.scss index cb7980b5a07..e223058be8b 100644 --- a/app/assets/stylesheets/themes/ui_blue.scss +++ b/app/assets/stylesheets/themes/ui_blue.scss @@ -1,5 +1,5 @@ /** - * Modern GitLab UI theme + * Blue GitLab UI theme */ .ui_blue { @include dark-theme(#BECDE9, #2980b9, #1970a9, #096099); -- GitLab From f039e8e03fb341e0cdca1c2597438549c84e15f1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 23 Mar 2015 21:20:48 +0100 Subject: [PATCH 1517/1609] Don't use required keyword arguments to maintain support for Ruby 2.0. --- app/mailers/emails/projects.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 48458baa674..3cd812825e2 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -16,13 +16,17 @@ module Emails subject: subject("Project was moved")) end - def repository_push_email(project_id, recipient, author_id:, - ref:, - action:, + def repository_push_email(project_id, recipient, author_id: nil, + ref: nil, + action: nil, compare: nil, reverse_compare: false, send_from_committer_email: false, disable_diffs: false) + unless author_id && ref && action + raise ArgumentError, "missing keywords: author_id, ref, action" + end + @project = Project.find(project_id) @author = User.find(author_id) @reverse_compare = reverse_compare -- GitLab From 8a7b4eeb0e547251e97b7fd466de009dcbd7fe37 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 23 Mar 2015 21:21:23 +0100 Subject: [PATCH 1518/1609] Revert "Update gemnasium-gitlab-service gem" This reverts commit af522ede14cad4605bc7f0137ddf6950974eccce. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4f1cab43dd5..513e2c643e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -188,7 +188,7 @@ GEM dotenv (>= 0.7) thor (>= 0.13.6) formatador (0.2.4) - gemnasium-gitlab-service (0.2.5) + gemnasium-gitlab-service (0.2.4) rugged (~> 0.21) gemojione (2.0.0) json -- GitLab From 4349e524f47e9081972057584d01579fd6588c1b Mon Sep 17 00:00:00 2001 From: Andrew Tomaka Date: Mon, 23 Mar 2015 17:22:37 -0400 Subject: [PATCH 1519/1609] Update broadcast messages to use color_field --- app/views/admin/broadcast_messages/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index c0afaf16d8f..1ecc0c9916c 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -21,12 +21,12 @@ .form-group.js-toggle-colors-container.hide = f.label :color, "Background Color", class: 'control-label' .col-sm-10 - = f.text_field :color, placeholder: "#AA33EE", class: "form-control" + = f.color_field :color, placeholder: "#AA33EE", class: "form-control" .light 6 character hex values starting with a # sign. .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 - = f.text_field :font, placeholder: "#224466", class: "form-control" + = f.color_field :font, placeholder: "#224466", class: "form-control" .light 6 character hex values starting with a # sign. .form-group = f.label :starts_at, class: 'control-label' -- GitLab From 49c3cc85c4e45cad6afef74b6d220b270290c514 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Mon, 23 Mar 2015 22:06:19 -0600 Subject: [PATCH 1520/1609] Remove the email patches link for merge commits Rugged's `to_mbox` method doesn't work for merge commits, so don't display the option to download merge commits as email patches. --- CHANGELOG | 1 + app/views/projects/commit/_commit_box.html.haml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4c644960088..c7c1ae36b2b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ v 7.10.0 (unreleased) - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) - Add a service to support external wikis (Hannes Rosenögger) + - Omit the "email patches" link and fix plain diff view for merge commits - List new commits for newly pushed branch in activity view. - Add changelog, license and contribution guide links to project sidebar. - Improve diff UI diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 7409f702c5d..6b1b9782f02 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -10,7 +10,8 @@ Download as %span.caret %ul.dropdown-menu - %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) + - unless @commit.parents.length > 1 + %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-primary btn-grouped" do %span Browse Code » -- GitLab From eecd897170c7f6428ba500610f368a1f3bdcbb8f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 24 Mar 2015 00:07:17 -0700 Subject: [PATCH 1521/1609] Fix code unfold not working on Compare commits page Closes #1274 --- CHANGELOG | 1 + app/assets/javascripts/dispatcher.js.coffee | 4 ++++ features/project/commits/commits.feature | 3 +++ features/steps/project/commits/commits.rb | 12 ++++++++++++ 4 files changed, 20 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 3ecc45cde07..b87f116af5d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Fix code unfold not working on Compare commits page (Stan Hu) - Include missing events and fix save functionality in admin service template settings form (Stan Hu) - Fix "Import projects from" button to show the correct instructions (Stan Hu) - Fix dots in Wiki slugs causing errors (Stan Hu) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index deabaf8a784..3535d8c2cfc 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -28,6 +28,8 @@ class Dispatcher new Milestone() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() + when 'projects:compare:show' + new Diff() when 'projects:issues:new','projects:issues:edit' GitLab.GfmAutoComplete.setup() shortcut_handler = new ShortcutsNavigation() @@ -115,6 +117,8 @@ class Dispatcher new Project() new ProjectAvatar() switch path[1] + when 'compare' + shortcut_handler = new ShortcutsNavigation() when 'edit' shortcut_handler = new ShortcutsNavigation() new ProjectNew() diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index 46076b6f3e6..c4b206edc95 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -21,10 +21,13 @@ Feature: Project Commits And I click side-by-side diff button Then I see inline diff button + @javascript Scenario: I compare refs Given I visit compare refs page And I fill compare fields with refs Then I see compared refs + And I unfold diff + Then I should see additional file lines Scenario: I browse commits for a specific path Given I visit my project's commits page for a specific path diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index b2dccf868b0..57b727f837e 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -38,6 +38,18 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps click_button "Compare" end + step 'I unfold diff' do + @diff = first('.js-unfold') + @diff.click + sleep 2 + end + + step 'I should see additional file lines' do + within @diff.parent do + first('.new_line').text.should_not have_content "..." + end + end + step 'I see compared refs' do page.should have_content "Compare View" page.should have_content "Commits (1)" -- GitLab From 56d87db32cffc4c1e7be410da08c3b3e4bd1dcc0 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 15 Mar 2015 19:07:23 -0700 Subject: [PATCH 1522/1609] Reduce Rack Attack false positives by clearing out auth failure count upon successful Git over HTTP authentication. Add logging when a ban goes into effect for debugging. Issue #1171 --- CHANGELOG | 1 + config/gitlab.yml.example | 3 ++ config/initializers/1_settings.rb | 1 + lib/gitlab/backend/grack_auth.rb | 45 +++++++++++----- lib/gitlab/backend/rack_attack_helpers.rb | 31 +++++++++++ spec/lib/gitlab/backend/grack_auth_spec.rb | 52 ++++++++++++++++++- .../backend/rack_attack_helpers_spec.rb | 35 +++++++++++++ 7 files changed, 153 insertions(+), 15 deletions(-) create mode 100644 lib/gitlab/backend/rack_attack_helpers.rb create mode 100644 spec/lib/gitlab/backend/rack_attack_helpers_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 3ecc45cde07..5ee64e6f540 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 7.10.0 (unreleased) - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu) + - Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu) - enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger) - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) - Fix a link in the patch update guide diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index a85db10e019..c4a0fefb7ab 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -285,6 +285,9 @@ production: &base rack_attack: git_basic_auth: + # Rack Attack IP banning enabled + # enabled: true + # # Whitelist requests from 127.0.0.1 for web proxies (NGINX/Apache) with incorrect headers # ip_whitelist: ["127.0.0.1"] # diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 70af7a829c4..15c1ae9466f 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -183,6 +183,7 @@ Settings['extra'] ||= Settingslogic.new({}) # Settings['rack_attack'] ||= Settingslogic.new({}) Settings.rack_attack['git_basic_auth'] ||= Settingslogic.new({}) +Settings.rack_attack.git_basic_auth['enabled'] = true if Settings.rack_attack.git_basic_auth['enabled'].nil? Settings.rack_attack.git_basic_auth['ip_whitelist'] ||= %w{127.0.0.1} Settings.rack_attack.git_basic_auth['maxretry'] ||= 10 Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index ee877e099b1..ffe4565ef1e 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -1,3 +1,4 @@ +require_relative 'rack_attack_helpers' require_relative 'shell_env' module Grack @@ -85,25 +86,41 @@ module Grack user = oauth_access_token_check(login, password) end - return user if user.present? - - # At this point, we know the credentials were wrong. We let Rack::Attack - # know there was a failed authentication attempt from this IP. This - # information is stored in the Rails cache (Redis) and will be used by - # the Rack::Attack middleware to decide whether to block requests from - # this IP. + # If the user authenticated successfully, we reset the auth failure count + # from Rack::Attack for that IP. A client may attempt to authenticate + # with a username and blank password first, and only after it receives + # a 401 error does it present a password. Resetting the count prevents + # false positives from occurring. + # + # Otherwise, we let Rack::Attack know there was a failed authentication + # attempt from this IP. This information is stored in the Rails cache + # (Redis) and will be used by the Rack::Attack middleware to decide + # whether to block requests from this IP. config = Gitlab.config.rack_attack.git_basic_auth - Rack::Attack::Allow2Ban.filter(@request.ip, config) do - # Unless the IP is whitelisted, return true so that Allow2Ban - # increments the counter (stored in Rails.cache) for the IP - if config.ip_whitelist.include?(@request.ip) - false + + if config.enabled + if user + # A successful login will reset the auth failure count from this IP + Rack::Attack::Allow2Ban.reset(@request.ip, config) else - true + banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do + # Unless the IP is whitelisted, return true so that Allow2Ban + # increments the counter (stored in Rails.cache) for the IP + if config.ip_whitelist.include?(@request.ip) + false + else + true + end + end + + if banned + Rails.logger.info "IP #{@request.ip} failed to login " \ + "as #{login} but has been temporarily banned from Git auth" + end end end - nil # No user was found + user end def authorized_request? diff --git a/lib/gitlab/backend/rack_attack_helpers.rb b/lib/gitlab/backend/rack_attack_helpers.rb new file mode 100644 index 00000000000..8538f3f6eca --- /dev/null +++ b/lib/gitlab/backend/rack_attack_helpers.rb @@ -0,0 +1,31 @@ +# rack-attack v4.2.0 doesn't yet support clearing of keys. +# Taken from https://github.com/kickstarter/rack-attack/issues/113 +class Rack::Attack::Allow2Ban + def self.reset(discriminator, options) + findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option" + + cache.reset_count("#{key_prefix}:count:#{discriminator}", findtime) + cache.delete("#{key_prefix}:ban:#{discriminator}") + end +end + +class Rack::Attack::Cache + def reset_count(unprefixed_key, period) + epoch_time = Time.now.to_i + # Add 1 to expires_in to avoid timing error: http://git.io/i1PHXA + expires_in = period - (epoch_time % period) + 1 + key = "#{(epoch_time / period).to_i}:#{unprefixed_key}" + delete(key) + end + + def delete(unprefixed_key) + store.delete("#{prefix}:#{unprefixed_key}") + end +end + +class Rack::Attack::StoreProxy::RedisStoreProxy + def delete(key, options={}) + self.del(key) + rescue Redis::BaseError + end +end diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb index 768312f0028..d0aad54f677 100644 --- a/spec/lib/gitlab/backend/grack_auth_spec.rb +++ b/spec/lib/gitlab/backend/grack_auth_spec.rb @@ -6,7 +6,7 @@ describe Grack::Auth do let(:app) { lambda { |env| [200, {}, "Success!"] } } let!(:auth) { Grack::Auth.new(app) } - let(:env) { + let(:env) { { "rack.input" => "", "REQUEST_METHOD" => "GET", @@ -85,6 +85,17 @@ describe Grack::Auth do it "responds with status 401" do expect(status).to eq(401) end + + context "when the user is IP banned" do + before do + expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) + allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') + end + + it "responds with status 401" do + expect(status).to eq(401) + end + end end context "when authentication succeeds" do @@ -109,10 +120,49 @@ describe Grack::Auth do end context "when the user isn't blocked" do + before do + expect(Rack::Attack::Allow2Ban).to receive(:reset) + end + it "responds with status 200" do expect(status).to eq(200) end end + + context "when blank password attempts follow a valid login" do + let(:options) { Gitlab.config.rack_attack.git_basic_auth } + let(:maxretry) { options[:maxretry] - 1 } + let(:ip) { '1.2.3.4' } + + before do + allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) + Rack::Attack::Allow2Ban.reset(ip, options) + end + + after do + Rack::Attack::Allow2Ban.reset(ip, options) + end + + def attempt_login(include_password) + password = include_password ? user.password : "" + env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password) + Grack::Auth.new(app) + auth.call(env).first + end + + it "repeated attempts followed by successful attempt" do + for n in 0..maxretry do + expect(attempt_login(false)).to eq(401) + end + + expect(attempt_login(true)).to eq(200) + expect(Rack::Attack::Allow2Ban.send(:banned?, ip)).to eq(nil) + + for n in 0..maxretry do + expect(attempt_login(false)).to eq(401) + end + end + end end context "when the user doesn't have access to the project" do diff --git a/spec/lib/gitlab/backend/rack_attack_helpers_spec.rb b/spec/lib/gitlab/backend/rack_attack_helpers_spec.rb new file mode 100644 index 00000000000..2ac496fd669 --- /dev/null +++ b/spec/lib/gitlab/backend/rack_attack_helpers_spec.rb @@ -0,0 +1,35 @@ +require "spec_helper" + +describe 'RackAttackHelpers' do + describe 'reset' do + let(:discriminator) { 'test-key'} + let(:maxretry) { 5 } + let(:period) { 1.minute } + let(:options) { { findtime: period, bantime: 60, maxretry: maxretry } } + + def do_filter + for i in 1..maxretry - 1 do + status = Rack::Attack::Allow2Ban.filter(discriminator, options) { true } + expect(status).to eq(false) + end + end + + def do_reset + Rack::Attack::Allow2Ban.reset(discriminator, options) + end + + before do + do_reset + end + + after do + do_reset + end + + it 'user is not banned after n - 1 retries' do + do_filter + do_reset + do_filter + end + end +end -- GitLab From b45c9115e4d3f1db323869574dcc2671c1af9019 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Mar 2015 10:50:58 +0100 Subject: [PATCH 1523/1609] Update rugments to 1.0.0.beta6 to fix C# highlighting. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4f1cab43dd5..58a8dfa3706 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -516,7 +516,7 @@ GEM rubyntlm (0.5.0) rubypants (0.2.0) rugged (0.21.4) - rugments (1.0.0.beta5) + rugments (1.0.0.beta6) safe_yaml (0.9.7) sanitize (2.1.0) nokogiri (>= 1.4.4) -- GitLab From 862e1e6f178d11473319225de51c25b72174f45b Mon Sep 17 00:00:00 2001 From: Dan Tudor Date: Tue, 24 Mar 2015 13:04:22 +0000 Subject: [PATCH 1524/1609] Unescape branch param to delete Branch names that contain `/` return a 405 error when being deleted because the slash is escaped to `%2F` This patch will unescape the param prior to executing the delete action. --- lib/api/branches.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/api/branches.rb b/lib/api/branches.rb index b52d786e020..edfdf842f85 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -1,4 +1,5 @@ require 'mime/types' +require 'uri' module API # Projects API @@ -103,7 +104,7 @@ module API delete ":id/repository/branches/:branch" do authorize_push_project result = DeleteBranchService.new(user_project, current_user). - execute(params[:branch]) + execute(URI.unescape(params[:branch])) if result[:status] == :success { -- GitLab From 4830b2be5e076126f89d2c67bab94302559aa93a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Mar 2015 14:10:55 +0100 Subject: [PATCH 1525/1609] Refactor GitAccess to use instance variables. --- .../projects/merge_requests_controller.rb | 2 +- app/helpers/branches_helper.rb | 2 +- app/helpers/tree_helper.rb | 2 +- app/services/files/create_service.rb | 2 +- app/services/files/delete_service.rb | 2 +- app/services/files/update_service.rb | 2 +- lib/api/internal.rb | 36 +++-- lib/api/merge_requests.rb | 3 +- lib/gitlab/backend/grack_auth.rb | 2 +- lib/gitlab/git_access.rb | 128 ++++++++++++------ lib/gitlab/git_access_wiki.rb | 2 +- spec/lib/gitlab/git_access_spec.rb | 38 +++--- spec/lib/gitlab/git_access_wiki_spec.rb | 4 +- 13 files changed, 132 insertions(+), 93 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e9b7d7e0083..47ce8467358 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -257,7 +257,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def allowed_to_push_code?(project, branch) - ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, branch) + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch) end def merge_request_params diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 4a5edf6d101..d6eaa7d57bc 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -12,6 +12,6 @@ module BranchesHelper def can_push_branch?(project, branch_name) return false unless project.repository.branch_names.include?(branch_name) - ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, branch_name) + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index b6fb7a8aa5a..bf6726574ec 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -56,7 +56,7 @@ module TreeHelper ref ||= @ref return false unless project.repository.branch_names.include?(ref) - ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, ref) + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) end def tree_breadcrumbs(tree, max_links = 2) diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index de5322e990a..eeafefc25af 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -3,7 +3,7 @@ require_relative "base_service" module Files class CreateService < BaseService def execute - allowed = Gitlab::GitAccess.can_push_to_branch?(current_user, project, ref) + allowed = Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) unless allowed return error("You are not allowed to create file in this branch") diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index 8e73c2e2727..1497a0f883b 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -3,7 +3,7 @@ require_relative "base_service" module Files class DeleteService < BaseService def execute - allowed = ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, ref) + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) unless allowed return error("You are not allowed to push into this branch") diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 328cf3a4b06..0724d3ae634 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -3,7 +3,7 @@ require_relative "base_service" module Files class UpdateService < BaseService def execute - allowed = ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, ref) + allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) unless allowed return error("You are not allowed to push into this branch") diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 753d0fcbd98..30bade74cf6 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -17,39 +17,37 @@ module API post "/allowed" do status 200 - actor = if params[:key_id] - Key.find_by(id: params[:key_id]) - elsif params[:user_id] - User.find_by(id: params[:user_id]) - end + actor = + if params[:key_id] + Key.find_by(id: params[:key_id]) + elsif params[:user_id] + User.find_by(id: params[:user_id]) + end unless actor return Gitlab::GitAccessStatus.new(false, 'No such user or key') end project_path = params[:project] - + # 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. - access = - if project_path.end_with?('.wiki') - project_path.chomp!('.wiki') - Gitlab::GitAccessWiki.new - else - Gitlab::GitAccess.new - end + wiki = project_path.end_with?('.wiki') + project_path.chomp!('.wiki') if wiki project = Project.find_with_namespace(project_path) if project - status = access.check( - actor, - params[:action], - project, - params[:changes] - ) + access = + if wiki + Gitlab::GitAccessWiki.new(actor, project) + else + Gitlab::GitAccess.new(actor, project) + end + + status = access.check(params[:action], params[:changes]) end if project && status && status.allowed? diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 25b7857f4b1..f3765f5ab03 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -178,7 +178,8 @@ module API put ":id/merge_request/:merge_request_id/merge" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) - allowed = ::Gitlab::GitAccess.can_push_to_branch?(current_user, user_project, merge_request.target_branch) + allowed = ::Gitlab::GitAccess.new(current_user, user_project). + can_push_to_branch?(merge_request.target_branch) if allowed if merge_request.unchecked? diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index ee877e099b1..b051ae4c842 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -112,7 +112,7 @@ module Grack case git_cmd when *Gitlab::GitAccess::DOWNLOAD_COMMANDS if user - Gitlab::GitAccess.new.download_access_check(user, project).allowed? + Gitlab::GitAccess.new(user, project).download_access_check.allowed? elsif project.public? # Allow clone/fetch for public projects true diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index cb69e4b13d3..d6e609e2c44 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -3,9 +3,32 @@ module Gitlab DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive } PUSH_COMMANDS = %w{ git-receive-pack } - attr_reader :params, :project, :git_cmd, :user + attr_reader :actor, :project - def self.can_push_to_branch?(user, project, ref) + def initialize(actor, project) + @actor = actor + @project = project + end + + def user + return @user if defined?(@user) + + @user = + case actor + when User + actor + when DeployKey + nil + when Key + actor.user + end + end + + def deploy_key + actor if actor.is_a?(DeployKey) + end + + def can_push_to_branch?(ref) return false unless user if project.protected_branch?(ref) && @@ -16,51 +39,65 @@ module Gitlab end end - def check(actor, cmd, project, changes = nil) + def can_read_project? + if user + user.can?(:read_project, project) + elsif deploy_key + deploy_key.projects.include?(project) + else + false + end + end + + def check(cmd, changes = nil) case cmd when *DOWNLOAD_COMMANDS - download_access_check(actor, project) + download_access_check when *PUSH_COMMANDS - if actor.is_a? User - push_access_check(actor, project, changes) - elsif actor.is_a? DeployKey - return build_status_object(false, "Deploy key not allowed to push") - elsif actor.is_a? Key - push_access_check(actor.user, project, changes) - else - raise 'Wrong actor' - end + push_access_check(changes) else - return build_status_object(false, "Wrong command") + build_status_object(false, "Wrong command") end end - def download_access_check(actor, project) - if actor.is_a?(User) - user_download_access_check(actor, project) - elsif actor.is_a?(DeployKey) - if actor.projects.include?(project) - build_status_object(true) - else - build_status_object(false, "Deploy key not allowed to access this project") - end - elsif actor.is_a? Key - user_download_access_check(actor.user, project) + def download_access_check + if user + user_download_access_check + elsif deploy_key + deploy_key_download_access_check else raise 'Wrong actor' end end - def user_download_access_check(user, project) - if user && user_allowed?(user) && user.can?(:download_code, project) + def push_access_check(changes) + if user + user_push_access_check(changes) + elsif deploy_key + build_status_object(false, "Deploy key not allowed to push") + else + raise 'Wrong actor' + end + end + + def user_download_access_check + if user && user_allowed? && user.can?(:download_code, project) build_status_object(true) else build_status_object(false, "You don't have access") end end - def push_access_check(user, project, changes) - unless user && user_allowed?(user) + def deploy_key_download_access_check + if can_read_project? + build_status_object(true) + else + build_status_object(false, "Deploy key not allowed to access this project") + end + end + + def user_push_access_check(changes) + unless user && user_allowed? return build_status_object(false, "You don't have access") end @@ -76,27 +113,28 @@ module Gitlab # Iterate over all changes to find if user allowed all of them to be applied changes.map(&:strip).reject(&:blank?).each do |change| - status = change_access_check(user, project, change) + status = change_access_check(change) unless status.allowed? # If user does not have access to make at least one change - cancel all push return status end end - return build_status_object(true) + build_status_object(true) end - def change_access_check(user, project, change) + def change_access_check(change) oldrev, newrev, ref = change.split(' ') - action = if project.protected_branch?(branch_name(ref)) - protected_branch_action(project, oldrev, newrev, branch_name(ref)) - elsif protected_tag?(project, tag_name(ref)) - # Prevent any changes to existing git tag unless user has permissions - :admin_project - else - :push_code - end + action = + if project.protected_branch?(branch_name(ref)) + protected_branch_action(oldrev, newrev, branch_name(ref)) + elsif protected_tag?(tag_name(ref)) + # Prevent any changes to existing git tag unless user has permissions + :admin_project + else + :push_code + end if user.can?(action, project) build_status_object(true) @@ -105,15 +143,15 @@ module Gitlab end end - def forced_push?(project, oldrev, newrev) + def forced_push?(oldrev, newrev) Gitlab::ForcePushCheck.force_push?(project, oldrev, newrev) end private - def protected_branch_action(project, oldrev, newrev, branch_name) + def protected_branch_action(oldrev, newrev, branch_name) # we dont allow force push to protected branch - if forced_push?(project, oldrev, newrev) + if forced_push?(oldrev, newrev) :force_push_code_to_protected_branches elsif Gitlab::Git.blank_ref?(newrev) # and we dont allow remove of protected branch @@ -125,11 +163,11 @@ module Gitlab end end - def protected_tag?(project, tag_name) + def protected_tag?(tag_name) project.repository.tag_names.include?(tag_name) end - def user_allowed?(user) + def user_allowed? Gitlab::UserAccess.allowed?(user) end diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb index a2177c8d548..73d99b96202 100644 --- a/lib/gitlab/git_access_wiki.rb +++ b/lib/gitlab/git_access_wiki.rb @@ -1,6 +1,6 @@ module Gitlab class GitAccessWiki < GitAccess - def change_access_check(user, project, change) + def change_access_check(change) if user.can?(:write_wiki, project) build_status_object(true) else diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 666398eedd4..39be9d64644 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -1,25 +1,26 @@ require 'spec_helper' describe Gitlab::GitAccess do - let(:access) { Gitlab::GitAccess.new } + let(:access) { Gitlab::GitAccess.new(actor, project) } let(:project) { create(:project) } let(:user) { create(:user) } + let(:actor) { user } describe 'can_push_to_branch?' do describe 'push to none protected branch' do it "returns true if user is a master" do project.team << [user, :master] - expect(Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch")).to be_truthy + expect(access.can_push_to_branch?("random_branch")).to be_truthy end it "returns true if user is a developer" do project.team << [user, :developer] - expect(Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch")).to be_truthy + expect(access.can_push_to_branch?("random_branch")).to be_truthy end it "returns false if user is a reporter" do project.team << [user, :reporter] - expect(Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch")).to be_falsey + expect(access.can_push_to_branch?("random_branch")).to be_falsey end end @@ -30,17 +31,17 @@ describe Gitlab::GitAccess do it "returns true if user is a master" do project.team << [user, :master] - expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_truthy + expect(access.can_push_to_branch?(@branch.name)).to be_truthy end it "returns false if user is a developer" do project.team << [user, :developer] - expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_falsey + expect(access.can_push_to_branch?(@branch.name)).to be_falsey end it "returns false if user is a reporter" do project.team << [user, :reporter] - expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_falsey + expect(access.can_push_to_branch?(@branch.name)).to be_falsey end end @@ -51,17 +52,17 @@ describe Gitlab::GitAccess do it "returns true if user is a master" do project.team << [user, :master] - expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_truthy + expect(access.can_push_to_branch?(@branch.name)).to be_truthy end it "returns true if user is a developer" do project.team << [user, :developer] - expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_truthy + expect(access.can_push_to_branch?(@branch.name)).to be_truthy end it "returns false if user is a reporter" do project.team << [user, :reporter] - expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_falsey + expect(access.can_push_to_branch?(@branch.name)).to be_falsey end end @@ -72,7 +73,7 @@ describe Gitlab::GitAccess do before { project.team << [user, :master] } context 'pull code' do - subject { access.download_access_check(user, project) } + subject { access.download_access_check } it { expect(subject.allowed?).to be_truthy } end @@ -82,7 +83,7 @@ describe Gitlab::GitAccess do before { project.team << [user, :guest] } context 'pull code' do - subject { access.download_access_check(user, project) } + subject { access.download_access_check } it { expect(subject.allowed?).to be_falsey } end @@ -95,7 +96,7 @@ describe Gitlab::GitAccess do end context 'pull code' do - subject { access.download_access_check(user, project) } + subject { access.download_access_check } it { expect(subject.allowed?).to be_falsey } end @@ -103,7 +104,7 @@ describe Gitlab::GitAccess do describe 'without acccess to project' do context 'pull code' do - subject { access.download_access_check(user, project) } + subject { access.download_access_check } it { expect(subject.allowed?).to be_falsey } end @@ -111,17 +112,18 @@ describe Gitlab::GitAccess do describe 'deploy key permissions' do let(:key) { create(:deploy_key) } + let(:actor) { key } context 'pull code' do context 'allowed' do before { key.projects << project } - subject { access.download_access_check(key, project) } + subject { access.download_access_check } it { expect(subject.allowed?).to be_truthy } end context 'denied' do - subject { access.download_access_check(key, project) } + subject { access.download_access_check } it { expect(subject.allowed?).to be_falsey } end @@ -205,7 +207,7 @@ describe Gitlab::GitAccess do permissions_matrix[role].each do |action, allowed| context action do - subject { access.push_access_check(user, project, changes[action]) } + subject { access.push_access_check(changes[action]) } it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } end @@ -221,7 +223,7 @@ describe Gitlab::GitAccess do updated_permissions_matrix[role].each do |action, allowed| context action do - subject { access.push_access_check(user, project, changes[action]) } + subject { access.push_access_check(changes[action]) } it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey } end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index c31c6764091..4cb91094cb3 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::GitAccessWiki do - let(:access) { Gitlab::GitAccessWiki.new } + let(:access) { Gitlab::GitAccessWiki.new(user, project) } let(:project) { create(:project) } let(:user) { create(:user) } @@ -11,7 +11,7 @@ describe Gitlab::GitAccessWiki do project.team << [user, :developer] end - subject { access.push_access_check(user, project, changes) } + subject { access.push_access_check(changes) } it { expect(subject.allowed?).to be_truthy } end -- GitLab From 4745424bd3b7f6e13e86ebf985977ad3268881e3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Mar 2015 14:12:03 +0100 Subject: [PATCH 1526/1609] Respond with full GitAccess error if user has project read access. --- CHANGELOG | 1 + lib/api/internal.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 25936eb1e1d..4dfa831b4ec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ v 7.10.0 (unreleased) - Replace commits calendar with faster contribution calendar that includes issues and merge requests - Add inifinite scroll to user page activity - Don't show commit comment button when user is not signed in. + - Make Git-over-SSH errors more descriptive. v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 30bade74cf6..f98a17773e7 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -50,7 +50,7 @@ module API status = access.check(params[:action], params[:changes]) end - if project && status && status.allowed? + if project && access.can_read_project? status else Gitlab::GitAccessStatus.new(false, 'No such project') -- GitLab From e3fb9b1609497e4152e6c14709cb295824372bc9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Mar 2015 15:44:17 +0100 Subject: [PATCH 1527/1609] Clean up subscriptions when user is deleted. --- app/models/user.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/user.rb b/app/models/user.rb index 50f664a09a3..979150b4d68 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -110,6 +110,7 @@ class User < ActiveRecord::Base has_many :notes, dependent: :destroy, foreign_key: :author_id has_many :merge_requests, dependent: :destroy, foreign_key: :author_id has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event" + has_many :subscriptions, dependent: :destroy has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event" has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" -- GitLab From 515e9d51df5a890887248e74b7a96d47a5d0722c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Mar 2015 16:52:02 +0100 Subject: [PATCH 1528/1609] Make sure issue assignee is properly reset. --- .../javascripts/project_users_select.js.coffee | 7 ++----- app/helpers/issues_helper.rb | 13 +------------ app/services/issues/bulk_update_service.rb | 6 +++--- app/services/issues/update_service.rb | 3 +++ app/services/merge_requests/update_service.rb | 3 +++ 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee index e22c7c11f1c..80ab1a61ab9 100644 --- a/app/assets/javascripts/project_users_select.js.coffee +++ b/app/assets/javascripts/project_users_select.js.coffee @@ -25,7 +25,7 @@ class @ProjectUsersSelect initSelection: (element, callback) -> id = $(element).val() - if id isnt "" + if id != "" && id != "-1" Api.user(id, callback) @@ -44,10 +44,7 @@ class @ProjectUsersSelect else avatar = gon.default_avatar_url - if user.id == '' - avatarMarkup = '' - else - avatarMarkup = "
        " + avatarMarkup = "
        " "
        #{avatarMarkup} diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 15c5dcb6a25..a4bd4d30215 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -58,22 +58,11 @@ module IssuesHelper end def bulk_update_milestone_options - options_for_select(['None (backlog)']) + + options_for_select([['None (backlog)', -1]]) + options_from_collection_for_select(project_active_milestones, 'id', 'title', params[:milestone_id]) end - def bulk_update_assignee_options(project = @project) - options_for_select(['None (unassigned)']) + - options_from_collection_for_select(project.team.members, 'id', - 'name', params[:assignee_id]) - end - - def assignee_options(object, project = @project) - options_from_collection_for_select(project.team.members.sort_by(&:name), - 'id', 'name', object.assignee_id) - end - def milestone_options(object) options_from_collection_for_select(object.project.milestones.active, 'id', 'title', object.milestone_id) diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb index c7cd20b6b60..eb07413ee94 100644 --- a/app/services/issues/bulk_update_service.rb +++ b/app/services/issues/bulk_update_service.rb @@ -4,9 +4,9 @@ module Issues issues_ids = params.delete(:issues_ids).split(",") issue_params = params - issue_params.delete(:state_event) unless issue_params[:state_event].present? - issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present? - issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present? + issue_params.delete(:state_event) unless issue_params[:state_event].present? + issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present? + issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present? issues = Issue.where(id: issues_ids) issues.each do |issue| diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index c61d67a7893..3371fe7d5ef 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -14,6 +14,9 @@ module Issues issue.update_nth_task(params[:task_num].to_i, false) end + params[:assignee_id] = "" if params[:assignee_id] == "-1" + params[:milestone_id] = "" if params[:milestone_id] == "-1" + old_labels = issue.labels.to_a if params.present? && issue.update_attributes(params.except(:state_event, diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 870b50bb60d..0ac6dfea6fd 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -23,6 +23,9 @@ module MergeRequests merge_request.update_nth_task(params[:task_num].to_i, false) end + params[:assignee_id] = "" if params[:assignee_id] == "-1" + params[:milestone_id] = "" if params[:milestone_id] == "-1" + old_labels = merge_request.labels.to_a if params.present? && merge_request.update_attributes( -- GitLab From fda55f9145b5280db28cd6723789f95e2cb7b6c0 Mon Sep 17 00:00:00 2001 From: "RICKETTM@uk.ibm.com" Date: Tue, 24 Mar 2015 15:52:26 +0000 Subject: [PATCH 1529/1609] Change ordering so that confirm is removed from attrs before attempting to User.build_user --- lib/api/users.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/api/users.rb b/lib/api/users.rb index 7c8b3250cd0..032a5d76e43 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -61,10 +61,10 @@ module API authenticated_as_admin! required_attributes! [:email, :password, :name, :username] attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm] - user = User.build_user(attrs) admin = attrs.delete(:admin) - user.admin = admin unless admin.nil? confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i)) + user = User.build_user(attrs) + user.admin = admin unless admin.nil? user.skip_confirmation! unless confirm identity_attrs = attributes_for_keys [:provider, :extern_uid] -- GitLab From f3650d2e5da4dc3624bcf4940825fe24266a91b4 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Mar 2015 17:00:56 +0100 Subject: [PATCH 1530/1609] Add migration. --- .../20150324155957_set_incorrect_assignee_id_to_null.rb | 6 ++++++ db/schema.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb diff --git a/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb new file mode 100644 index 00000000000..42dc8173e46 --- /dev/null +++ b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb @@ -0,0 +1,6 @@ +class SetIncorrectAssigneeIdToNull < ActiveRecord::Migration + def up + execute "UPDATE issues SET assignee_id = NULL WHERE assignee_id = -1" + execute "UPDATE merge_requests SET assignee_id = NULL WHERE assignee_id = -1" + end +end diff --git a/db/schema.rb b/db/schema.rb index e1a5b70532a..4a445ae5832 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150320234437) do +ActiveRecord::Schema.define(version: 20150324155957) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" -- GitLab From 83b5a9ae2bfb594bcb5a9a931d6781ba05c9b9ef Mon Sep 17 00:00:00 2001 From: Andrew Tomaka Date: Tue, 24 Mar 2015 12:27:26 -0400 Subject: [PATCH 1531/1609] Update help texts and default value setting --- app/views/admin/broadcast_messages/index.html.haml | 6 ++---- app/views/projects/labels/_form.html.haml | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml index 1ecc0c9916c..7e29311bf42 100644 --- a/app/views/admin/broadcast_messages/index.html.haml +++ b/app/views/admin/broadcast_messages/index.html.haml @@ -21,13 +21,11 @@ .form-group.js-toggle-colors-container.hide = f.label :color, "Background Color", class: 'control-label' .col-sm-10 - = f.color_field :color, placeholder: "#AA33EE", class: "form-control" - .light 6 character hex values starting with a # sign. + = f.color_field :color, value: "#AA33EE", class: "form-control" .form-group.js-toggle-colors-container.hide = f.label :font, "Font Color", class: 'control-label' .col-sm-10 - = f.color_field :font, placeholder: "#224466", class: "form-control" - .light 6 character hex values starting with a # sign. + = f.color_field :font, value: "#224466", class: "form-control" .form-group = f.label :starts_at, class: 'control-label' .col-sm-10.datetime-controls diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index 2305fce112e..ad993db6c0b 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -16,9 +16,9 @@ .col-sm-10 .input-group .input-group-addon.label-color-preview   - = f.color_field :color, placeholder: "#AA33EE", class: "form-control" + = f.color_field :color, value: "#AA33EE", class: "form-control" .help-block - 6 character hex values starting with a # sign. + Choose any color. %br Or you can choose one of suggested colors below -- GitLab From 1651c0bc448b4e80462f73f36a778e736df88b2b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 24 Mar 2015 18:16:40 +0100 Subject: [PATCH 1532/1609] Link note avatar to user. --- CHANGELOG | 1 + app/views/projects/notes/_discussion.html.haml | 3 ++- app/views/projects/notes/_note.html.haml | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 25936eb1e1d..b61eae50698 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ v 7.10.0 (unreleased) - Replace commits calendar with faster contribution calendar that includes issues and merge requests - Add inifinite scroll to user page activity - Don't show commit comment button when user is not signed in. + - Link note avatar to user. v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml index f4c6fad2fed..3561ca49f81 100644 --- a/app/views/projects/notes/_discussion.html.haml +++ b/app/views/projects/notes/_discussion.html.haml @@ -2,7 +2,8 @@ .timeline-entry .timeline-entry-inner .timeline-icon - = image_tag avatar_icon(note.author_email), class: "avatar s40" + = link_to user_path(note.author) do + = image_tag avatar_icon(note.author_email), class: "avatar s40" .timeline-content - if note.for_merge_request? - if note.outdated? diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index f3d00a6f06d..71bdf5c8f2a 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -4,7 +4,8 @@ - if note.system %span.fa.fa-circle - else - = image_tag avatar_icon(note.author_email), class: "avatar s40" + = link_to user_path(note.author) do + = image_tag avatar_icon(note.author_email), class: "avatar s40" .timeline-content .note-header .note-actions @@ -21,7 +22,8 @@ %i.fa.fa-trash-o.cred Remove - if note.system - = image_tag avatar_icon(note.author_email), class: "avatar s16" + = link_to user_path(note.author) do + = image_tag avatar_icon(note.author_email), class: "avatar s16" = link_to_member(@project, note.author, avatar: false) %span.author-username = '@' + note.author.username -- GitLab From 3b3662da0a5f31dddbe19be7f8e787c1b90b1b22 Mon Sep 17 00:00:00 2001 From: Stephan van Leeuwen Date: Sun, 21 Dec 2014 13:35:11 +0100 Subject: [PATCH 1533/1609] Updated api method GET /projects/:id/events to use paginate instead of a self-implementation Also updated example request url Added changelog item --- CHANGELOG | 1 + lib/api/projects.rb | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3ecc45cde07..297a8ee3489 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,6 +33,7 @@ v 7.10.0 (unreleased) - Don't show commit comment button when user is not signed in. - Don't include system notes in issue/MR comment count. - Don't mark merge request as updated when merge status relative to target branch changes. + - API: Add pagination to project events v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 83f65eec6cc..e3fff79d68f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -88,17 +88,14 @@ module API present user_project, with: Entities::ProjectWithAccess, user: current_user end - # Get a single project events + # Get events for a single project # # Parameters: # id (required) - The ID of a project # Example Request: # GET /projects/:id/events get ":id/events" do - limit = (params[:per_page] || 20).to_i - offset = (params[:page] || 0).to_i * limit - events = user_project.events.recent.limit(limit).offset(offset) - + events = paginate user_project.events.recent present events, with: Entities::Event end -- GitLab From 7070ccebc7097bd987d716fa46c96f3ca89a2a78 Mon Sep 17 00:00:00 2001 From: Aurelio Jargas Date: Tue, 24 Mar 2015 17:11:22 -0300 Subject: [PATCH 1534/1609] Remove duplicate CHANGELOG items for v7.8.0 [ci skip] --- CHANGELOG | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3ecc45cde07..c8566e92313 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -154,7 +154,6 @@ v 7.8.0 - Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen) - View note image attachments in new tab when clicked instead of downloading them - Improve sorting logic in UI and API. Explicitly define what sorting method is used by default - - Allow more variations for commit messages closing issues (Julien Bianchi and Hannes Rosenögger) - Fix overflow at sidebar when have several items - Add notes for label changes in issue and merge requests - Show tags in commit view (Hannes Rosenögger) @@ -176,7 +175,7 @@ v 7.8.0 - Add a commit calendar to the user profile (Hannes Rosenögger) - Submit comment on command-enter - Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`. - - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" + - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger) - Fix long broadcast message cut-off on left sidebar (Visay Keo) - Add Project Avatars (Steven Thonus and Hannes Rosenögger) - Password reset token validity increased from 2 hours to 2 days since it is also send on account creation. -- GitLab From bbf6019dd87fcf95c584addc253bf8333809f4c8 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 24 Mar 2015 15:18:27 -0700 Subject: [PATCH 1535/1609] Milestones and labels can be used even when issues are disabled. --- app/helpers/projects_helper.rb | 2 +- app/views/projects/milestones/show.html.haml | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 77ad7a40fab..7bf51b5b8e8 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -146,7 +146,7 @@ module ProjectsHelper nav_tabs << feature if project.send :"#{feature}_enabled" end - if project.issues_enabled + if project.issues_enabled || project.merge_requests_enabled nav_tabs << [:milestones, :labels] end diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 110d8967342..25cc0030965 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -60,11 +60,12 @@ Participants %span.badge= @users.count - .pull-right - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do - %i.fa.fa-plus - New Issue - = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped" + - if @project.issues_enabled + .pull-right + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do + %i.fa.fa-plus + New Issue + = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped" .tab-content .tab-pane.active#tab-issues -- GitLab From 35ec08a6e21caedd5c0b7cb36cb89472005304ed Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Mar 2015 16:13:29 -0700 Subject: [PATCH 1536/1609] Change merge request button color based on CI status --- .../javascripts/merge_request.js.coffee | 9 ++++++++ .../stylesheets/pages/merge_requests.scss | 21 +++---------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 09c202e42a5..6127d2bb480 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -113,8 +113,14 @@ class @MergeRequest allowed_states = ["failed", "canceled", "running", "pending", "success"] if state in allowed_states $('.ci_widget.ci-' + state).show() + switch state + when "failed", "canceled" + @setMergeButtonClass('btn-danger') + when "running", "pending" + @setMergeButtonClass('btn-warning') else $('.ci_widget.ci-error').show() + @setMergeButtonClass('btn-danger') showCiCoverage: (coverage) -> cov_html = $('') @@ -144,6 +150,9 @@ class @MergeRequest this.$('.merge-in-progress').hide() this.$('.automerge_widget.already_cannot_be_merged').show() + setMergeButtonClass: (css_class) -> + $('.accept_merge_request').removeClass("btn-create").addClass(css_class) + mergeInProgress: -> $.ajax type: 'GET' diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d8fe339b7b3..8abd4207beb 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -137,30 +137,15 @@ background-color: #F1FAF1; } - &.ci-pending { - color: #548; - border-color: #548; - background-color: #F4F1FA; - } - + &.ci-pending, &.ci-running { color: $gl-warning; border-color: $gl-warning; background-color: #FAF5F1; } - &.ci-failed { - color: $gl-danger; - border-color: $gl-danger; - background-color: #FAF1F1; - } - - &.ci-canceled { - color: $gl-warning; - border-color: $gl-danger; - background-color: #FAF5F1; - } - + &.ci-failed, + &.ci-canceled, &.ci-error { color: $gl-danger; border-color: $gl-danger; -- GitLab From 0d50a65b320ac709e82f8b57d543a76638339d07 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Mar 2015 16:27:40 -0700 Subject: [PATCH 1537/1609] Fix diff header in discussion blocks --- .../projects/notes/discussions/_diff.html.haml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml index f717c77a898..711aa39101b 100644 --- a/app/views/projects/notes/discussions/_diff.html.haml +++ b/app/views/projects/notes/discussions/_diff.html.haml @@ -2,13 +2,13 @@ - if diff .diff-file .diff-header - - if diff.deleted_file - %span= diff.old_path - - else - %span= diff.new_path - - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode - %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" - %br/ + %span + - if diff.deleted_file + = diff.old_path + - else + = diff.new_path + - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode + %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}" .diff-content %table - note.truncated_diff_lines.each do |line| -- GitLab From 6ec8ff069ceaa7bb914cbbd97ac248d926ef7e4e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Mar 2015 18:28:10 -0700 Subject: [PATCH 1538/1609] Enable more rubocop style checks --- .rubocop.yml | 14 +++++++------- app/helpers/gitlab_markdown_helper.rb | 2 +- app/mailers/notify.rb | 2 +- app/models/project_services/asana_service.rb | 2 +- lib/api/helpers.rb | 4 ++-- lib/gitlab/satellite/merge_action.rb | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 53ca2ca2191..7188b0ecefe 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -355,7 +355,7 @@ Style/MultilineBlockChain: Style/MultilineBlockLayout: Description: 'Ensures newlines after multiline block do statements.' - Enabled: false + Enabled: true Style/MultilineIfThen: Description: 'Do not use then for multi-line if/unless.' @@ -390,7 +390,7 @@ Style/NegatedWhile: Style/NestedTernaryOperator: Description: 'Use one expression per branch in a ternary operator.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary' - Enabled: false + Enabled: true Style/Next: Description: 'Use `next` to skip iteration instead of a condition at the end.' @@ -400,17 +400,17 @@ Style/Next: Style/NilComparison: Description: 'Prefer x.nil? to x == nil.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' - Enabled: false + Enabled: true Style/NonNilCheck: Description: 'Checks for redundant nil checks.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks' - Enabled: false + Enabled: true Style/Not: Description: 'Use ! instead of not.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not' - Enabled: false + Enabled: true Style/NumericLiterals: Description: >- @@ -424,7 +424,7 @@ Style/OneLineConditional: Favor the ternary operator(?:) over if/then/else/end constructs. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator' - Enabled: false + Enabled: true Style/OpMethod: Description: 'When defining binary operators, name the argument other.' @@ -436,7 +436,7 @@ Style/ParenthesesAroundCondition: Don't use parentheses around the condition of an if/unless/while. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if' - Enabled: false + Enabled: true Style/PercentLiteralDelimiters: Description: 'Use `%`-literal delimiters consistently' diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index f8e104b0827..985def4ad66 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -29,7 +29,7 @@ module GitlabMarkdownHelper end def markdown(text, options={}) - unless (@markdown and options == @options) + unless @markdown && options == @options @options = options gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index ee27879cf40..8fcdd3bc853 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -148,7 +148,7 @@ class Notify < ActionMailer::Base headers['References'] = message_id(model) headers['X-GitLab-Project'] = "#{@project.name} | " if @project - if (headers[:subject]) + if headers[:subject] headers[:subject].prepend('Re: ') end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index d52214cdd69..e6e16058d41 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -82,7 +82,7 @@ automatically inspected. Leave blank to include all branches.' branch_restriction = restrict_to_branch.to_s # check the branch restriction is poplulated and branch is not included - if branch_restriction.length > 0 && branch_restriction.index(branch) == nil + if branch_restriction.length > 0 && branch_restriction.index(branch).nil? return end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a6e77002a01..be133a2920b 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -20,7 +20,7 @@ module API identifier = sudo_identifier() # If the sudo is the current user do nothing - if (identifier && !(@current_user.id == identifier || @current_user.username == identifier)) + if identifier && !(@current_user.id == identifier || @current_user.username == identifier) render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin? @current_user = User.by_username_or_id(identifier) not_found!("No user id or username for: #{identifier}") if @current_user.nil? @@ -33,7 +33,7 @@ module API identifier ||= params[SUDO_PARAM] ||= env[SUDO_HEADER] # Regex for integers - if (!!(identifier =~ /^[0-9]+$/)) + if !!(identifier =~ /^[0-9]+$/) identifier.to_i else identifier diff --git a/lib/gitlab/satellite/merge_action.rb b/lib/gitlab/satellite/merge_action.rb index 25122666f5e..1f2e5f82dd5 100644 --- a/lib/gitlab/satellite/merge_action.rb +++ b/lib/gitlab/satellite/merge_action.rb @@ -97,7 +97,7 @@ module Gitlab in_locked_and_timed_satellite do |merge_repo| prepare_satellite!(merge_repo) update_satellite_source_and_target!(merge_repo) - if (merge_request.for_fork?) + if merge_request.for_fork? repository = Gitlab::Git::Repository.new(merge_repo.path) commits = Gitlab::Git::Commit.between( repository, -- GitLab From 69454e36f77db6f6e1c45c04c39acf670fe443e4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Mar 2015 18:35:57 -0700 Subject: [PATCH 1539/1609] Style/RedundantReturn enabled --- .rubocop.yml | 2 +- app/helpers/gitlab_markdown_helper.rb | 4 ++-- app/helpers/merge_requests_helper.rb | 2 +- app/helpers/submodule_helper.rb | 7 +++++-- lib/gitlab/git_access.rb | 4 ++-- lib/gitlab/popen.rb | 2 +- lib/gitlab/theme.rb | 2 +- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 7188b0ecefe..7290d627d24 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -480,7 +480,7 @@ Style/RedundantException: Style/RedundantReturn: Description: "Don't use return where it's not required." StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return' - Enabled: false + Enabled: true Style/RedundantSelf: Description: "Don't use self where it's not needed." diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 985def4ad66..08221aaa2f8 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -182,7 +182,7 @@ module GitlabMarkdownHelper def file_exists?(path) return false if path.nil? - return @repository.blob_at(current_sha, path).present? || @repository.tree(current_sha, path).entries.any? + @repository.blob_at(current_sha, path).present? || @repository.tree(current_sha, path).entries.any? end # Check if the path is pointing to a directory(tree) or a file(blob) @@ -190,7 +190,7 @@ module GitlabMarkdownHelper def local_path(path) return "tree" if @repository.tree(current_sha, path).entries.any? return "raw" if @repository.blob_at(current_sha, path).image? - return "blob" + "blob" end def current_sha diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 51b60770e0b..54462fd00e3 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -17,7 +17,7 @@ module MergeRequestsHelper end def new_mr_from_push_event(event, target_project) - return { + { merge_request: { source_project_id: event.project.id, target_project_id: target_project.id, diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index 525266fb3b5..241462e5e4c 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -49,7 +49,7 @@ module SubmoduleHelper def standard_links(host, namespace, project, commit) base = [ 'https://', host, '/', namespace, '/', project ].join('') - return base, [ base, '/tree/', commit ].join('') + [base, [ base, '/tree/', commit ].join('')] end def relative_self_links(url, commit) @@ -58,7 +58,10 @@ module SubmoduleHelper else base = [ @project.group.path, '/', url[/([^\/]*)\.git/, 1] ].join('') end - return namespace_project_path(base.namespace, base), + + [ + namespace_project_path(base.namespace, base), namespace_project_tree_path(base.namespace, base, commit) + ] end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index cb69e4b13d3..573415baec1 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -7,7 +7,7 @@ module Gitlab def self.can_push_to_branch?(user, project, ref) return false unless user - + if project.protected_branch?(ref) && !(project.developers_can_push_to_protected_branch?(ref) && project.team.developer?(user)) user.can?(:push_code_to_protected_branches, project) @@ -83,7 +83,7 @@ module Gitlab end end - return build_status_object(true) + build_status_object(true) end def change_access_check(user, project, change) diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb index fea4d2d55d2..43e07e09160 100644 --- a/lib/gitlab/popen.rb +++ b/lib/gitlab/popen.rb @@ -29,7 +29,7 @@ module Gitlab @cmd_status = wait_thr.value.exitstatus end - return @cmd_output, @cmd_status + [@cmd_output, @cmd_status] end end end diff --git a/lib/gitlab/theme.rb b/lib/gitlab/theme.rb index 9799e54de5d..43093c7d27e 100644 --- a/lib/gitlab/theme.rb +++ b/lib/gitlab/theme.rb @@ -19,7 +19,7 @@ module Gitlab id ||= Gitlab.config.gitlab.default_theme - return themes[id] + themes[id] end def self.type_css_class_by_id(id) -- GitLab From f2ea9d9f3fc34d66c8b6c630ef8fb9b810b821e9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 24 Mar 2015 19:00:00 -0700 Subject: [PATCH 1540/1609] Improve sticky headers in diffs * disable sticky headers in discussion * enable sticky header on mr page with you click changes tab --- app/assets/javascripts/diff.js.coffee | 2 -- app/views/projects/diffs/_diffs.html.haml | 12 +++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee index 05f5af42571..069f91c30e1 100644 --- a/app/assets/javascripts/diff.js.coffee +++ b/app/assets/javascripts/diff.js.coffee @@ -37,8 +37,6 @@ class @Diff ) ) - $('.diff-header').stick_in_parent(recalc_every: 1, offset_top: $('.navbar').height()) - lineNumbers: (line) -> return ([0, 0]) unless line.children().length lines = line.children().slice(0, 2) diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 1747f36dcf3..2b9b6599a7d 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,10 +1,9 @@ -.row.prepend-top-20.append-bottom-10 - .col-md-8 - = render 'projects/diffs/stats', diffs: diffs - .col-md-4 - .btn-group.pull-right +.prepend-top-20.append-bottom-20 + .pull-right + .btn-group = inline_diff_btn = parallel_diff_btn + = render 'projects/diffs/stats', diffs: diffs - if show_diff_size_warning?(diffs) = render 'projects/diffs/warning', diffs: diffs @@ -19,3 +18,6 @@ Failed to collect changes %p Maybe diff is really big and operation failed with timeout. Try to get diff locally + +:coffeescript + $('.files .diff-header').stick_in_parent(recalc_every: 1, offset_top: $('.navbar').height()) -- GitLab From 083027fc84e61f5b592ed43cdbdb8425e17d2a7f Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Tue, 24 Mar 2015 20:37:19 -0600 Subject: [PATCH 1541/1609] Change directory when removing old backups --- CHANGELOG | 1 + lib/backup/manager.rb | 15 ++++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 737831eb195..2be8a6bbd4b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,6 +25,7 @@ v 7.10.0 (unreleased) - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) - Add location field to user profile - Fix print view for markdown files and wiki pages + - Fix errors when deleting old backups - Improve GitLab performance when working with git repositories - Add tag message and last commit to tag hook (Kamil Trzciński) - Restrict permissions on backup files diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index c6087830b40..afd05897509 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -70,16 +70,17 @@ module Backup # delete backups $progress.print "Deleting old backups ... " keep_time = Gitlab.config.backup.keep_time.to_i - path = Gitlab.config.backup.path if keep_time > 0 removed = 0 - file_list = Dir.glob(Rails.root.join(path, "*_gitlab_backup.tar")) - file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } - file_list.sort.each do |timestamp| - if Time.at(timestamp) < (Time.now - keep_time) - if Kernel.system(*%W(rm #{timestamp}_gitlab_backup.tar)) - removed += 1 + Dir.chdir(Gitlab.config.backup.path) do + file_list = Dir.glob('*_gitlab_backup.tar') + file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } + file_list.sort.each do |timestamp| + if Time.at(timestamp) < (Time.now - keep_time) + if Kernel.system(*%W(rm #{timestamp}_gitlab_backup.tar)) + removed += 1 + end end end end -- GitLab From d554070a62b0bc34ab2289d4a071b950df4d5485 Mon Sep 17 00:00:00 2001 From: Robert Schilling Date: Wed, 25 Mar 2015 08:38:05 +0100 Subject: [PATCH 1542/1609] Fix changelog for 7.9 --- CHANGELOG | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7b24fb00c6d..d4a1346c481 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -37,11 +37,9 @@ v 7.10.0 (unreleased) - Don't mark merge request as updated when merge status relative to target branch changes. - Link note avatar to user. - Make Git-over-SSH errors more descriptive. - -v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. -v 7.9.0 (unreleased) +v 7.9.0 - Add HipChat integration documentation (Stan Hu) - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) - Fix broken email images (Hannes Rosenögger) -- GitLab From ec3f0b0e178bfb64165ce1572518ab3b9f56eb15 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Wed, 25 Mar 2015 12:18:44 +0200 Subject: [PATCH 1543/1609] [doc] Groups can be browsable if they contain at least one public project. --- doc/permissions/permissions.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index c9928e11b2e..8cfa7f9c876 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -41,6 +41,11 @@ If a user is a GitLab administrator they receive all permissions. ## Group +In order for a group to appear as public and be browsable, it must contain at +least one public project. + +Any user can remove themselves from a group, unless they are the last Owner of the group. + | Action | Guest | Reporter | Developer | Master | Owner | |-------------------------|-------|----------|-----------|--------|-------| | Browse group | ✓ | ✓ | ✓ | ✓ | ✓ | @@ -48,5 +53,3 @@ If a user is a GitLab administrator they receive all permissions. | Create project in group | | | | ✓ | ✓ | | Manage group members | | | | | ✓ | | Remove group | | | | | ✓ | - -Any user can remove themselves from a group, unless they are the last Owner of the group. -- GitLab From a30011372f9608f688cb490c584e8f380b54c211 Mon Sep 17 00:00:00 2001 From: nicklegr Date: Wed, 25 Mar 2015 21:05:06 +0900 Subject: [PATCH 1544/1609] Reset parking branch to HEAD everytime * Reduces overhead of git checkout --- lib/gitlab/satellite/satellite.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb index 70125d539da..f24c6199c44 100644 --- a/lib/gitlab/satellite/satellite.rb +++ b/lib/gitlab/satellite/satellite.rb @@ -99,11 +99,7 @@ module Gitlab heads = repo.heads.map(&:name) # update or create the parking branch - if heads.include? PARKING_BRANCH - repo.git.checkout({}, PARKING_BRANCH) - else - repo.git.checkout(default_options({ b: true }), PARKING_BRANCH) - end + repo.git.checkout(default_options({ B: true }), PARKING_BRANCH) # remove the parking branch from the list of heads ... heads.delete(PARKING_BRANCH) -- GitLab From f21bd9b1d59f276db6b83c126960ecd3d7a30dbd Mon Sep 17 00:00:00 2001 From: nicklegr Date: Wed, 25 Mar 2015 22:46:01 +0900 Subject: [PATCH 1545/1609] Update CHANGELOG [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index d4a1346c481..6c2627fc431 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -38,6 +38,7 @@ v 7.10.0 (unreleased) - Link note avatar to user. - Make Git-over-SSH errors more descriptive. - Send EmailsOnPush email when branch or tag is created or deleted. + - Faster merge request processing for large repository v 7.9.0 - Add HipChat integration documentation (Stan Hu) -- GitLab From c8fb972606538298ce000ea16b8037bb6569b63f Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 25 Mar 2015 18:18:26 +0200 Subject: [PATCH 1546/1609] notification on project moving --- app/services/projects/transfer_service.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 3372cfc11d0..489e03bd5ef 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -43,6 +43,9 @@ module Projects project.namespace = new_namespace project.save! + # Notifications + project.send_move_instructions + # Move main repository unless gitlab_shell.mv_repository(old_path, new_path) raise TransferError.new('Cannot move project') -- GitLab From 8173ef976f91088d17b28a1581ab6fd80949e462 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 25 Mar 2015 11:58:31 -0700 Subject: [PATCH 1547/1609] Set Application controller default URL options to ensure all url_for calls are consistent Closes #1249 --- CHANGELOG | 1 + app/controllers/application_controller.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 7c3e5dfcb31..301bf87245a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu) - Allow HTML tags in Markdown input - Include missing events and fix save functionality in admin service template settings form (Stan Hu) - Fix "Import projects from" button to show the correct instructions (Stan Hu) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e284f31f7ee..2809f90c0d5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -178,6 +178,18 @@ class ApplicationController < ActionController::Base response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" end + def default_url_options + if !Rails.env.test? + port = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? + { host: Gitlab.config.gitlab.host, + protocol: Gitlab.config.gitlab.protocol, + port: port, + script_name: Gitlab.config.gitlab.relative_url_root } + else + super + end + end + def default_headers headers['X-Frame-Options'] = 'DENY' headers['X-XSS-Protection'] = '1; mode=block' -- GitLab From 30080961062b896102cb9a4816d4c0c7abe63ffa Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 Mar 2015 17:29:27 -0400 Subject: [PATCH 1548/1609] Add archived status to Admin > Project page --- app/views/admin/projects/show.html.haml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 077ee569085..62a6772e238 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -68,6 +68,11 @@ %strong.cred does not exist + - if @project.archived? + %li + %span.light archived: + %strong repository is read-only + %li %span.light access: %strong -- GitLab From c31d40e358eb778a2c0f8e539a610f7763411db2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 25 Mar 2015 17:29:45 -0400 Subject: [PATCH 1549/1609] Update the "Edit Group" button class on Project Admin page --- app/views/admin/projects/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 62a6772e238..05372f4124f 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -102,7 +102,7 @@ %strong #{@group.name} group members (#{@group.group_members.count}) .pull-right - = link_to admin_group_path(@group), class: 'btn btn-sm' do + = link_to admin_group_path(@group), class: 'btn btn-xs' do %i.fa.fa-pencil-square-o %ul.well-list - @group_members.each do |member| -- GitLab From 8b7aedad2bf8b8b9eb453a43206db114d9128dcd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Mar 2015 15:47:31 -0700 Subject: [PATCH 1550/1609] Prevent doubling AJAX request with each commit visit via Turbolink --- CHANGELOG | 1 + app/views/projects/commit/_commit_box.html.haml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9aa6e3530b4..144ada47ec1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -40,6 +40,7 @@ v 7.10.0 (unreleased) - Make Git-over-SSH errors more descriptive. - Send EmailsOnPush email when branch or tag is created or deleted. - Faster merge request processing for large repository + - Prevent doubling AJAX request with each commit visit via Turbolink v 7.9.0 - Add HipChat integration documentation (Stan Hu) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 7409f702c5d..2579f2cac92 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -48,5 +48,4 @@ = preserve(gfm(escape_once(@commit.description))) :coffeescript - $ -> - $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}") + $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}") -- GitLab From a1d09190e3311cfe7744e9792a4c69822ff84b39 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Mar 2015 21:45:01 -0700 Subject: [PATCH 1551/1609] Prevent unnecessary doubling of js events on import pages and user calendar --- CHANGELOG | 1 + app/views/import/bitbucket/status.html.haml | 3 +-- app/views/import/github/status.html.haml | 3 +-- app/views/import/gitlab/status.html.haml | 3 +-- app/views/import/gitorious/status.html.haml | 3 +-- app/views/projects/issues/_issue_context.html.haml | 3 +-- .../projects/merge_requests/show/_context.html.haml | 5 +---- app/views/projects/new.html.haml | 11 +++++------ app/views/users/show.html.haml | 3 +-- 9 files changed, 13 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f9bd0940a71..d16f20266cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -42,6 +42,7 @@ v 7.10.0 (unreleased) - Send EmailsOnPush email when branch or tag is created or deleted. - Faster merge request processing for large repository - Prevent doubling AJAX request with each commit visit via Turbolink + - Prevent unnecessary doubling of js events on import pages and user calendar v 7.9.0 - Add HipChat integration documentation (Stan Hu) diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 9da3c920c62..4e49bbbc7fa 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -42,5 +42,4 @@ = button_tag "Import", class: "btn js-add-to-import" :coffeescript - $ -> - new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}") + new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}") diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 9c4d91013ec..f0bc3e6b1ac 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -42,5 +42,4 @@ = button_tag "Import", class: "btn js-add-to-import" :coffeescript - $ -> - new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}") + new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}") diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index e809643d8d4..33b0a21acf3 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -42,5 +42,4 @@ = button_tag "Import", class: "btn js-add-to-import" :coffeescript - $ -> - new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}") + new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}") diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml index 645241a6c69..78c5e957be0 100644 --- a/app/views/import/gitorious/status.html.haml +++ b/app/views/import/gitorious/status.html.haml @@ -42,5 +42,4 @@ = button_tag "Import", class: "btn js-add-to-import" :coffeescript - $ -> - new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}") + new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}") diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index 91fe0b68371..c3d6dc2e50b 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -43,7 +43,6 @@ You're receiving notifications because you're subscribed to this thread. :coffeescript - $ -> - new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") + new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index 14ad89a2000..80e5c223d60 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -45,7 +45,4 @@ You're receiving notifications because you're subscribed to this thread. :coffeescript - $ -> - new Subscription("#{toggle_subscription_namespace_project_merge_request_path(@merge_request.project.namespace, @project, @merge_request)}") - - \ No newline at end of file + new Subscription("#{toggle_subscription_namespace_project_merge_request_path(@merge_request.project.namespace, @project, @merge_request)}") diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 9687c8ad87c..42af2f32239 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -109,9 +109,8 @@ %p Please wait a moment, this page will automatically refresh when ready. :coffeescript - $ -> - $('.how_to_import_link').bind 'click', (e) -> - e.preventDefault() - import_modal = $(this).next(".modal").show() - $('.modal-header .close').bind 'click', -> - $(".modal").hide() + $('.how_to_import_link').bind 'click', (e) -> + e.preventDefault() + import_modal = $(this).next(".modal").show() + $('.modal-header .close').bind 'click', -> + $(".modal").hide() diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0653fb871ae..5e1d65e2ed8 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -47,5 +47,4 @@ = render 'projects' :coffeescript - $ -> - $(".user-calendar").load("#{user_calendar_path}") + $(".user-calendar").load("#{user_calendar_path}") -- GitLab From 80fd8f2de19634b62356e27cd0b105fef6787931 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Mar 2015 22:22:10 -0700 Subject: [PATCH 1552/1609] Capitalize js class name --- app/assets/javascripts/calendar.js.coffee | 2 +- app/views/users/calendar.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index d08ef9361a6..37b7ba2cc10 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -1,4 +1,4 @@ -class @calendar +class @Calendar options = month: "short" day: "numeric" diff --git a/app/views/users/calendar.html.haml b/app/views/users/calendar.html.haml index 488f49267c7..922b0c6cebf 100644 --- a/app/views/users/calendar.html.haml +++ b/app/views/users/calendar.html.haml @@ -4,7 +4,7 @@ %small Issues, merge requests and push events #cal-heatmap.calendar :javascript - new calendar( + new Calendar( #{@timestamps.to_json}, #{@starting_year}, #{@starting_month}, -- GitLab From 429a43ca6a6263cd3fad56059dbbd8abf9210aa5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Mar 2015 22:32:08 -0700 Subject: [PATCH 1553/1609] Dont bind all checkboxes when you need only protected branches --- app/assets/javascripts/protected_branches.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee index 691fd4f10d8..59cf4e8a752 100644 --- a/app/assets/javascripts/protected_branches.js.coffee +++ b/app/assets/javascripts/protected_branches.js.coffee @@ -1,5 +1,5 @@ $ -> - $(":checkbox").change -> + $(".protected-branches-list :checkbox").change -> name = $(this).attr("name") if name == "developers_can_push" id = $(this).val() -- GitLab From 3a11165648baaa5bfa474e335526747ddb6dde5e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Mar 2015 22:45:58 -0700 Subject: [PATCH 1554/1609] Improve protected branches page UX --- app/assets/javascripts/protected_branches.js.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee index 59cf4e8a752..5753c9d4e72 100644 --- a/app/assets/javascripts/protected_branches.js.coffee +++ b/app/assets/javascripts/protected_branches.js.coffee @@ -1,5 +1,5 @@ $ -> - $(".protected-branches-list :checkbox").change -> + $(".protected-branches-list :checkbox").change (e) -> name = $(this).attr("name") if name == "developers_can_push" id = $(this).val() @@ -14,8 +14,8 @@ $ -> developers_can_push: checked success: -> - new Flash("Branch updated.", "notice") - location.reload true + row = $(e.target) + row.closest('tr').effect('highlight') error: -> new Flash("Failed to update branch!", "alert") -- GitLab From 0b355aba020ff2b1e9a89428154aa08484a06297 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Mar 2015 23:07:59 -0700 Subject: [PATCH 1555/1609] Replace alerts with well where alert is not needed --- app/views/admin/services/_form.html.haml | 2 +- app/views/projects/edit.html.haml | 13 +++++-------- app/views/projects/imports/new.html.haml | 2 +- app/views/projects/new.html.haml | 2 +- .../projects/protected_branches/index.html.haml | 2 +- app/views/projects/services/_form.html.haml | 2 +- 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 18b7e8ba270..eb7a099bfe2 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -10,7 +10,7 @@ - @service.errors.full_messages.each do |msg| %p= msg - if @service.help.present? - .alert.alert-info + .well = preserve do = markdown @service.help diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index e0d75113a5e..fbf04847e48 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -31,14 +31,11 @@ = render "visibility_level", f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can?(current_user, :change_visibility_level, @project) - %fieldset.features - %legend - Tags: - .form-group - = f.label :tag_list, "Tags", class: 'control-label' - .col-sm-10 - = f.text_field :tag_list, maxlength: 2000, class: "form-control" - %p.hint Separate tags with commas. + .form-group + = f.label :tag_list, "Tags", class: 'control-label' + .col-sm-10 + = f.text_field :tag_list, maxlength: 2000, class: "form-control" + %p.help-block Separate tags with commas. %fieldset.features %legend diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index f1248ac2af5..934b6b8c017 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -12,7 +12,7 @@ %span Import existing git repo .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .alert.alert-info + .well.prepend-top-20 This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. %br The import will time out after 4 minutes. For big repositories, use a clone/push combination. diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 42af2f32239..69909a8554e 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -72,7 +72,7 @@ %span Git repository URL .col-sm-10 = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' - .alert.alert-info.prepend-top-10 + .well.prepend-top-20 %ul %li The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: https://username:password@gitlab.company.com/group/project.git. diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 4db71ce8ff9..a3464c0e5e1 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -2,7 +2,7 @@ %p.light Keep stable branches secure and force developers to use Merge Requests %hr -.alert.alert-info +.well.append-bottom-20 %p Protected branches are designed to %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 32e97a754cb..ce6b7a0737a 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -18,7 +18,7 @@ %li= msg - if @service.help.present? - .alert.alert-info + .well = preserve do = markdown @service.help -- GitLab From 729f0b2e68c0f0ca416579c5845fd92c1e62faa4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Mar 2015 23:16:01 -0700 Subject: [PATCH 1556/1609] Nicer well --- app/assets/stylesheets/base/gl_variables.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss index 17b5622d74a..56f4c794e1b 100644 --- a/app/assets/stylesheets/base/gl_variables.scss +++ b/app/assets/stylesheets/base/gl_variables.scss @@ -115,6 +115,12 @@ $panel-default-border: $border-color; $panel-default-heading-bg: $background-color; +//== Wells +// +//## + +$well-bg: #F9F9F9; +$well-border: #EEE; //== Code // -- GitLab From 366ce781b3d2687c2c8246a7230617217cae59f5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Mar 2015 23:16:43 -0700 Subject: [PATCH 1557/1609] Remove unused doorkeeper layout --- app/views/layouts/doorkeeper/admin.html.haml | 22 ------------------- .../layouts/doorkeeper/application.html.haml | 15 ------------- 2 files changed, 37 deletions(-) delete mode 100644 app/views/layouts/doorkeeper/admin.html.haml delete mode 100644 app/views/layouts/doorkeeper/application.html.haml diff --git a/app/views/layouts/doorkeeper/admin.html.haml b/app/views/layouts/doorkeeper/admin.html.haml deleted file mode 100644 index bd9adfab66d..00000000000 --- a/app/views/layouts/doorkeeper/admin.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -!!! -%html - %head - %meta{:charset => "utf-8"} - %meta{:content => "IE=edge", "http-equiv" => "X-UA-Compatible"} - %meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"} - %title Doorkeeper - = stylesheet_link_tag "doorkeeper/admin/application" - = csrf_meta_tags - %body - .navbar.navbar-inverse.navbar-fixed-top{:role => "navigation"} - .container - .navbar-header - = link_to 'OAuth2 Provider', oauth_applications_path, class: 'navbar-brand' - %ul.nav.navbar-nav - = content_tag :li, class: "#{'active' if request.path == oauth_applications_path}" do - = link_to 'Applications', oauth_applications_path - .container - - if flash[:notice].present? - .alert.alert-info - = flash[:notice] - = yield \ No newline at end of file diff --git a/app/views/layouts/doorkeeper/application.html.haml b/app/views/layouts/doorkeeper/application.html.haml deleted file mode 100644 index e5f37fad1f4..00000000000 --- a/app/views/layouts/doorkeeper/application.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -!!! -%html - %head - %title OAuth authorize required - %meta{:charset => "utf-8"} - %meta{:content => "IE=edge", "http-equiv" => "X-UA-Compatible"} - %meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"} - = stylesheet_link_tag "doorkeeper/application" - = csrf_meta_tags - %body - #container - - if flash[:notice].present? - .alert.alert-info - = flash[:notice] - = yield \ No newline at end of file -- GitLab From 33d134f0933c50522c437f883d7cf52c1b974ecf Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 25 Mar 2015 23:21:13 -0700 Subject: [PATCH 1558/1609] Fix diff header for discussion --- app/assets/stylesheets/base/gl_bootstrap.scss | 1 + app/assets/stylesheets/pages/notes.scss | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss index e176cce5c69..62a3eade5c7 100644 --- a/app/assets/stylesheets/base/gl_bootstrap.scss +++ b/app/assets/stylesheets/base/gl_bootstrap.scss @@ -182,6 +182,7 @@ .panel-heading { padding: 6px 15px; font-size: 13px; + font-weight: normal; a { color: #777; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 70505dc4300..d66093bc2e5 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -90,8 +90,13 @@ ul.notes { } // Diff code in discussion view -.discussion-body .diff-file .line_content { - white-space: pre-wrap; +.discussion-body .diff-file { + .diff-header > span { + margin-right: 10px; + } + .line_content { + white-space: pre-wrap; + } } .diff-file .notes_holder { -- GitLab From 329db2c5dedbddace96af2c343443dcba52c170b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 26 Mar 2015 07:35:26 +0100 Subject: [PATCH 1559/1609] Fix EmailsOnPush. --- CHANGELOG | 1 + app/workers/emails_on_push_worker.rb | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 25936eb1e1d..b133a3ec59b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -31,6 +31,7 @@ v 7.10.0 (unreleased) - Replace commits calendar with faster contribution calendar that includes issues and merge requests - Add inifinite scroll to user page activity - Don't show commit comment button when user is not signed in. + - Fix EmailsOnPush. v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 89fa2117dd2..1d21addece6 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,7 +1,15 @@ class EmailsOnPushWorker include Sidekiq::Worker - def perform(project_id, recipients, push_data, send_from_committer_email: false, disable_diffs: false) + def perform(project_id, recipients, push_data, options = {}) + options.symbolize_keys! + options.reverse_merge!( + send_from_committer_email: false, + disable_diffs: false + ) + send_from_committer_email = options[:send_from_committer_email] + disable_diffs = options[:disable_diffs] + project = Project.find(project_id) before_sha = push_data["before"] after_sha = push_data["after"] -- GitLab From 546dab6da9b157efcd2e45c38b94eb118919fa4f Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 25 Mar 2015 23:39:34 -0700 Subject: [PATCH 1560/1609] Fix broken side-by-side diff view on merge request page Closes #1294 --- CHANGELOG | 1 + app/helpers/diff_helper.rb | 4 ++++ features/project/merge_requests.feature | 7 +++++++ features/steps/project/merge_requests.rb | 14 ++++++++++++++ 4 files changed, 26 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index f9bd0940a71..0de04eac64f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.10.0 (unreleased) + - Fix broken side-by-side diff view on merge request page (Stan Hu) - Allow HTML tags in Markdown input - Fix code unfold not working on Compare commits page (Stan Hu) - Include missing events and fix save functionality in admin service template settings form (Stan Hu) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f81504991d3..b56f21c7a18 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -121,6 +121,8 @@ module DiffHelper def inline_diff_btn params_copy = params.dup params_copy[:view] = 'inline' + # Always use HTML to handle case where JSON diff rendered this button + params_copy.delete(:format) link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do 'Inline' @@ -130,6 +132,8 @@ module DiffHelper def parallel_diff_btn params_copy = params.dup params_copy[:view] = 'parallel' + # Always use HTML to handle case where JSON diff rendered this button + params_copy.delete(:format) link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do 'Side-by-side' diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 91dc576f8b4..cbb5c8eb39b 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -166,6 +166,13 @@ Feature: Project Merge Requests And I click Side-by-side Diff tab Then I should see comments on the side-by-side diff page + @javascript + Scenario: I view diffs on a merge request + Given project "Shop" have "Bug NS-05" open merge request with diffs inside + And I visit merge request page "Bug NS-05" + And I click on the Changes tab via Javascript + Then I should see the proper Inline and Side-by-side links + # Task status in issues list Scenario: Merge requests list should display task status diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 6f6ce439f3e..40c102833a4 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -117,6 +117,20 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) end + step 'I click on the Changes tab via Javascript' do + find('.diffs-tab').click + sleep 2 + end + + step 'I should see the proper Inline and Side-by-side links' do + buttons = all('#commit-diff-viewtype') + expect(buttons.count).to eq(2) + + buttons.each do |b| + expect(b['href']).should_not have_content('json') + end + end + step 'I switch to the merge request\'s comments tab' do visit namespace_project_merge_request_path(project.namespace, project, merge_request) end -- GitLab From 7fdc017650e053ace32b7937c104577db45bf513 Mon Sep 17 00:00:00 2001 From: Keith Pitt Date: Thu, 26 Mar 2015 11:40:43 +0000 Subject: [PATCH 1561/1609] Renamed Buildbox to Buildkite. --- .../project_services/buildbox_service.rb | 26 ++++++++++--------- .../project_services/buildbox_service_spec.rb | 12 ++++----- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildbox_service.rb index fef1c9b7349..3a381ff11b8 100644 --- a/app/models/project_services/buildbox_service.rb +++ b/app/models/project_services/buildbox_service.rb @@ -20,7 +20,11 @@ require "addressable/uri" +# Buildbox renamed to Buildkite, but for backwards compatability with the STI +# of Services, the class name is kept as "Buildbox" class BuildboxService < CiService + ENDPOINT = "https://buildkite.com" + prop_accessor :project_url, :token validates :project_url, presence: true, if: :activated? @@ -29,7 +33,7 @@ class BuildboxService < CiService after_save :compose_service_hook, if: :activated? def webhook_url - "#{buildbox_endpoint('webhook')}/deliver/#{webhook_token}" + "#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}" end def compose_service_hook @@ -59,7 +63,7 @@ class BuildboxService < CiService end def commit_status_path(sha) - "#{buildbox_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}" + "#{buildkite_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}" end def build_page(sha, ref) @@ -71,11 +75,11 @@ class BuildboxService < CiService end def status_img_path - "#{buildbox_endpoint('badge')}/#{status_token}.svg" + "#{buildkite_endpoint('badge')}/#{status_token}.svg" end def title - 'Buildbox' + 'Buildkite' end def description @@ -83,18 +87,18 @@ class BuildboxService < CiService end def to_param - 'buildbox' + 'buildkite' end def fields [ { type: 'text', name: 'token', - placeholder: 'Buildbox project GitLab token' }, + placeholder: 'Buildkite project GitLab token' }, { type: 'text', name: 'project_url', - placeholder: 'https://buildbox.io/example/project' } + placeholder: "#{ENDPOINT}/example/project" } ] end @@ -116,11 +120,9 @@ class BuildboxService < CiService end end - def buildbox_endpoint(subdomain = nil) - endpoint = 'https://buildbox.io' - + def buildkite_endpoint(subdomain = nil) if subdomain.present? - uri = Addressable::URI.parse(endpoint) + uri = Addressable::URI.parse(ENDPOINT) new_endpoint = "#{uri.scheme || 'http'}://#{subdomain}.#{uri.host}" if uri.port.present? @@ -129,7 +131,7 @@ class BuildboxService < CiService new_endpoint end else - endpoint + ENDPOINT end end end diff --git a/spec/models/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildbox_service_spec.rb index fcbf3e45b9a..9f29fbe12b0 100644 --- a/spec/models/project_services/buildbox_service_spec.rb +++ b/spec/models/project_services/buildbox_service_spec.rb @@ -36,7 +36,7 @@ describe BuildboxService do @service.stub( project: @project, service_hook: true, - project_url: 'https://buildbox.io/account-name/example-project', + project_url: 'https://buildkite.com/account-name/example-project', token: 'secret-sauce-webhook-token:secret-sauce-status-token' ) end @@ -44,7 +44,7 @@ describe BuildboxService do describe :webhook_url do it 'returns the webhook url' do expect(@service.webhook_url).to eq( - 'https://webhook.buildbox.io/deliver/secret-sauce-webhook-token' + 'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token' ) end end @@ -52,7 +52,7 @@ describe BuildboxService do describe :commit_status_path do it 'returns the correct status page' do expect(@service.commit_status_path('2ab7834c')).to eq( - 'https://gitlab.buildbox.io/status/secret-sauce-status-token.json?commit=2ab7834c' + 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c' ) end end @@ -60,7 +60,7 @@ describe BuildboxService do describe :build_page do it 'returns the correct build page' do expect(@service.build_page('2ab7834c', nil)).to eq( - 'https://buildbox.io/account-name/example-project/builds?commit=2ab7834c' + 'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c' ) end end @@ -68,14 +68,14 @@ describe BuildboxService do describe :builds_page do it 'returns the correct path to the builds page' do expect(@service.builds_path).to eq( - 'https://buildbox.io/account-name/example-project/builds?branch=default-brancho' + 'https://buildkite.com/account-name/example-project/builds?branch=default-brancho' ) end end describe :status_img_path do it 'returns the correct path to the status image' do - expect(@service.status_img_path).to eq('https://badge.buildbox.io/secret-sauce-status-token.svg') + expect(@service.status_img_path).to eq('https://badge.buildkite.com/secret-sauce-status-token.svg') end end end -- GitLab From 3cb28de28f7d0c45439a1eb43c4b4875e1f34a3d Mon Sep 17 00:00:00 2001 From: Bart Deslagmulder Date: Thu, 26 Mar 2015 20:14:40 +0100 Subject: [PATCH 1562/1609] Fix typo 'projecti' to 'project' in ssh doc [ci skip] --- doc/ssh/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 66941521c2e..0acb15896d3 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -68,6 +68,6 @@ You can't add the same deploy key twice with the 'New Deploy Key' option. If you want to add the same key to another project, please enable it in the list that says 'Deploy keys from projects available to you'. All the deploy keys of all the projects you have access to are available. This project -access can happen through being a direct member of the projecti, or through +access can happen through being a direct member of the project, or through a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more information. -- GitLab From 91b108192c84178ec100ece41f159b68738f1a32 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 26 Mar 2015 19:38:20 +0000 Subject: [PATCH 1563/1609] Fork/Star button re-design --- app/assets/stylesheets/generic/mobile.scss | 7 ------ app/assets/stylesheets/pages/projects.scss | 22 +++-------------- app/helpers/projects_helper.rb | 4 ++-- app/views/projects/_home_panel.html.haml | 28 ++++++++++------------ 4 files changed, 17 insertions(+), 44 deletions(-) diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index 1b0e056216f..71a1fc4493f 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -24,13 +24,6 @@ display: none !important; } - .project-home-panel { - .star-fork-buttons { - padding-top: 10px; - padding-right: 15px; - } - } - .project-home-links { display: none; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6d55a5fa66e..9ad1be41579 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -49,8 +49,7 @@ @extend .clearfix; margin-bottom: 15px; - .project-home-desc, - .star-fork-buttons { + .project-home-desc { font-size: 16px; line-height: 1.3; } @@ -60,23 +59,8 @@ color: #666; } - .star-fork-buttons { - float: right; - min-width: 200px; - font-weight: bold; - - .star-buttons, .fork-buttons { - float: right; - margin-left: 20px; - - a:hover { - text-decoration: none; - } - - .count { - margin-left: 5px; - } - } + .btn-action-count { + margin-left: 5px; } } diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 7bf51b5b8e8..f535b01b2ec 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -81,7 +81,7 @@ module ProjectsHelper end def link_to_toggle_star(title, starred, signed_in) - cls = 'star-btn' + cls = 'star-btn btn btn-primary' cls << ' disabled' unless signed_in toggle_html = content_tag('span', class: 'toggle') do @@ -120,7 +120,7 @@ module ProjectsHelper def link_to_toggle_fork out = icon('code-fork') out << ' Fork' - out << content_tag(:span, class: 'count') do + out << content_tag(:span, class: 'count btn-action-count') do @project.forks_count.to_s end end diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index a295a0d6cdc..3bb70458b16 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -14,29 +14,25 @@ – = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do = readme.name - .star-fork-buttons + + .project-home-row.hidden-xs + - if current_user && !empty_repo + .project-home-dropdown + = render "dropdown" + %span.star.pull-right.prepend-left-10.js-toggler-container{class: @show_star ? 'on' : ''} + - if current_user + = link_to_toggle_star('Star this project.', false, true) + = link_to_toggle_star('Unstar this project.', true, true) + .pull-right.prepend-left-10 - unless @project.empty_repo? .fork-buttons - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-primary' do = link_to_toggle_fork - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project" do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-primary' do = link_to_toggle_fork - - .star-buttons - %span.star.js-toggler-container{class: @show_star ? 'on' : ''} - - if current_user - = link_to_toggle_star('Star this project.', false, true) - = link_to_toggle_star('Unstar this project.', true, true) - - else - = link_to_toggle_star('You must sign in to star a project.', false, false) - - .project-home-row.hidden-xs - - if current_user && !empty_repo - .project-home-dropdown - = render "dropdown" - unless @project.empty_repo? - if can? current_user, :download_code, @project .pull-right.prepend-left-10 -- GitLab From b322344520b5d92c0199bd2222187fab62b7eee5 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 26 Mar 2015 18:07:20 -0700 Subject: [PATCH 1564/1609] Add a note about not adding new setting options to gitlab.yml. --- config/gitlab.yml.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index c4a0fefb7ab..11278ef40dc 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -2,6 +2,11 @@ # GitLab application config file # # # # # # # # # # # # # # # # # # # # +########################### NOTE ##################################### +# This file should not receive new settings. All configuration options # +# are being moved to ApplicationSetting model! # +######################################################################## +# # How to use: # 1. Copy file as gitlab.yml # 2. Update gitlab -> host with your fully qualified domain name -- GitLab From dfb4fcb68544717d5cc74a2035a9d9c5bd61fc21 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 26 Mar 2015 18:56:42 -0700 Subject: [PATCH 1565/1609] No magic numbers for issues filtering --- app/finders/issuable_finder.rb | 8 +++++--- app/services/issues/update_service.rb | 4 ++-- app/services/merge_requests/update_service.rb | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 088a766ed3a..67939c094c5 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -19,6 +19,8 @@ require_relative 'projects_finder' class IssuableFinder + NONE = 0 + attr_accessor :current_user, :params def execute(current_user, params) @@ -112,7 +114,7 @@ class IssuableFinder def by_milestone(items) if params[:milestone_id].present? - items = items.where(milestone_id: (params[:milestone_id] == '0' ? nil : params[:milestone_id])) + items = items.where(milestone_id: (params[:milestone_id] == NONE ? nil : params[:milestone_id])) end items @@ -120,7 +122,7 @@ class IssuableFinder def by_assignee(items) if params[:assignee_id].present? - items = items.where(assignee_id: (params[:assignee_id] == '0' ? nil : params[:assignee_id])) + items = items.where(assignee_id: (params[:assignee_id] == NONE ? nil : params[:assignee_id])) end items @@ -128,7 +130,7 @@ class IssuableFinder def by_author(items) if params[:author_id].present? - items = items.where(author_id: (params[:author_id] == '0' ? nil : params[:author_id])) + items = items.where(author_id: (params[:author_id] == NONE ? nil : params[:author_id])) end items diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 3371fe7d5ef..8f04a69287a 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -14,8 +14,8 @@ module Issues issue.update_nth_task(params[:task_num].to_i, false) end - params[:assignee_id] = "" if params[:assignee_id] == "-1" - params[:milestone_id] = "" if params[:milestone_id] == "-1" + params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE + params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE old_labels = issue.labels.to_a diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 0ac6dfea6fd..23af2656c37 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -23,8 +23,8 @@ module MergeRequests merge_request.update_nth_task(params[:task_num].to_i, false) end - params[:assignee_id] = "" if params[:assignee_id] == "-1" - params[:milestone_id] = "" if params[:milestone_id] == "-1" + params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE + params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE old_labels = merge_request.labels.to_a -- GitLab From 26053c870530ebb6d276a364ea9d40d202ead0de Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 26 Mar 2015 19:06:19 -0700 Subject: [PATCH 1566/1609] Add autocomplete controller --- app/controllers/autocomplete_controller.rb | 30 +++++++++++ config/routes.rb | 5 ++ .../autocomplete_controller_spec.rb | 51 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 app/controllers/autocomplete_controller.rb create mode 100644 spec/controllers/autocomplete_controller_spec.rb diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb new file mode 100644 index 00000000000..11af9895261 --- /dev/null +++ b/app/controllers/autocomplete_controller.rb @@ -0,0 +1,30 @@ +class AutocompleteController < ApplicationController + def users + @users = + if params[:project_id].present? + project = Project.find(params[:project_id]) + + if can?(current_user, :read_project, project) + project.team.users + end + elsif params[:group_id] + group = Group.find(params[:group_id]) + + if can?(current_user, :read_group, group) + group.users + end + else + User.all + end + + @users = @users.search(params[:search]) if params[:search].present? + @users = @users.active + @users = @users.page(params[:page]).per(PER_PAGE) + render json: @users, only: [:name, :username, :id], methods: [:avatar_url] + end + + def user + @user = User.find(params[:id]) + render json: @user, only: [:name, :username, :id], methods: [:avatar_url] + end +end diff --git a/config/routes.rb b/config/routes.rb index c30cd768572..388858d2670 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,11 @@ Gitlab::Application.routes.draw do authorizations: 'oauth/authorizations' end + # Autocomplete + get '/autocomplete/users' => 'autocomplete#users' + get '/autocomplete/users/:id' => 'autocomplete#user' + + # Search get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb new file mode 100644 index 00000000000..a0909cec3bd --- /dev/null +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe AutocompleteController do + let!(:project) { create(:project) } + let!(:user) { create(:user) } + let!(:user2) { create(:user) } + + context 'project members' do + before do + sign_in(user) + project.team << [user, :master] + + get(:users, project_id: project.id) + end + + let(:body) { JSON.parse(response.body) } + + it { body.should be_kind_of(Array) } + it { body.size.should eq(1) } + it { body.first["username"].should == user.username } + end + + context 'group members' do + let(:group) { create(:group) } + + before do + sign_in(user) + group.add_owner(user) + + get(:users, group_id: group.id) + end + + let(:body) { JSON.parse(response.body) } + + it { body.should be_kind_of(Array) } + it { body.size.should eq(1) } + it { body.first["username"].should == user.username } + end + + context 'all users' do + before do + sign_in(user) + get(:users) + end + + let(:body) { JSON.parse(response.body) } + + it { body.should be_kind_of(Array) } + it { body.size.should eq(User.count) } + end +end -- GitLab From 1c2711f7e38a7ca6adb8a0bca5166845405ad5fe Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 26 Mar 2015 19:13:38 -0700 Subject: [PATCH 1567/1609] Refactor UsersSelect to use internal gitlab autocomplete controller --- app/assets/javascripts/users_select.js.coffee | 71 +++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 9eee7406511..f464067686e 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -1,20 +1,48 @@ class @UsersSelect constructor: -> + @usersPath = "/autocomplete/users.json" + @userPath = "/autocomplete/users/:id.json" + $('.ajax-users-select').each (i, select) => + @projectId = $(select).data('project-id') + @groupId = $(select).data('group-id') + showNullUser = $(select).data('null-user') + showAnyUser = $(select).data('any-user') + $(select).select2 placeholder: "Search for a user" multiple: $(select).hasClass('multiselect') minimumInputLength: 0 - query: (query) -> - Api.users query.term, (users) -> + query: (query) => + @users query.term, (users) => data = { results: users } + + if query.term.length == 0 + anyUser = { + name: 'Any', + avatar: null, + username: 'none', + id: null + } + + nullUser = { + name: 'Unassigned', + avatar: null, + username: 'none', + id: 0 + } + + if showNullUser + data.results.unshift(nullUser) + if showAnyUser + data.results.unshift(anyUser) + query.callback(data) - initSelection: (element, callback) -> + initSelection: (element, callback) => id = $(element).val() - if id isnt "" - Api.user(id, callback) - + if id != "" && id != "0" + @user(id, callback) formatResult: (args...) => @formatResult(args...) @@ -38,3 +66,34 @@ class @UsersSelect formatSelection: (user) -> user.name + + user: (user_id, callback) => + url = @buildUrl(@userPath) + url = url.replace(':id', user_id) + + $.ajax( + url: url + dataType: "json" + ).done (user) -> + callback(user) + + # Return users list. Filtered by query + # Only active users retrieved + users: (query, callback) => + url = @buildUrl(@usersPath) + + $.ajax( + url: url + data: + search: query + per_page: 20 + active: true + project_id: @projectId + group_id: @groupId + dataType: "json" + ).done (users) -> + callback(users) + + buildUrl: (url) -> + url = gon.relative_url_root + url if gon.relative_url_root? + return url -- GitLab From d6c8eefb5d0298f0c733ac4880e1e64f2a37b24c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 26 Mar 2015 19:13:49 -0700 Subject: [PATCH 1568/1609] Big refactoring of issues filters * Squash project users selectbox and users selectbox into one class * Move from API autocomplete to GitLab internal one * Smarter filter for project/group/all issues * Use selectbox with searchbox for assignee/author/milestone/label * Switch to ajax filter for issue author/assignee --- app/assets/javascripts/api.js.coffee | 67 --------- app/assets/javascripts/dispatcher.js.coffee | 2 +- .../project_users_select.js.coffee | 56 ------- app/assets/stylesheets/generic/selects.scss | 5 + app/assets/stylesheets/pages/issuable.scss | 8 + app/assets/stylesheets/pages/issues.scss | 5 +- app/helpers/application_helper.rb | 10 +- app/helpers/labels_helper.rb | 5 + app/helpers/milestones_helper.rb | 12 ++ app/helpers/selects_helper.rb | 29 ++-- app/views/projects/_issuable_form.html.haml | 2 +- .../projects/issues/_issue_context.html.haml | 4 +- app/views/projects/issues/index.html.haml | 2 +- app/views/projects/issues/update.js.haml | 2 +- .../merge_requests/_new_submit.html.haml | 2 +- .../merge_requests/show/_context.html.haml | 2 +- .../projects/merge_requests/update.js.haml | 2 +- .../_new_project_member.html.haml | 2 +- app/views/shared/_issuable_filter.html.haml | 141 ++++++------------ 19 files changed, 114 insertions(+), 244 deletions(-) delete mode 100644 app/assets/javascripts/project_users_select.js.coffee diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee index 27d04e7cac6..9e5d594c861 100644 --- a/app/assets/javascripts/api.js.coffee +++ b/app/assets/javascripts/api.js.coffee @@ -1,57 +1,7 @@ @Api = groups_path: "/api/:version/groups.json" group_path: "/api/:version/groups/:id.json" - users_path: "/api/:version/users.json" - user_path: "/api/:version/users/:id.json" - notes_path: "/api/:version/projects/:id/notes.json" namespaces_path: "/api/:version/namespaces.json" - project_users_path: "/api/:version/projects/:id/users.json" - - # Get 20 (depends on api) recent notes - # and sort the ascending from oldest to newest - notes: (project_id, callback) -> - url = Api.buildUrl(Api.notes_path) - url = url.replace(':id', project_id) - - $.ajax( - url: url, - data: - private_token: gon.api_token - gfm: true - recent: true - dataType: "json" - ).done (notes) -> - notes.sort (a, b) -> - return a.id - b.id - callback(notes) - - user: (user_id, callback) -> - url = Api.buildUrl(Api.user_path) - url = url.replace(':id', user_id) - - $.ajax( - url: url - data: - private_token: gon.api_token - dataType: "json" - ).done (user) -> - callback(user) - - # Return users list. Filtered by query - # Only active users retrieved - users: (query, callback) -> - url = Api.buildUrl(Api.users_path) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - active: true - dataType: "json" - ).done (users) -> - callback(users) group: (group_id, callback) -> url = Api.buildUrl(Api.group_path) @@ -80,23 +30,6 @@ ).done (groups) -> callback(groups) - # Return project users list. Filtered by query - # Only active users retrieved - projectUsers: (project_id, query, callback) -> - url = Api.buildUrl(Api.project_users_path) - url = url.replace(':id', project_id) - - $.ajax( - url: url - data: - private_token: gon.api_token - search: query - per_page: 20 - active: true - dataType: "json" - ).done (users) -> - callback(users) - # Return namespaces list. Filtered by query namespaces: (query, callback) -> url = Api.buildUrl(Api.namespaces_path) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 3535d8c2cfc..821712f7512 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -127,7 +127,7 @@ class Dispatcher when 'show' new ProjectShow() when 'issues', 'merge_requests' - new ProjectUsersSelect() + new UsersSelect() when 'wikis' new Wikis() shortcut_handler = new ShortcutsNavigation() diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee deleted file mode 100644 index 80ab1a61ab9..00000000000 --- a/app/assets/javascripts/project_users_select.js.coffee +++ /dev/null @@ -1,56 +0,0 @@ -class @ProjectUsersSelect - constructor: -> - $('.ajax-project-users-select').each (i, select) => - project_id = $(select).data('project-id') || $('body').data('project-id') - - $(select).select2 - placeholder: $(select).data('placeholder') || "Search for a user" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.projectUsers project_id, query.term, (users) -> - data = { results: users } - - if query.term.length == 0 - nullUser = { - name: 'Unassigned', - avatar: null, - username: 'none', - id: -1 - } - - data.results.unshift(nullUser) - - query.callback(data) - - initSelection: (element, callback) -> - id = $(element).val() - if id != "" && id != "-1" - Api.user(id, callback) - - - formatResult: (args...) => - @formatResult(args...) - formatSelection: (args...) => - @formatSelection(args...) - dropdownCssClass: "ajax-project-users-dropdown" - dropdownAutoWidth: true - escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results - m - - formatResult: (user) -> - if user.avatar_url - avatar = user.avatar_url - else - avatar = gon.default_avatar_url - - avatarMarkup = "
        " - - "
        - #{avatarMarkup} -
        #{user.name}
        -
        #{user.username}
        -
        " - - formatSelection: (user) -> - user.name diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index 7557f411111..69613608c82 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -28,6 +28,7 @@ .select2-drop-active { border: 1px solid #BBB !important; margin-top: 4px; + font-size: 13px; &.select2-drop-above { margin-bottom: 8px; @@ -106,3 +107,7 @@ font-weight: bolder; } } + +.ajax-users-dropdown { + min-width: 225px !important; +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index a640a4e2051..13e09d5596f 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -45,3 +45,11 @@ .btn { font-size: 13px; } } + +.filter-item { + margin-right: 15px; + + > span { + margin-right: 4px; + } +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 6c1dd4f7e9f..55e648a568f 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -60,6 +60,7 @@ } @media (min-width: 800px) { + .issues-filters, .issues_bulk_update { select, .select2-container { width: 120px !important; @@ -69,14 +70,16 @@ } @media (min-width: 1200px) { + .issues-filters, .issues_bulk_update { select, .select2-container { - width: 160px !important; + width: 140px !important; display: inline-block; } } } +.issues-filters, .issues_bulk_update { .select2-container .select2-choice { color: #444 !important; diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 38b5fc4a011..3f3509bb18a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -275,7 +275,9 @@ module ApplicationHelper 'https://' + promo_host end - def page_filter_path(options={}) + def page_filter_path(options = {}) + without = options.delete(:without) + exist_opts = { state: params[:state], scope: params[:scope], @@ -288,6 +290,12 @@ module ApplicationHelper options = exist_opts.merge(options) + if without.present? + without.each do |key| + options.delete(key) + end + end + path = request.path path << "?#{options.to_param}" path diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 49063491abf..aa98ead43f1 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -47,4 +47,9 @@ module LabelsHelper "#FFF" end end + + def project_labels_options(project) + options_for_select([['Any', nil]]) + + options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) + end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 59fdc0d49cc..e1dec3ec628 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -19,4 +19,16 @@ module MilestonesHelper content_tag :div, nil, options end end + + def projects_milestones_options + milestones = + if @project + @project.milestones + else + Milestone.where(project_id: @projects) + end.active + + options_for_select([['Any', nil]]) + + options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) + end end diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 796d805f219..457cd3fa46b 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -4,18 +4,27 @@ module SelectsHelper css_class << "multiselect " if opts[:multiple] css_class << (opts[:class] || '') value = opts[:selected] || '' + placeholder = opts[:placeholder] || 'Search for a user' - hidden_field_tag(id, value, class: css_class) - end + null_user = opts[:null_user] || false + any_user = opts[:any_user] || false - def project_users_select_tag(id, opts = {}) - css_class = "ajax-project-users-select " - css_class << "multiselect " if opts[:multiple] - css_class << (opts[:class] || '') - value = opts[:selected] || '' - placeholder = opts[:placeholder] || 'Select user' - project_id = opts[:project_id] || @project.id - hidden_field_tag(id, value, class: css_class, 'data-placeholder' => placeholder, 'data-project-id' => project_id) + html = { + class: css_class, + 'data-placeholder' => placeholder, + 'data-null-user' => null_user, + 'data-any-user' => any_user, + } + + unless opts[:scope] == :all + if @project + html['data-project-id'] = @project.id + elsif @group + html['data-group-id'] = @group.id + end + end + + hidden_field_tag(id, value, html) end def groups_select_tag(id, opts = {}) diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 7fd5fe8a6e1..0cb1f913cbd 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -35,7 +35,7 @@ %i.fa.fa-user Assign to .col-sm-10 - = project_users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", + = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", placeholder: 'Select a user', class: 'custom-form-control', selected: issuable.assignee_id)   diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml index c3d6dc2e50b..52e38050419 100644 --- a/app/views/projects/issues/_issue_context.html.haml +++ b/app/views/projects/issues/_issue_context.html.haml @@ -8,7 +8,7 @@ - else none - if can?(current_user, :modify_issue, @issue) - = project_users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id) + = users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id, null_user: true) %div.prepend-top-20.clearfix .issuable-context-title @@ -44,5 +44,3 @@ :coffeescript new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}") - - diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 54e3009cca2..210b77a6b15 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -19,7 +19,7 @@ .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') - = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee') + = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true) = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml index 82c0e653759..1d38662bff8 100644 --- a/app/views/projects/issues/update.js.haml +++ b/app/views/projects/issues/update.js.haml @@ -13,5 +13,5 @@ $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}) $('.edit-issue.inline-update input[type="submit"]').hide(); -new ProjectUsersSelect(); +new UsersSelect() new Issue(); diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 1d8eef4e8ce..d986ce67c0c 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -39,7 +39,7 @@ %i.fa.fa-user Assign to .col-sm-10 - = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id) + = users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id)   = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml index 80e5c223d60..105562fb05e 100644 --- a/app/views/projects/merge_requests/show/_context.html.haml +++ b/app/views/projects/merge_requests/show/_context.html.haml @@ -9,7 +9,7 @@ none .issuable-context-selectbox - if can?(current_user, :modify_merge_request, @merge_request) - = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id) + = users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id, null_user: true) %div.prepend-top-20.clearfix .issuable-context-title diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml index f5cc98c7fa4..b4df1d20737 100644 --- a/app/views/projects/merge_requests/update.js.haml +++ b/app/views/projects/merge_requests/update.js.haml @@ -2,7 +2,7 @@ $('.context').html("#{escape_javascript(render partial: 'projects/merge_requests/show/context', locals: { issue: @issue })}"); $('.context').effect('highlight'); - new ProjectUsersSelect(); + new UsersSelect() $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true}); merge_request = new MergeRequest(); diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 0f824bdabf8..5daae2708e6 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -1,7 +1,7 @@ = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f| .form-group = f.label :user_ids, "People", class: 'control-label' - .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large') + .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all) .form-group = f.label :access_level, "Project Access", class: 'control-label' diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml index 5412b9ef0f4..686a3389bb4 100644 --- a/app/views/shared/_issuable_filter.html.haml +++ b/app/views/shared/_issuable_filter.html.haml @@ -15,105 +15,50 @@ All %div - - if controller.controller_name == 'issues' - .check-all-holder - = check_box_tag "check_all_issues", nil, false, - class: "check_all_issues left", - disabled: !can?(current_user, :modify_issue, @project) - .issues-other-filters - .dropdown.inline.assignee-filter - %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light assignee: - - if @assignee.present? - %strong= @assignee.name - - elsif params[:assignee_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(assignee_id: nil) do - Any - = link_to page_filter_path(assignee_id: 0) do - Unassigned - - @assignees.sort_by(&:name).each do |user| - %li - = link_to page_filter_path(assignee_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_id, :label_name]), method: :get, class: 'filter-form' do + - if controller.controller_name == 'issues' + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left", + disabled: !can?(current_user, :modify_issue, @project) + .issues-other-filters + .filter-item.inline + %span.light + %i.fa.fa-user + Assignee + %strong + = users_select_tag(:assignee_id, selected: params[:assignee_id], + placeholder: 'Any', class: 'trigger-submit', any_user: true, null_user: true) - .dropdown.inline.prepend-left-10.author-filter - %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"} - %i.fa.fa-user - %span.light author: - - if @author.present? - %strong= @author.name - - elsif params[:author_id] == "0" - Unassigned - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(author_id: nil) do - Any - = link_to page_filter_path(author_id: 0) do - Unassigned - - @authors.sort_by(&:name).each do |user| - %li - = link_to page_filter_path(author_id: user.id) do - = image_tag avatar_icon(user.email), class: "avatar s16", alt: '' - = user.name + .filter-item.inline + %span.light + %i.fa.fa-user + Author + %strong + = users_select_tag(:author_id, selected: params[:author_id], + placeholder: 'Any', class: 'trigger-submit', any_user: true) - .dropdown.inline.prepend-left-10.milestone-filter - %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"} - %i.fa.fa-clock-o - %span.light milestone: - - if @milestone.present? - %strong= @milestone.title - - elsif params[:milestone_id] == "0" - None (backlog) - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(milestone_id: nil) do - Any - = link_to page_filter_path(milestone_id: 0) do - None (backlog) - - @milestones.each do |milestone| - %li - = link_to page_filter_path(milestone_id: milestone.id) do - %strong= milestone.title - %small.light= milestone.expires_at + .filter-item.inline + %span.light + %i.fa.fa-clock-o + Milestone + %strong + = select_tag('milestone_id', projects_milestones_options, class: "select2 trigger-submit") - - if @project - .dropdown.inline.prepend-left-10.labels-filter - %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"} - %i.fa.fa-tags - %span.light label: - - if params[:label_name].present? - %strong= params[:label_name] - - else - Any - %b.caret - %ul.dropdown-menu - %li - = link_to page_filter_path(label_name: nil) do - Any - - if @project.labels.any? - - @project.labels.each do |label| - %li - = link_to page_filter_path(label_name: label.name) do - = render_colored_label(label) - - else - %li - = link_to generate_namespace_project_labels_path(@project.namespace, @project, redirect: request.original_url), method: :post do - %i.fa.fa-plus-circle - Create default labels + - if @project + .filter-item.inline + %span.light + %i.fa.fa-tag + Label + %strong + = select_tag('label_name', project_labels_options(@project), class: "select2 trigger-submit") - .pull-right - = render 'shared/sort_dropdown' + .pull-right + = render 'shared/sort_dropdown' + +:coffeescript + new UsersSelect() + + $('form.filter-form').on 'submit', (event) -> + event.preventDefault() + Turbolinks.visit @.action + '&' + $(@).serialize() -- GitLab From 79bd8ca9895e030b20bbb7756e1583486c908386 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 27 Mar 2015 07:06:38 +0000 Subject: [PATCH 1569/1609] Changed button type --- app/helpers/projects_helper.rb | 2 +- app/views/projects/_home_panel.html.haml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index f535b01b2ec..3199ff3b99a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -81,7 +81,7 @@ module ProjectsHelper end def link_to_toggle_star(title, starred, signed_in) - cls = 'star-btn btn btn-primary' + cls = 'star-btn btn btn-default' cls << ' disabled' unless signed_in toggle_html = content_tag('span', class: 'toggle') do diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 3bb70458b16..599afe6e779 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -28,10 +28,10 @@ .fork-buttons - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-primary' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-default' do = link_to_toggle_fork - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-primary' do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-default' do = link_to_toggle_fork - unless @project.empty_repo? - if can? current_user, :download_code, @project -- GitLab From c1c93f4f7a51760660ea2e1994071e63e6793808 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 27 Mar 2015 00:27:51 -0700 Subject: [PATCH 1570/1609] Fix tests and unassigned filter for issues. Updated CHANGELOG --- CHANGELOG | 3 +++ app/finders/issuable_finder.rb | 2 +- app/views/projects/_issuable_form.html.haml | 2 +- features/dashboard/issues.feature | 2 ++ features/dashboard/merge_requests.feature | 2 ++ features/project/issues/filter_labels.feature | 6 +---- features/steps/dashboard/issues.rb | 17 ++++---------- features/steps/dashboard/merge_requests.rb | 17 ++++---------- .../steps/project/issues/filter_labels.rb | 23 ++----------------- spec/features/issues_spec.rb | 4 ++-- spec/support/select2_helper.rb | 4 ++-- 11 files changed, 26 insertions(+), 56 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 242d2c773c6..06b7413e616 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -43,6 +43,9 @@ v 7.10.0 (unreleased) - Link note avatar to user. - Make Git-over-SSH errors more descriptive. - Fix EmailsOnPush. + - Refactor issue filtering + - AJAX selectbox for issue assignee and author filters + - Fix issue with missing options in issue filtering dropdown if selected one v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 67939c094c5..2c0702073d4 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -19,7 +19,7 @@ require_relative 'projects_finder' class IssuableFinder - NONE = 0 + NONE = '0' attr_accessor :current_user, :params diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 0cb1f913cbd..e321a84974e 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -36,7 +36,7 @@ Assign to .col-sm-10 = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select a user', class: 'custom-form-control', + placeholder: 'Select a user', class: 'custom-form-control', null_user: true, selected: issuable.assignee_id)   = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' diff --git a/features/dashboard/issues.feature b/features/dashboard/issues.feature index 72627e43e05..99dad88a402 100644 --- a/features/dashboard/issues.feature +++ b/features/dashboard/issues.feature @@ -10,10 +10,12 @@ Feature: Dashboard Issues Scenario: I should see assigned issues Then I should see issues assigned to me + @javascript Scenario: I should see authored issues When I click "Authored by me" link Then I should see issues authored by me + @javascript Scenario: I should see all issues When I click "All" link Then I should see all issues diff --git a/features/dashboard/merge_requests.feature b/features/dashboard/merge_requests.feature index dcef1290e7e..4a2c997d707 100644 --- a/features/dashboard/merge_requests.feature +++ b/features/dashboard/merge_requests.feature @@ -10,10 +10,12 @@ Feature: Dashboard Merge Requests Scenario: I should see assigned merge_requests Then I should see merge requests assigned to me + @javascript Scenario: I should see authored merge_requests When I click "Authored by me" link Then I should see merge requests authored by me + @javascript Scenario: I should see all merge_requests When I click "All" link Then I should see all merge requests diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature index 2c69a78a749..e316f519861 100644 --- a/features/project/issues/filter_labels.feature +++ b/features/project/issues/filter_labels.feature @@ -8,11 +8,7 @@ Feature: Project Issues Filter Labels And project "Shop" has issue "Feature1" with labels: "feature" Given I visit project "Shop" issues page - Scenario: I should see project issues - Then I should see "bug" in labels filter - And I should see "feature" in labels filter - And I should see "enhancement" in labels filter - + @javascript Scenario: I filter by one label Given I click link "bug" Then I should see "Bugfix1" in issues list diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index b77113e3974..60da36e86de 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -1,6 +1,7 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps include SharedAuthentication include SharedPaths + include Select2Helper step 'I should see issues assigned to me' do should_see(assigned_issue) @@ -35,21 +36,13 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps end step 'I click "Authored by me" link' do - within ".assignee-filter" do - click_link "Any" - end - within ".author-filter" do - click_link current_user.name - end + select2(current_user.id, from: "#author_id") + select2(nil, from: "#assignee_id") end step 'I click "All" link' do - within ".author-filter" do - click_link "Any" - end - within ".assignee-filter" do - click_link "Any" - end + select2(nil, from: "#author_id") + select2(nil, from: "#assignee_id") end def should_see(issue) diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index 6261c89924c..9d92082bb83 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -1,6 +1,7 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps include SharedAuthentication include SharedPaths + include Select2Helper step 'I should see merge requests assigned to me' do should_see(assigned_merge_request) @@ -39,21 +40,13 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps end step 'I click "Authored by me" link' do - within ".assignee-filter" do - click_link "Any" - end - within ".author-filter" do - click_link current_user.name - end + select2(current_user.id, from: "#author_id") + select2(nil, from: "#assignee_id") end step 'I click "All" link' do - within ".author-filter" do - click_link "Any" - end - within ".assignee-filter" do - click_link "Any" - end + select2(nil, from: "#author_id") + select2(nil, from: "#assignee_id") end def should_see(merge_request) diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb index e62fa9c84c8..5740bd12837 100644 --- a/features/steps/project/issues/filter_labels.rb +++ b/features/steps/project/issues/filter_labels.rb @@ -2,24 +2,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps include SharedAuthentication include SharedProject include SharedPaths - - step 'I should see "bug" in labels filter' do - within ".labels-filter" do - page.should have_content "bug" - end - end - - step 'I should see "feature" in labels filter' do - within ".labels-filter" do - page.should have_content "feature" - end - end - - step 'I should see "enhancement" in labels filter' do - within ".labels-filter" do - page.should have_content "enhancement" - end - end + include Select2Helper step 'I should see "Bugfix1" in issues list' do within ".issues-list" do @@ -46,9 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps end step 'I click link "bug"' do - within ".labels-filter" do - click_link "bug" - end + select2('bug', from: "#label_name") end step 'I click link "feature"' do diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index a2db57ad908..e5f33d5a25a 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -95,7 +95,7 @@ describe 'Issues', feature: true do let(:issue) { @issue } it 'should allow filtering by issues with no specified milestone' do - visit namespace_project_issues_path(project.namespace, project, milestone_id: '0') + visit namespace_project_issues_path(project.namespace, project, milestone_id: IssuableFinder::NONE) expect(page).not_to have_content 'foobar' expect(page).to have_content 'barbaz' @@ -111,7 +111,7 @@ describe 'Issues', feature: true do end it 'should allow filtering by issues with no specified assignee' do - visit namespace_project_issues_path(project.namespace, project, assignee_id: '0') + visit namespace_project_issues_path(project.namespace, project, assignee_id: IssuableFinder::NONE) expect(page).to have_content 'foobar' expect(page).not_to have_content 'barbaz' diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb index c7cf109a7bb..691f84f39d4 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/select2_helper.rb @@ -17,9 +17,9 @@ module Select2Helper selector = options[:from] if options[:multiple] - execute_script("$('#{selector}').select2('val', ['#{value}']);") + execute_script("$('#{selector}').select2('val', ['#{value}'], true);") else - execute_script("$('#{selector}').select2('val', '#{value}');") + execute_script("$('#{selector}').select2('val', '#{value}', true);") end end end -- GitLab From 351e61f4b27f287778cf778a41f1a4e4cef977e2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 27 Mar 2015 12:30:21 +0100 Subject: [PATCH 1571/1609] Prevent note form from being cleared when submitting failed. --- CHANGELOG | 1 + app/assets/javascripts/notes.js.coffee | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 06b7413e616..3a5749b5e36 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -46,6 +46,7 @@ v 7.10.0 (unreleased) - Refactor issue filtering - AJAX selectbox for issue assignee and author filters - Fix issue with missing options in issue filtering dropdown if selected one + - Prevent note form from being cleared when submitting failed. v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index c366c98cf54..dc43a06dbe7 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -37,7 +37,8 @@ class @Notes $(document).on "click", ".js-note-attachment-delete", @removeAttachment # reset main target form after submit - $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm + $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton + $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm # update the file name when an attachment is selected $(document).on "change", ".js-note-attachment-input", @updateFormAttachment @@ -70,6 +71,7 @@ class @Notes $(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-attachment-delete" $(document).off "ajax:complete", ".js-main-target-form" + $(document).off "ajax:success", ".js-main-target-form" $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" @@ -169,6 +171,11 @@ class @Notes form.find(".js-note-text").data("autosave").reset() + reenableTargetFormSubmitButton: -> + form = $(".js-main-target-form") + + form.find(".js-note-text").trigger "input" + ### Shows the main form and does some setup on it. -- GitLab From 7a70fb123c0a21e1180665c9b28b8483d2c66a3e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 27 Mar 2015 11:00:54 +0100 Subject: [PATCH 1572/1609] Prevent holding Control-Enter or Command-Enter from posting comment multiple times. --- CHANGELOG | 1 + app/assets/javascripts/notes.js.coffee | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 06b7413e616..04d889456dd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -46,6 +46,7 @@ v 7.10.0 (unreleased) - Refactor issue filtering - AJAX selectbox for issue assignee and author filters - Fix issue with missing options in issue filtering dropdown if selected one + - Prevent holding Control-Enter or Command-Enter from posting comment multiple times. v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index c366c98cf54..b61c4dd6544 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -57,6 +57,7 @@ class @Notes @notes_forms = '.js-main-target-form textarea, .js-discussion-note-form textarea' # Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown. $(document).on('keydown', @notes_forms, (e) -> + return if e.originalEvent.repeat if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13) $(@).parents('form').submit() ) -- GitLab From bfc36fff1134c0a0435b54520021d07af3324430 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 27 Mar 2015 09:25:35 -0700 Subject: [PATCH 1573/1609] We've moved external trackers to service, remove from gitlab.yml. --- config/gitlab.yml.example | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 11278ef40dc..3f1ca34a667 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -86,35 +86,6 @@ production: &base # The default is 'tmp/repositories' relative to the root of the Rails app. # repository_downloads_path: tmp/repositories - ## External issues trackers - issues_tracker: - # redmine: - # title: "Redmine" - # ## If not nil, link 'Issues' on project page will be replaced with this - # ## Use placeholders: - # ## :project_id - GitLab project identifier - # ## :issues_tracker_id - Project Name or Id in external issue tracker - # project_url: "http://redmine.sample/projects/:issues_tracker_id" - # - # ## If not nil, links from /#\d/ entities from commit messages will replaced with this - # ## Use placeholders: - # ## :project_id - GitLab project identifier - # ## :issues_tracker_id - Project Name or Id in external issue tracker - # ## :id - Issue id (from commit messages) - # issues_url: "http://redmine.sample/issues/:id" - # - # ## If not nil, links to creating new issues will be replaced with this - # ## Use placeholders: - # ## :project_id - GitLab project identifier - # ## :issues_tracker_id - Project Name or Id in external issue tracker - # new_issue_url: "http://redmine.sample/projects/:issues_tracker_id/issues/new" - # - # jira: - # title: "Atlassian Jira" - # project_url: "http://jira.sample/issues/?jql=project=:issues_tracker_id" - # issues_url: "http://jira.sample/browse/:id" - # new_issue_url: "http://jira.sample/secure/CreateIssue.jspa" - ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: -- GitLab From 70496fe5d840eeb2be08556008a0595b4c808e4b Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 27 Mar 2015 12:00:31 -0700 Subject: [PATCH 1574/1609] Improve UI for issues filters --- app/assets/stylesheets/generic/filters.scss | 25 +++++++++++++++ app/assets/stylesheets/pages/issuable.scss | 8 ----- app/assets/stylesheets/pages/issues.scss | 9 ++---- app/helpers/labels_helper.rb | 3 +- app/helpers/milestones_helper.rb | 3 +- app/views/shared/_issuable_filter.html.haml | 34 ++++++--------------- 6 files changed, 39 insertions(+), 43 deletions(-) create mode 100644 app/assets/stylesheets/generic/filters.scss diff --git a/app/assets/stylesheets/generic/filters.scss b/app/assets/stylesheets/generic/filters.scss new file mode 100644 index 00000000000..20c6a85de98 --- /dev/null +++ b/app/assets/stylesheets/generic/filters.scss @@ -0,0 +1,25 @@ +.filter-item { + margin-right: 15px; +} + +.issues-state-filters { + li.active a, + li.active a:hover { + background: #f5f5f5; + border-bottom: 1px solid #f5f5f5 !important; + } +} + +.issues-details-filters { + font-size: 13px; + background: #f5f5f5; + margin: -10px 0; + padding: 10px 15px; + margin-top: -15px; + border-left: 1px solid #DDD; + border-right: 1px solid #DDD; + + .btn { + font-size: 13px; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 13e09d5596f..a640a4e2051 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -45,11 +45,3 @@ .btn { font-size: 13px; } } - -.filter-item { - margin-right: 15px; - - > span { - margin-right: 4px; - } -} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 55e648a568f..b8ad26d69cd 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -41,12 +41,9 @@ } .check-all-holder { - height: 36px; + line-height: 36px; float: left; - margin-right: 12px; - padding: 6px 15px; - border: 1px solid #ccc; - @include border-radius(4px); + margin-right: 15px; } .issues_content { @@ -73,7 +70,7 @@ .issues-filters, .issues_bulk_update { select, .select2-container { - width: 140px !important; + width: 150px !important; display: inline-block; } } diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index aa98ead43f1..32ef2e7ca84 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -49,7 +49,6 @@ module LabelsHelper end def project_labels_options(project) - options_for_select([['Any', nil]]) + - options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) + options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name]) end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index e1dec3ec628..282bdf744d2 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -28,7 +28,6 @@ module MilestonesHelper Milestone.where(project_id: @projects) end.active - options_for_select([['Any', nil]]) + - options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) + options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id]) end end diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml index 686a3389bb4..77085ccb56b 100644 --- a/app/views/shared/_issuable_filter.html.haml +++ b/app/views/shared/_issuable_filter.html.haml @@ -14,7 +14,7 @@ %i.fa.fa-compass All - %div + .issues-details-filters = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_id, :label_name]), method: :get, class: 'filter-form' do - if controller.controller_name == 'issues' .check-all-holder @@ -23,35 +23,19 @@ disabled: !can?(current_user, :modify_issue, @project) .issues-other-filters .filter-item.inline - %span.light - %i.fa.fa-user - Assignee - %strong - = users_select_tag(:assignee_id, selected: params[:assignee_id], - placeholder: 'Any', class: 'trigger-submit', any_user: true, null_user: true) + = users_select_tag(:assignee_id, selected: params[:assignee_id], + placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true) .filter-item.inline - %span.light - %i.fa.fa-user - Author - %strong - = users_select_tag(:author_id, selected: params[:author_id], - placeholder: 'Any', class: 'trigger-submit', any_user: true) + = users_select_tag(:author_id, selected: params[:author_id], + placeholder: 'Author', class: 'trigger-submit', any_user: true) - .filter-item.inline - %span.light - %i.fa.fa-clock-o - Milestone - %strong - = select_tag('milestone_id', projects_milestones_options, class: "select2 trigger-submit") + .filter-item.inline.milestone-filter + = select_tag('milestone_id', projects_milestones_options, class: "select2 trigger-submit", prompt: 'Milestone') - if @project - .filter-item.inline - %span.light - %i.fa.fa-tag - Label - %strong - = select_tag('label_name', project_labels_options(@project), class: "select2 trigger-submit") + .filter-item.inline.labels-filter + = select_tag('label_name', project_labels_options(@project), class: "select2 trigger-submit", prompt: 'Label') .pull-right = render 'shared/sort_dropdown' -- GitLab From 65c4dc19f77286374cbc93aa1e18dea8cd00dea5 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 3 Feb 2015 20:13:06 -0500 Subject: [PATCH 1575/1609] Simplify toggle-nav-collapse JS --- app/assets/javascripts/sidebar.js.coffee | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 7febcba0e94..2e3f5608257 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -3,12 +3,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) -> collapsed = 'page-sidebar-collapsed' expanded = 'page-sidebar-expanded' - if $('.page-with-sidebar').hasClass(collapsed) - $('.page-with-sidebar').removeClass(collapsed).addClass(expanded) - $('.toggle-nav-collapse i').removeClass('fa-angle-right').addClass('fa-angle-left') - $.cookie("collapsed_nav", "false", { path: '/' }) - else - $('.page-with-sidebar').removeClass(expanded).addClass(collapsed) - $('.toggle-nav-collapse i').removeClass('fa-angle-left').addClass('fa-angle-right') - $.cookie("collapsed_nav", "true", { path: '/' }) + $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") + $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") + $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) ) -- GitLab From 190e08979c25177e47a4055bc9b59e58bfcf3134 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 27 Mar 2015 15:31:16 -0400 Subject: [PATCH 1576/1609] Add ActiveRecord::Migration.maintain_test_schema! to spec_helper New in Rails 4.1, this eliminates spec failures due to forgetting to run `db:test:prepare`. --- spec/spec_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index eaec2198dc8..53ccaa4fd67 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,3 +44,5 @@ RSpec.configure do |config| TestEnv.init end end + +ActiveRecord::Migration.maintain_test_schema! -- GitLab From 83064492a0281f44f4e3e5726a9a92183c06330a Mon Sep 17 00:00:00 2001 From: Steven Burgart Date: Fri, 27 Mar 2015 15:50:54 -0400 Subject: [PATCH 1577/1609] Change merging notification to sound more natural --- app/views/projects/merge_requests/show/_state_widget.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml index a4f2a890969..44bd9347f51 100644 --- a/app/views/projects/merge_requests/show/_state_widget.html.haml +++ b/app/views/projects/merge_requests/show/_state_widget.html.haml @@ -29,7 +29,7 @@ %h4 Merge in progress... %p - GitLab tries to merge it right now. During this time merge request is locked and can not be closed. + Merging is in progress. While merging this request is locked and cannot be closed. - unless @commits.any? %h4 Nothing to merge -- GitLab From 6664da431aaa9f9a521323004420bcf700af4306 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 27 Mar 2015 14:28:24 -0700 Subject: [PATCH 1578/1609] Small css improvements to selectboxes and issue filter --- app/assets/stylesheets/generic/filters.scss | 38 ++++++++++++++++++--- app/assets/stylesheets/generic/selects.scss | 13 ++++--- app/assets/stylesheets/pages/issues.scss | 31 ++--------------- app/views/projects/issues/index.html.haml | 10 ------ app/views/shared/_issuable_filter.html.haml | 10 ++++++ 5 files changed, 55 insertions(+), 47 deletions(-) diff --git a/app/assets/stylesheets/generic/filters.scss b/app/assets/stylesheets/generic/filters.scss index 20c6a85de98..bd93a79722d 100644 --- a/app/assets/stylesheets/generic/filters.scss +++ b/app/assets/stylesheets/generic/filters.scss @@ -3,10 +3,13 @@ } .issues-state-filters { - li.active a, - li.active a:hover { - background: #f5f5f5; - border-bottom: 1px solid #f5f5f5 !important; + li.active a { + border-color: #DDD !important; + + &, &:hover, &:active, &.active { + background: #f5f5f5 !important; + border-bottom: 1px solid #f5f5f5 !important; + } } } @@ -23,3 +26,30 @@ font-size: 13px; } } + +@media (min-width: 800px) { + .issues-filters, + .issues_bulk_update { + select, .select2-container { + width: 120px !important; + display: inline-block; + } + } +} + +@media (min-width: 1200px) { + .issues-filters, + .issues_bulk_update { + select, .select2-container { + width: 150px !important; + display: inline-block; + } + } +} + +.issues-filters, +.issues_bulk_update { + .select2-container .select2-choice { + color: #444 !important; + } +} diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss index 69613608c82..d8e0dc028d1 100644 --- a/app/assets/stylesheets/generic/selects.scss +++ b/app/assets/stylesheets/generic/selects.scss @@ -2,20 +2,25 @@ .select2-container, .select2-container.select2-drop-above { .select2-choice { background: #FFF; - border-color: #CCC; + border-color: #DDD; + height: 34px; padding: 6px 14px; + font-size: 14px; line-height: 1.42857143; - height: auto; + + @include border-radius(4px); .select2-arrow { background: #FFF; - border-left: 1px solid #DDD; + border-left: none; + padding-top: 3px; } } } .select2-container-multi .select2-choices { - @include border-radius(4px) + @include border-radius(4px); + border-color: #CCC; } .select2-container-multi .select2-choices .select2-search-field input { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index b8ad26d69cd..cd86a9be8b2 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -56,33 +56,6 @@ } } -@media (min-width: 800px) { - .issues-filters, - .issues_bulk_update { - select, .select2-container { - width: 120px !important; - display: inline-block; - } - } -} - -@media (min-width: 1200px) { - .issues-filters, - .issues_bulk_update { - select, .select2-container { - width: 150px !important; - display: inline-block; - } - } -} - -.issues-filters, -.issues_bulk_update { - .select2-container .select2-choice { - color: #444 !important; - } -} - .participants { margin-bottom: 20px; } @@ -120,12 +93,12 @@ form.edit-issue { } &.closed { - background: #F5f5f5; + background: #F9F9F9; border-color: #E5E5E5; } &.merged { - background: #F5f5f5; + background: #F9F9F9; border-color: #E5E5E5; } } diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 210b77a6b15..d3c7ae24a75 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -15,15 +15,5 @@ = render 'shared/issuable_filter' - .clearfix - .issues_bulk_update.hide - = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do - = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') - = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true) - = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") - = hidden_field_tag 'update[issues_ids]', [] - = hidden_field_tag :state_event, params[:state_event] - = button_tag "Update issues", class: "btn update_selected_issues btn-save" - .issues-holder = render "issues" diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml index 77085ccb56b..f169733f2e9 100644 --- a/app/views/shared/_issuable_filter.html.haml +++ b/app/views/shared/_issuable_filter.html.haml @@ -40,6 +40,16 @@ .pull-right = render 'shared/sort_dropdown' + - if controller.controller_name == 'issues' + .issues_bulk_update.hide + = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do + = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') + = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true) + = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") + = hidden_field_tag 'update[issues_ids]', [] + = hidden_field_tag :state_event, params[:state_event] + = button_tag "Update issues", class: "btn update_selected_issues btn-save" + :coffeescript new UsersSelect() -- GitLab From 0f1d8e771f3115c12af9ca6e24ea57b2f403b826 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 27 Mar 2015 16:39:41 -0400 Subject: [PATCH 1579/1609] Define GIT_TEMPLATE_DIR environment variable in TestEnv See http://schacon.github.io/git/git-init.html#_template_directory Without this variable, any global git hooks a developer might have in ~/.git_template would be linked in the `.git/hooks` folder for every test repository that gets checked out by TestEnv, and would cause certain specs to fail due to pre-existing hook files. --- spec/support/test_env.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index f869488d8d8..44d70e741b2 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -85,7 +85,7 @@ module TestEnv end # We must copy bare repositories because we will push to them. - system(*%W(git clone -q --bare #{factory_repo_path} #{factory_repo_path_bare})) + system(git_env, *%W(git clone -q --bare #{factory_repo_path} #{factory_repo_path_bare})) end def copy_repo(project) @@ -113,4 +113,10 @@ module TestEnv def factory_repo_name 'gitlab-test' end + + # Prevent developer git configurations from being persisted to test + # repositories + def git_env + {'GIT_TEMPLATE_DIR' => ''} + end end -- GitLab From 32d6a14098551b2b8d8210c8c388ec471dc9c5c4 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 27 Mar 2015 22:53:27 -0400 Subject: [PATCH 1580/1609] Move asana_service_spec to its correct location --- spec/models/{ => project_services}/asana_service_spec.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/models/{ => project_services}/asana_service_spec.rb (100%) diff --git a/spec/models/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb similarity index 100% rename from spec/models/asana_service_spec.rb rename to spec/models/project_services/asana_service_spec.rb -- GitLab From fabaeb096fad0307308c9af3b617a5c7a69734e6 Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Sat, 28 Mar 2015 09:07:25 -0600 Subject: [PATCH 1581/1609] Upgrade gitlab_git gem to version 7.1.3 --- CHANGELOG | 1 + Gemfile | 2 +- Gemfile.lock | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c7c1ae36b2b..fe499e7aa93 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ v 7.10.0 (unreleased) - Improve diff UI - Fix alignment of navbar toggle button (Cody Mize) - Identical look of selectboxes in UI + - Upgrade the gitlab_git gem to version 7.1.3 - Move "Import existing repository by URL" option to button. - Improve error message when save profile has error. - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) diff --git a/Gemfile b/Gemfile index 285ccf32b66..f43d5bad1a6 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.1.2' +gem "gitlab_git", '~> 7.1.3' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' diff --git a/Gemfile.lock b/Gemfile.lock index 80eebc16e4c..a608c70d48d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -212,7 +212,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.1.2) + gitlab_git (7.1.3) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -698,7 +698,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.1.2) + gitlab_git (~> 7.1.3) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) -- GitLab From 8ef7220c2316810e2a9bd784f22d25b4a073c138 Mon Sep 17 00:00:00 2001 From: Sullivan SENECHAL Date: Sat, 4 Oct 2014 12:29:18 +0200 Subject: [PATCH 1582/1609] Improve file icons rendering on tree --- CHANGELOG | 1 + app/helpers/blob_helper.rb | 8 ++ app/helpers/icons_helper.rb | 44 +++++++ app/helpers/tree_helper.rb | 9 +- app/views/projects/blob/_blob.html.haml | 2 +- app/views/projects/tree/_blob_item.html.haml | 2 +- .../projects/tree/_submodule_item.html.haml | 2 +- app/views/projects/tree/_tree_item.html.haml | 2 +- spec/helpers/icons_helper_spec.rb | 109 ++++++++++++++++++ 9 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 spec/helpers/icons_helper_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 04d889456dd..d961e3db12b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,6 +47,7 @@ v 7.10.0 (unreleased) - AJAX selectbox for issue assignee and author filters - Fix issue with missing options in issue filtering dropdown if selected one - Prevent holding Control-Enter or Command-Enter from posting comment multiple times. + - Improve file icons rendering on tree (Sullivan Sénéchal) v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 798d62b3a09..4ea838ca447 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -61,4 +61,12 @@ module BlobHelper 'Preview changes' end end + + # Return an image icon depending on the file mode and extension + # + # mode - File unix mode + # mode - File name + def blob_icon(mode, name) + icon("#{file_type_icon_class('file', mode, name)} fw") + end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 18260f0ed4d..187e21832f0 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -36,4 +36,48 @@ module IconsHelper def private_icon icon('lock') end + + def file_type_icon_class(type, mode, name) + if type == 'folder' + icon_class = 'folder' + elsif mode == 0120000 + icon_class = 'share' + else + # Guess which icon to choose based on file extension. + # If you think a file extension is missing, feel free to add it on PR + + case File.extname(name).downcase + when '.pdf' + icon_class = 'file-pdf-o' + when '.jpg', '.jpeg', '.jif', '.jfif', + '.jp2', '.jpx', '.j2k', '.j2c', + '.png', '.gif', '.tif', '.tiff', + '.svg', '.ico', '.bmp' + icon_class = 'file-image-o' + when '.zip', '.zipx', '.tar', '.gz', '.bz', '.bzip', + '.xz', '.rar', '.7z' + icon_class = 'file-archive-o' + when '.mp3', '.wma', '.ogg', '.oga', '.wav', '.flac', '.aac' + icon_class = 'file-audio-o' + when '.mp4', '.m4p', '.m4v', + '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv', + '.mpg', '.mpeg', '.m2v', + '.avi', '.mkv', '.flv', '.ogv', '.mov', + '.3gp', '.3g2' + icon_class = 'file-video-o' + when '.doc', '.dot', '.docx', '.docm', '.dotx', '.dotm', '.docb' + icon_class = 'file-word-o' + when '.xls', '.xlt', '.xlm', '.xlsx', '.xlsm', '.xltx', '.xltm', + '.xlsb', '.xla', '.xlam', '.xll', '.xlw' + icon_class = 'file-excel-o' + when '.ppt', '.pot', '.pps', '.pptx', '.pptm', '.potx', '.potm', + '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm' + icon_class = 'file-powerpoint-o' + else + icon_class = 'file-text-o' + end + end + + icon_class + end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index bf6726574ec..6dd9b6f017c 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -34,12 +34,13 @@ module TreeHelper end end - # Return an image icon depending on the file type + # Return an image icon depending on the file type and mode # # type - String type of the tree item; either 'folder' or 'file' - def tree_icon(type) - icon_class = type == 'folder' ? 'folder' : 'file-o' - icon(icon_class) + # mode - File unix mode + # name - File name + def tree_icon(type, mode, name) + icon("#{file_type_icon_class(type, mode, name)} fw") end def tree_hex_class(content) diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index ba60bd92869..65c3ab10e02 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -22,7 +22,7 @@ %div#tree-content-holder.tree-content-holder %article.file-holder .file-title - %i.fa.fa-file + = blob_icon blob.mode, blob.name %strong = blob.name %small diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index b253fe896e3..02ecbade219 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -1,6 +1,6 @@ %tr{ class: "tree-item #{tree_hex_class(blob_item)}" } %td.tree-item-file-name - = tree_icon(type) + = tree_icon(type, blob_item.mode, blob_item.name) %span.str-truncated = link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)) %td.tree_time_ago.cgray diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml index 20c70cac699..2b5f671c09e 100644 --- a/app/views/projects/tree/_submodule_item.html.haml +++ b/app/views/projects/tree/_submodule_item.html.haml @@ -1,6 +1,6 @@ %tr{ class: "tree-item" } %td.tree-item-file-name - %i.fa.fa-archive + %i.fa.fa-archive.fa-fw = submodule_link(submodule_item, @ref) %td %td.hidden-xs diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml index 94342bc9b2b..e87138bf980 100644 --- a/app/views/projects/tree/_tree_item.html.haml +++ b/app/views/projects/tree/_tree_item.html.haml @@ -1,6 +1,6 @@ %tr{ class: "tree-item #{tree_hex_class(tree_item)}" } %td.tree-item-file-name - = tree_icon(type) + = tree_icon(type, tree_item.mode, tree_item.name) %span.str-truncated - path = flatten_tree(tree_item) = link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)) diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb new file mode 100644 index 00000000000..0b1cf07b7b0 --- /dev/null +++ b/spec/helpers/icons_helper_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe IconsHelper do + describe 'file_type_icon_class' do + it 'returns folder class' do + expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder' + end + + it 'returns share class' do + expect(file_type_icon_class('file', 0120000, 'link')).to eq 'share' + end + + it 'returns file-pdf-o class with .pdf' do + expect(file_type_icon_class('file', 0, 'filename.pdf')).to eq 'file-pdf-o' + end + + it 'returns file-image-o class with .jpg' do + expect(file_type_icon_class('file', 0, 'filename.jpg')).to eq 'file-image-o' + end + + it 'returns file-image-o class with .JPG' do + expect(file_type_icon_class('file', 0, 'filename.JPG')).to eq 'file-image-o' + end + + it 'returns file-image-o class with .png' do + expect(file_type_icon_class('file', 0, 'filename.png')).to eq 'file-image-o' + end + + it 'returns file-archive-o class with .tar' do + expect(file_type_icon_class('file', 0, 'filename.tar')).to eq 'file-archive-o' + end + + it 'returns file-archive-o class with .TAR' do + expect(file_type_icon_class('file', 0, 'filename.TAR')).to eq 'file-archive-o' + end + + it 'returns file-archive-o class with .tar.gz' do + expect(file_type_icon_class('file', 0, 'filename.tar.gz')).to eq 'file-archive-o' + end + + it 'returns file-audio-o class with .mp3' do + expect(file_type_icon_class('file', 0, 'filename.mp3')).to eq 'file-audio-o' + end + + it 'returns file-audio-o class with .MP3' do + expect(file_type_icon_class('file', 0, 'filename.MP3')).to eq 'file-audio-o' + end + + it 'returns file-audio-o class with .wav' do + expect(file_type_icon_class('file', 0, 'filename.wav')).to eq 'file-audio-o' + end + + it 'returns file-video-o class with .avi' do + expect(file_type_icon_class('file', 0, 'filename.avi')).to eq 'file-video-o' + end + + it 'returns file-video-o class with .AVI' do + expect(file_type_icon_class('file', 0, 'filename.AVI')).to eq 'file-video-o' + end + + it 'returns file-video-o class with .mp4' do + expect(file_type_icon_class('file', 0, 'filename.mp4')).to eq 'file-video-o' + end + + it 'returns file-word-o class with .doc' do + expect(file_type_icon_class('file', 0, 'filename.doc')).to eq 'file-word-o' + end + + it 'returns file-word-o class with .DOC' do + expect(file_type_icon_class('file', 0, 'filename.DOC')).to eq 'file-word-o' + end + + it 'returns file-word-o class with .docx' do + expect(file_type_icon_class('file', 0, 'filename.docx')).to eq 'file-word-o' + end + + it 'returns file-excel-o class with .xls' do + expect(file_type_icon_class('file', 0, 'filename.xls')).to eq 'file-excel-o' + end + + it 'returns file-excel-o class with .XLS' do + expect(file_type_icon_class('file', 0, 'filename.XLS')).to eq 'file-excel-o' + end + + it 'returns file-excel-o class with .xlsx' do + expect(file_type_icon_class('file', 0, 'filename.xlsx')).to eq 'file-excel-o' + end + + it 'returns file-excel-o class with .ppt' do + expect(file_type_icon_class('file', 0, 'filename.ppt')).to eq 'file-powerpoint-o' + end + + it 'returns file-excel-o class with .PPT' do + expect(file_type_icon_class('file', 0, 'filename.PPT')).to eq 'file-powerpoint-o' + end + + it 'returns file-excel-o class with .pptx' do + expect(file_type_icon_class('file', 0, 'filename.pptx')).to eq 'file-powerpoint-o' + end + + it 'returns file-text-o class with .unknow' do + expect(file_type_icon_class('file', 0, 'filename.unknow')).to eq 'file-text-o' + end + + it 'returns file-text-o class with no extension' do + expect(file_type_icon_class('file', 0, 'CHANGELOG')).to eq 'file-text-o' + end + end +end -- GitLab From be65373fc78ea887aa2d59ee49ae8bb14c64b084 Mon Sep 17 00:00:00 2001 From: Nihad Abbasov Date: Sun, 29 Mar 2015 03:39:32 +0400 Subject: [PATCH 1583/1609] update acts-as-taggable-on --- Gemfile | 2 +- Gemfile.lock | 6 +- .../initializers/acts_as_taggable_on_patch.rb | 131 ------------------ 3 files changed, 4 insertions(+), 135 deletions(-) delete mode 100644 config/initializers/acts_as_taggable_on_patch.rb diff --git a/Gemfile b/Gemfile index e7f75055f3f..b5df7cfcd75 100644 --- a/Gemfile +++ b/Gemfile @@ -115,7 +115,7 @@ end gem "state_machine" # Issue tags -gem "acts-as-taggable-on" +gem 'acts-as-taggable-on', '~> 3.4' # Background jobs gem 'slim' diff --git a/Gemfile.lock b/Gemfile.lock index 7da4d3c3583..5adaffb5dc8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,8 +33,8 @@ GEM minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) - acts-as-taggable-on (2.4.1) - rails (>= 3, < 5) + acts-as-taggable-on (3.5.0) + activerecord (>= 3.2, < 5) addressable (2.3.5) annotate (2.6.0) activerecord (>= 2.3.0) @@ -662,7 +662,7 @@ PLATFORMS DEPENDENCIES RedCloth ace-rails-ap - acts-as-taggable-on + acts-as-taggable-on (~> 3.4) addressable annotate (~> 2.6.0.beta2) asana (~> 0.0.6) diff --git a/config/initializers/acts_as_taggable_on_patch.rb b/config/initializers/acts_as_taggable_on_patch.rb deleted file mode 100644 index 0d535cb5cac..00000000000 --- a/config/initializers/acts_as_taggable_on_patch.rb +++ /dev/null @@ -1,131 +0,0 @@ -# This is a patch to address the issue in https://github.com/mbleigh/acts-as-taggable-on/issues/427 caused by -# https://github.com/rails/rails/commit/31a43ebc107fbd50e7e62567e5208a05909ec76c -# gem 'acts-as-taggable-on' has the fix included https://github.com/mbleigh/acts-as-taggable-on/commit/89bbed3864a9252276fb8dd7d535fce280454b90 -# but not in the currently used version of gem ('2.4.1') -# With replacement of 'acts-as-taggable-on' gem this file will become obsolete - -module ActsAsTaggableOn::Taggable - module Core - module ClassMethods - def tagged_with(tags, options = {}) - tag_list = ActsAsTaggableOn::TagList.from(tags) - empty_result = where("1 = 0") - - return empty_result if tag_list.empty? - - joins = [] - conditions = [] - having = [] - select_clause = [] - - context = options.delete(:on) - owned_by = options.delete(:owned_by) - alias_base_name = undecorated_table_name.gsub('.','_') - quote = ActsAsTaggableOn::Tag.using_postgresql? ? '"' : '' - - if options.delete(:exclude) - if options.delete(:wild) - tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ") - else - tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ") - end - - conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})" - - if owned_by - joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" + - " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{owned_by.id}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}" - end - - elsif options.delete(:any) - # get tags, drop out if nothing returned (we need at least one) - tags = - if options.delete(:wild) - ActsAsTaggableOn::Tag.named_like_any(tag_list) - else - ActsAsTaggableOn::Tag.named_any(tag_list) - end - - return empty_result unless tags.length > 0 - - # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123 - # avoid ambiguous column name - taggings_context = context ? "_#{context}" : '' - - taggings_alias = adjust_taggings_alias( - "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}" - ) - - tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" - tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context - - # don't need to sanitize sql, map all ids and join with OR logic - conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ") - select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one? - - if owned_by - tagging_join << " AND " + - sanitize_sql([ - "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?", - owned_by.id, - owned_by.class.base_class.to_s - ]) - end - - joins << tagging_join - else - tags = ActsAsTaggableOn::Tag.named_any(tag_list) - - return empty_result unless tags.length == tag_list.length - - tags.each do |tag| - taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}") - tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" + - " AND #{taggings_alias}.tag_id = #{tag.id}" - - tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context - - if owned_by - tagging_join << " AND " + - sanitize_sql([ - "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?", - owned_by.id, - owned_by.class.base_class.to_s - ]) - end - - joins << tagging_join - end - end - - taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group" - - if options.delete(:match_all) - joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" - - - group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}" - group = group_columns - having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}" - end - - select(select_clause) \ - .joins(joins.join(" ")) \ - .where(conditions.join(" AND ")) \ - .group(group) \ - .having(having) \ - .order(options[:order]) \ - .readonly(false) - end - end - end -end -- GitLab From bba2b10eb5430195b3f5f874c0379fe77213246a Mon Sep 17 00:00:00 2001 From: Nihad Abbasov Date: Sun, 29 Mar 2015 05:36:53 +0500 Subject: [PATCH 1584/1609] properly paginate project events in API --- lib/api/projects.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 83f65eec6cc..620922092de 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -95,10 +95,7 @@ module API # Example Request: # GET /projects/:id/events get ":id/events" do - limit = (params[:per_page] || 20).to_i - offset = (params[:page] || 0).to_i * limit - events = user_project.events.recent.limit(limit).offset(offset) - + events = paginate user_project.events.recent present events, with: Entities::Event end -- GitLab From 7652510c6ba5ed784506830667b94923d9eb8ca8 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Sun, 29 Mar 2015 12:23:56 +0100 Subject: [PATCH 1585/1609] Moved download button into sidebar Star and fork buttons moved up --- app/views/projects/_home_panel.html.haml | 14 +++++--------- app/views/projects/show.html.haml | 5 ++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 599afe6e779..e0ceafc99e0 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -14,11 +14,6 @@ – = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do = readme.name - - .project-home-row.hidden-xs - - if current_user && !empty_repo - .project-home-dropdown - = render "dropdown" %span.star.pull-right.prepend-left-10.js-toggler-container{class: @show_star ? 'on' : ''} - if current_user = link_to_toggle_star('Star this project.', false, true) @@ -33,8 +28,9 @@ - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-default' do = link_to_toggle_fork - - unless @project.empty_repo? - - if can? current_user, :download_code, @project - .pull-right.prepend-left-10 - = render 'projects/repositories/download_archive', split_button: true + + .project-home-row.hidden-xs + - if current_user && !empty_repo + .project-home-dropdown + = render "dropdown" = render "shared/clone_panel" diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index cfa6f558dd6..b9d65a9bf59 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -87,6 +87,10 @@ - else %span.light CI provided by = link_to ci_service.title, ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' + - unless @project.empty_repo? + - if can? current_user, :download_code, @project + .pull-right.prepend-left-10 + = render 'projects/repositories/download_archive', split_button: true - if readme .tab-pane#tab-readme @@ -97,4 +101,3 @@ = readme.name .wiki = render_readme(readme) - -- GitLab From 6920fd3dddf2960204545f64ca37b2e56972b2d7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Sun, 29 Mar 2015 12:56:45 +0100 Subject: [PATCH 1586/1609] Alignment fixes for repo buttons --- app/assets/stylesheets/pages/projects.scss | 8 ++++++++ app/helpers/projects_helper.rb | 2 +- app/views/projects/_home_panel.html.haml | 16 ++++++++-------- app/views/projects/show.html.haml | 3 +-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 9ad1be41579..de39fc3e9fb 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -48,6 +48,10 @@ .project-home-row { @extend .clearfix; margin-bottom: 15px; + + &.project-home-row-top { + margin-bottom: 11px; + } .project-home-desc { font-size: 16px; @@ -71,6 +75,10 @@ color: inherit; } } + + .project-repo-buttons { + margin-top: -5px; + } } .project-home-links { diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 3199ff3b99a..5ca71b14d0c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -81,7 +81,7 @@ module ProjectsHelper end def link_to_toggle_star(title, starred, signed_in) - cls = 'star-btn btn btn-default' + cls = 'star-btn btn btn-sm btn-default' cls << ' disabled' unless signed_in toggle_html = content_tag('span', class: 'toggle') do diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index e0ceafc99e0..750d3054625 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -2,7 +2,7 @@ .project-home-panel{:class => ("empty-project" if empty_repo)} .project-identicon-holder = project_icon(@project, alt: '', class: 'avatar project-avatar') - .project-home-row + .project-home-row.project-home-row-top .project-home-desc - if @project.description.present? = escaped_autolink(@project.description) @@ -14,20 +14,20 @@ – = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do = readme.name - %span.star.pull-right.prepend-left-10.js-toggler-container{class: @show_star ? 'on' : ''} - - if current_user - = link_to_toggle_star('Star this project.', false, true) - = link_to_toggle_star('Unstar this project.', true, true) - .pull-right.prepend-left-10 + .pull-right.prepend-left-10.project-repo-buttons - unless @project.empty_repo? .fork-buttons - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-default' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-sm btn-default' do = link_to_toggle_fork - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-default' do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-sm btn-default' do = link_to_toggle_fork + .star.pull-right.prepend-left-10.project-repo-buttons.js-toggler-container{class: @show_star ? 'on' : ''} + - if current_user + = link_to_toggle_star('Star this project.', false, true) + = link_to_toggle_star('Unstar this project.', true, true) .project-home-row.hidden-xs - if current_user && !empty_repo diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index b9d65a9bf59..9a2ddeb5900 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -89,8 +89,7 @@ = link_to ci_service.title, ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' - unless @project.empty_repo? - if can? current_user, :download_code, @project - .pull-right.prepend-left-10 - = render 'projects/repositories/download_archive', split_button: true + = render 'projects/repositories/download_archive', split_button: true - if readme .tab-pane#tab-readme -- GitLab From 405f91d2b8eb7c8276341ac44fcabc758fc89343 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 29 Mar 2015 18:15:54 -0700 Subject: [PATCH 1587/1609] Refactor star/fork buttons --- app/assets/stylesheets/pages/projects.scss | 30 ++++++++++++++----- app/helpers/projects_helper.rb | 10 +++++-- app/views/projects/_home_panel.html.haml | 14 ++++----- .../repositories/_download_archive.html.haml | 4 +-- app/views/projects/show.html.haml | 13 +++++--- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index de39fc3e9fb..5bd725d1222 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -48,24 +48,21 @@ .project-home-row { @extend .clearfix; margin-bottom: 15px; - + &.project-home-row-top { - margin-bottom: 11px; + margin-bottom: 15px; } .project-home-desc { font-size: 16px; line-height: 1.3; + margin-right: 215px; } .project-home-desc { float: left; color: #666; } - - .btn-action-count { - margin-left: 5px; - } } .visibility-level-label { @@ -75,9 +72,26 @@ color: inherit; } } - + .project-repo-buttons { - margin-top: -5px; + margin-top: -3px; + position: absolute; + right: 0; + width: 260px; + text-align: right; + + .btn { + font-weight: bold; + font-size: 14px; + line-height: 16px; + + .count { + padding-left: 10px; + border-left: 1px solid #ccc; + display: inline-block; + margin-left: 10px; + } + } } } diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5ca71b14d0c..4629de2ecad 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -118,11 +118,15 @@ module ProjectsHelper end def link_to_toggle_fork - out = icon('code-fork') - out << ' Fork' - out << content_tag(:span, class: 'count btn-action-count') do + html = content_tag('span') do + icon('code-fork') + ' Fork' + end + + count_html = content_tag(:span, class: 'count') do @project.forks_count.to_s end + + html + count_html end private diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 750d3054625..774be6cd138 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -14,20 +14,20 @@ – = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do = readme.name - .pull-right.prepend-left-10.project-repo-buttons + .project-repo-buttons + .inline.star.js-toggler-container{class: @show_star ? 'on' : ''} + - if current_user + = link_to_toggle_star('Star this project.', false, true) + = link_to_toggle_star('Unstar this project.', true, true) - unless @project.empty_repo? - .fork-buttons - - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace + - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace + .inline.fork-buttons.prepend-left-10 - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-sm btn-default' do = link_to_toggle_fork - else = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-sm btn-default' do = link_to_toggle_fork - .star.pull-right.prepend-left-10.project-repo-buttons.js-toggler-container{class: @show_star ? 'on' : ''} - - if current_user - = link_to_toggle_star('Star this project.', false, true) - = link_to_toggle_star('Unstar this project.', true, true) .project-home-row.hidden-xs - if current_user && !empty_repo diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 26669fb00a9..1ba7a1f2060 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,10 +3,10 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn btn-sm', rel: 'nofollow' do %i.fa.fa-download %span Download zip - %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %a.btn-sm.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only Select Archive Format diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 9a2ddeb5900..85113ffa7e2 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -68,7 +68,7 @@ = link_to license_url(@project), class: 'btn btn-block' do View license - .prepend-top-10 + .prepend-top-10.append-bottom-10 %p %span.light Created on #{@project.created_at.stamp('Aug 22, 2013')} @@ -79,17 +79,22 @@ - else #{link_to @project.owner_name, @project.owner} + - unless @project.empty_repo? + - if can? current_user, :download_code, @project + %hr + .prepend-top-10.append-bottom-10 + = render 'projects/repositories/download_archive', split_button: true + + .prepend-top-10 - @project.ci_services.each do |ci_service| - if ci_service.active? && ci_service.respond_to?(:builds_path) + %hr - if ci_service.respond_to?(:status_img_path) = link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do = image_tag ci_service.status_img_path, alt: "build status" - else %span.light CI provided by = link_to ci_service.title, ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' - - unless @project.empty_repo? - - if can? current_user, :download_code, @project - = render 'projects/repositories/download_archive', split_button: true - if readme .tab-pane#tab-readme -- GitLab From 9d6ffcfa5f09608d81031c7e366470b9c8a46db8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 29 Mar 2015 18:50:45 -0700 Subject: [PATCH 1588/1609] Refactor star btn logic for non-logged in user and fix tests --- app/helpers/projects_helper.rb | 22 ++++++++++------------ app/views/projects/_home_panel.html.haml | 11 +++++++++-- features/project/star.feature | 2 +- features/steps/project/star.rb | 8 ++++++-- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4629de2ecad..e3734023be3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -80,17 +80,17 @@ module ProjectsHelper @project.milestones.active.order("due_date, title ASC") end - def link_to_toggle_star(title, starred, signed_in) + def link_to_toggle_star(title, starred) cls = 'star-btn btn btn-sm btn-default' - cls << ' disabled' unless signed_in - toggle_html = content_tag('span', class: 'toggle') do - toggle_text = if starred - ' Unstar' - else - ' Star' - end + toggle_text = + if starred + ' Unstar' + else + ' Star' + end + toggle_html = content_tag('span', class: 'toggle') do icon('star') + toggle_text end @@ -106,12 +106,10 @@ module ProjectsHelper data: { type: 'json' } } + path = toggle_star_namespace_project_path(@project.namespace, @project) content_tag 'span', class: starred ? 'turn-on' : 'turn-off' do - link_to( - toggle_star_namespace_project_path(@project.namespace, @project), - link_opts - ) do + link_to(path, link_opts) do toggle_html + ' ' + count_html end end diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 774be6cd138..5689bdee1c6 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -17,8 +17,15 @@ .project-repo-buttons .inline.star.js-toggler-container{class: @show_star ? 'on' : ''} - if current_user - = link_to_toggle_star('Star this project.', false, true) - = link_to_toggle_star('Unstar this project.', true, true) + = link_to_toggle_star('Star this project.', false) + = link_to_toggle_star('Unstar this project.', true) + - else + = link_to new_user_session_path, class: 'btn star-btn has_tooltip', title: 'You must sign in to star a project' do + %span + = icon('star') + Star + %span.count + = @project.star_count - unless @project.empty_repo? - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace .inline.fork-buttons.prepend-left-10 diff --git a/features/project/star.feature b/features/project/star.feature index 3322f891805..a45f9c470ea 100644 --- a/features/project/star.feature +++ b/features/project/star.feature @@ -13,7 +13,7 @@ Feature: Project Star Given public project "Community" And I visit project "Community" page When I click on the star toggle button - Then The project has 0 stars + Then I redirected to sign in page @javascript Scenario: Signed in users can toggle star diff --git a/features/steps/project/star.rb b/features/steps/project/star.rb index ae2e4c7a201..50cdfd73c34 100644 --- a/features/steps/project/star.rb +++ b/features/steps/project/star.rb @@ -22,12 +22,16 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps # Requires @javascript step "I click on the star toggle button" do - find(".star .toggle", visible: true).click + find(".star-btn", visible: true).click + end + + step 'I redirected to sign in page' do + current_path.should == new_user_session_path end protected def has_n_stars(n) - expect(page).to have_css(".star .count", text: /^#{n}$/, visible: true) + expect(page).to have_css(".star-btn .count", text: n, visible: true) end end -- GitLab From 3371e40bd40ad91f476af52c8d89f022c9895784 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 29 Mar 2015 18:55:18 -0700 Subject: [PATCH 1589/1609] Include brakeman in rake test --- lib/tasks/gitlab/test.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index b4076f8238f..b4c0ae3ff79 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -2,6 +2,7 @@ namespace :gitlab do desc "GITLAB | Run all tests" task :test do cmds = [ + %W(rake brakeman), %W(rake rubocop), %W(rake spinach), %W(rake spec), -- GitLab From 227535761d48355cd4523446750e129893016cea Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 30 Mar 2015 11:03:42 +0300 Subject: [PATCH 1590/1609] LICENSE year update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index d11b8730bf1..d8cb29f3638 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2014 GitLab B.V. +Copyright (c) 2011-2015 GitLab B.V. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal -- GitLab From b06a4ff5a73b81362f24a3893860a936ed84a5c9 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 30 Mar 2015 15:18:43 +0000 Subject: [PATCH 1591/1609] Bump Docker build to GitLab v7.9.1 --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index b228a66832a..f0a8b9f53df 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update -q \ # If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.0-omnibus.2-1_amd64.deb \ + wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.1-omnibus.1-1_amd64.deb \ && dpkg -i $TMP_FILE \ && rm -f $TMP_FILE -- GitLab From 718661f9aa4bfe9e767f2e49880bd3774499e3d2 Mon Sep 17 00:00:00 2001 From: Marco Vito Moscaritolo Date: Mon, 30 Mar 2015 18:17:41 +0200 Subject: [PATCH 1592/1609] Fix merge errors on CHANGELOG During merges (d554070a and 497fd75d) changelog was "damaged", I restored the tagged 7.9.1 and added the required changes about 7.10.0 (unreleased). I think we need to ensure all PR are rebased on master before merge. --- CHANGELOG | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 021110b0e2a..90bd9db8c33 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,10 +5,7 @@ v 7.10.0 (unreleased) - Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu) - Allow HTML tags in Markdown input - Fix code unfold not working on Compare commits page (Stan Hu) - - Include missing events and fix save functionality in admin service template settings form (Stan Hu) - - Fix "Import projects from" button to show the correct instructions (Stan Hu) - Fix dots in Wiki slugs causing errors (Stan Hu) - - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu) @@ -39,7 +36,6 @@ v 7.10.0 (unreleased) - Add ability to unlink connected accounts - Replace commits calendar with faster contribution calendar that includes issues and merge requests - Add inifinite scroll to user page activity - - Don't show commit comment button when user is not signed in. - Don't include system notes in issue/MR comment count. - Don't mark merge request as updated when merge status relative to target branch changes. - Link note avatar to user. @@ -51,13 +47,20 @@ v 7.10.0 (unreleased) - Prevent holding Control-Enter or Command-Enter from posting comment multiple times. - Prevent note form from being cleared when submitting failed. - Improve file icons rendering on tree (Sullivan Sénéchal) - -v 7.9.0 - Send EmailsOnPush email when branch or tag is created or deleted. - Faster merge request processing for large repository - Prevent doubling AJAX request with each commit visit via Turbolink - Prevent unnecessary doubling of js events on import pages and user calendar +v 7.9.1 + - Include missing events and fix save functionality in admin service template settings form (Stan Hu) + - Fix "Import projects from" button to show the correct instructions (Stan Hu) + - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) + - Fix for LDAP with commas in DN + - Fix missing events and in admin Slack service template settings form (Stan Hu) + - Don't show commit comment button when user is not signed in. + - Downgrade gemnasium-gitlab-service gem + v 7.9.0 - Add HipChat integration documentation (Stan Hu) - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu) -- GitLab From b7e2be247f3414ec5635db382358a6c3a3ddf089 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 30 Mar 2015 14:17:49 -0700 Subject: [PATCH 1593/1609] Fix adding new members to group --- .../group_members/_new_group_member.html.haml | 2 +- features/groups.feature | 8 ++++++ features/steps/groups.rb | 26 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index c4c29bb2e8d..a52b8197384 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -1,7 +1,7 @@ = form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| .form-group = f.label :user_ids, "People", class: 'control-label' - .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large') + .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all) .form-group = f.label :access_level, "Group Access", class: 'control-label' diff --git a/features/groups.feature b/features/groups.feature index 05546e0d6ef..65d06a0daf9 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -47,6 +47,14 @@ Feature: Groups Then I should not see group "Owned" avatar And I should not see the "Remove avatar" button + @javascript + Scenario: Add user to group + Given gitlab user "Mike" + When I visit group "Owned" members page + And I click link "Add members" + When I select "Mike" as "Reporter" + Then I should see "Mike" in team list as "Reporter" + # Leave @javascript diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 91921f5e21c..ec5213e4b93 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -5,6 +5,32 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedUser include Select2Helper + step 'gitlab user "Mike"' do + create(:user, name: "Mike") + end + + step 'I click link "Add members"' do + find(:css, 'button.btn-new').click + end + + step 'I select "Mike" as "Reporter"' do + user = User.find_by(name: "Mike") + + within ".users-group-form" do + select2(user.id, from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see "Mike" in team list as "Reporter"' do + within '.well-list' do + page.should have_content('Mike') + page.should have_content('Reporter') + end + end + step 'I should see group "Owned" projects list' do Group.find_by(name: "Owned").projects.each do |project| page.should have_link project.name -- GitLab From 4d7cd4f329a3ee88864ac7594227c50357c2ccfc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 26 Mar 2015 21:35:10 +0000 Subject: [PATCH 1594/1609] Added badge to commits tab --- app/views/projects/commits/_head.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 83e4d24cf5f..a714f5f79e0 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,6 +1,8 @@ %ul.nav.nav-tabs = nav_link(controller: [:commit, :commits]) do - = link_to 'Commits', namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) + = link_to namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) do + Commits + %span.badge= number_with_precision(@repository.commit_count, precision: 0, delimiter: ',') = nav_link(controller: :compare) do = link_to 'Compare', namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) -- GitLab From 549e6c100b8b07cc31591cde33bae055a60a6ec9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 31 Mar 2015 05:50:10 +0300 Subject: [PATCH 1595/1609] Better legend for contribution calendar --- app/assets/javascripts/calendar.js.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 37b7ba2cc10..ff8cab6fabc 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -20,9 +20,9 @@ class @Calendar position: "top" legend: [ 0 - 1 - 4 - 7 + 5 + 10 + 20 ] legendCellPadding: 3 onClick: (date, count) -> -- GitLab From 0191857fac465fbfb4acad1b923c29f3b05529aa Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 31 Mar 2015 05:57:36 +0300 Subject: [PATCH 1596/1609] Better legend for contribution calendar pt2 :) --- app/assets/javascripts/calendar.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index ff8cab6fabc..44d75bd694f 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -20,9 +20,9 @@ class @Calendar position: "top" legend: [ 0 - 5 10 20 + 30 ] legendCellPadding: 3 onClick: (date, count) -> -- GitLab From d840b0ca5902d62652f328ca90bd9e2165491f31 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 17 Nov 2014 15:44:14 +0100 Subject: [PATCH 1597/1609] Version 7.5.0.rc1 --- VERSION | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 67fc32adaba..c720a593958 100644 --- a/VERSION +++ b/VERSION @@ -1 +1,3 @@ -7.10.0.pre + +rebase from 7.10.0.pre to 7.5.0.rc1 +Version 7.5.0.rc1 -- GitLab From 20a1c35c12813351be0a6e938e353659a726f168 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 21 Nov 2014 15:28:56 +0100 Subject: [PATCH 1598/1609] Version 7.5.0 --- VERSION | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/VERSION b/VERSION index c720a593958..7c79da894c1 100644 --- a/VERSION +++ b/VERSION @@ -1,3 +1 @@ - -rebase from 7.10.0.pre to 7.5.0.rc1 -Version 7.5.0.rc1 +Version 7.5.0 -- GitLab From 41b062f48c792dfd5b111d14b1d350d6e6524439 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 21 Nov 2014 14:52:57 +0100 Subject: [PATCH 1599/1609] Add missing timestamps to the 'members' table --- db/schema.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/schema.rb b/db/schema.rb index 4a445ae5832..b841206c6cb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,6 +12,7 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 20150324155957) do + Add missing timestamps to the 'members' table # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" -- GitLab From face8134759afee17021cae4d4f2e2d0908da4ea Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 21 Nov 2014 16:20:34 +0100 Subject: [PATCH 1600/1609] Version 7.5.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 7c79da894c1..23c21ccd734 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 7.5.0 +Version 7.5.1 -- GitLab From 2ad0ded19443032c8e38ae125f445ca49c618686 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 1 Dec 2014 18:58:37 +0100 Subject: [PATCH 1601/1609] Disable Sidekiq arguments logging by default Conflicts: config/initializers/4_sidekiq.rb --- CHANGELOG | 3 +++ config/initializers/4_sidekiq.rb | 3 ++- doc/development/README.md | 1 + doc/sidekiq_debugging.md | 14 ++++++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 doc/sidekiq_debugging.md diff --git a/CHANGELOG b/CHANGELOG index 37054da46b8..04f300a57e9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -308,6 +308,9 @@ v 7.5.2 v 7.5.1 - Add missing timestamps to 'members' table +v 7.5.2 + - Don't log Sidekiq arguments by default + v 7.5.0 - API: Add support for Hipchat (Kevin Houdebert) - Add time zone configuration in gitlab.yml (Sullivan Senechal) diff --git a/config/initializers/4_sidekiq.rb b/config/initializers/4_sidekiq.rb index e856499732e..3c930a43ad6 100644 --- a/config/initializers/4_sidekiq.rb +++ b/config/initializers/4_sidekiq.rb @@ -15,7 +15,8 @@ Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] - chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] + chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MAX_RSS'] + Disable Sidekiq arguments logging by default end end diff --git a/doc/development/README.md b/doc/development/README.md index d5d264be19d..62881781145 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -6,3 +6,4 @@ - [CI setup](ci_setup.md) for testing GitLab - [Sidekiq debugging](sidekiq_debugging.md) - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements +Disable Sidekiq arguments logging by default diff --git a/doc/sidekiq_debugging.md b/doc/sidekiq_debugging.md new file mode 100644 index 00000000000..cea11e5f126 --- /dev/null +++ b/doc/sidekiq_debugging.md @@ -0,0 +1,14 @@ +# Sidekiq debugging + +## Log arguments to Sidekiq jobs + +If you want to see what arguments are being passed to Sidekiq jobs you can set +the SIDEKIQ_LOG_ARGUMENTS environment variable. + +``` +SIDEKIQ_LOG_ARGUMENTS=1 bundle exec foreman start +``` + +It is not recommend to enable this setting in production because some Sidekiq +jobs (such as sending a password reset email) take secret arguments (for +example the password reset token). -- GitLab From 6c92a8b978dfbc0ea3dd2cf86fa03add14edc9bd Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sun, 30 Nov 2014 18:24:05 +0200 Subject: [PATCH 1602/1609] Properly fix wiki restore. ProjectWiki.new() creates a new wiki git repository, so any tries to bare clone a bundle fail. With this patch we remove the newly created wiki.git before restoring from the backup bundle. Conflicts: lib/backup/repository.rb --- lib/backup/repository.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index dfb2da9f84e..75b4e0fee5d 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -60,6 +60,7 @@ module Backup Project.find_each(batch_size: 1000) do |project| $progress.print " * #{project.path_with_namespace} ... " + Properly fix wiki restore. project.namespace.ensure_dir_exist if project.namespace @@ -81,7 +82,11 @@ module Backup wiki = ProjectWiki.new(project) if File.exists?(path_to_bundle(wiki)) +<<<<<<< HEAD $progress.print " * #{wiki.path_with_namespace} ... " +======= + print " * #{wiki.path_with_namespace} ... " +>>>>>>> Properly fix wiki restore. # If a wiki bundle exists, first remove the empty repo # that was initialized with ProjectWiki.new() and then @@ -90,7 +95,11 @@ module Backup cmd = %W(git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)}) if system(*cmd, silent) +<<<<<<< HEAD $progress.puts " [DONE]".green +======= + puts " [DONE]".green +>>>>>>> Properly fix wiki restore. else puts " [FAILED]".red puts "failed: #{cmd.join(' ')}" -- GitLab From 9b53bec62e6d20d65c01a7aa3c9e5b5b2a307daa Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 1 Dec 2014 19:14:58 +0100 Subject: [PATCH 1603/1609] Remove unnecessary backported change Make the diff with 7.5.1 smaller. --- lib/backup/repository.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 75b4e0fee5d..5b5afc3b78d 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -61,6 +61,7 @@ module Backup Project.find_each(batch_size: 1000) do |project| $progress.print " * #{project.path_with_namespace} ... " Properly fix wiki restore. + Remove unnecessary backported change project.namespace.ensure_dir_exist if project.namespace -- GitLab From 6dc4bc94b4f9596fa71e9c5ae1439ff272e9db52 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Mon, 1 Dec 2014 19:15:57 +0100 Subject: [PATCH 1604/1609] Add wiki restore fix to CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 04f300a57e9..67e2e82413e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -310,6 +310,7 @@ v 7.5.1 v 7.5.2 - Don't log Sidekiq arguments by default + - Fix restore of wiki repositories from backups v 7.5.0 - API: Add support for Hipchat (Kevin Houdebert) -- GitLab From 21640531c7acf318cae5a4e49769822e45183afe Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 2 Dec 2014 14:53:45 +0100 Subject: [PATCH 1605/1609] Version 7.5.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 23c21ccd734..f99e0b349a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 7.5.1 +Version 7.5.2 -- GitLab From 86092b96a6772c734c9108abd9d33775f2147710 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 28 Nov 2014 19:06:21 +0100 Subject: [PATCH 1606/1609] Update Sidekiq to 2.17.8 Conflicts: CHANGELOG --- CHANGELOG | 5 +++++ Gemfile | 1 + Gemfile.lock | 15 +++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 67e2e82413e..26bfd538376 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -307,11 +307,16 @@ v 7.5.2 v 7.5.1 - Add missing timestamps to 'members' table +v 7.5.3 + - Update Sidekiq to version 2.17.8 v 7.5.2 - Don't log Sidekiq arguments by default - Fix restore of wiki repositories from backups +v 7.5.1 + - Add missing timestamps to 'members' table + v 7.5.0 - API: Add support for Hipchat (Kevin Houdebert) - Add time zone configuration in gitlab.yml (Sullivan Senechal) diff --git a/Gemfile b/Gemfile index e767aec5053..5b6aa2acd9e 100644 --- a/Gemfile +++ b/Gemfile @@ -122,6 +122,7 @@ gem 'slim' gem 'sinatra', require: nil gem 'sidekiq', '~> 3.3' gem 'sidetiq', '0.6.3' +Update Sidekiq to 2.17.8 # HTTP requests gem "httparty" diff --git a/Gemfile.lock b/Gemfile.lock index ed8663b358b..6e63e1586d1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -106,6 +106,7 @@ GEM colored (1.2) colorize (0.5.8) columnize (0.9.0) + Update Sidekiq to 2.17.8 connection_pool (2.1.0) coveralls (0.7.0) multi_json (~> 1.3) @@ -541,6 +542,7 @@ GEM sexp_processor (4.4.5) shoulda-matchers (2.7.0) activesupport (>= 3.0.0) +<<<<<<< HEAD sidekiq (3.3.0) celluloid (>= 0.16.0) connection_pool (>= 2.0.0) @@ -551,6 +553,14 @@ GEM celluloid (>= 0.14.1) ice_cube (= 0.11.1) sidekiq (>= 3.0.0) +======= + sidekiq (2.17.8) + celluloid (= 0.15.2) + connection_pool (~> 2.0) + json + redis (~> 3.1) + redis-namespace (~> 1.3) +>>>>>>> Update Sidekiq to 2.17.8 simple_oauth (0.1.9) simplecov (0.9.0) docile (~> 1.1.0) @@ -767,9 +777,14 @@ DEPENDENCIES seed-fu select2-rails settingslogic +<<<<<<< HEAD shoulda-matchers (~> 2.7.0) sidekiq (~> 3.3) sidetiq (= 0.6.3) +======= + shoulda-matchers (~> 2.1.0) + sidekiq (= 2.17.8) +>>>>>>> Update Sidekiq to 2.17.8 simplecov sinatra six -- GitLab From 1101361c53a9a53e80b180f023bf74e5832d8cbe Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 5 Dec 2014 11:19:23 +0100 Subject: [PATCH 1607/1609] Revert "Update Sidekiq to 2.17.8" This reverts commit 91ee82c54f41d830922f5504ea37a8a75a0c360b. --- CHANGELOG | 5 ++--- Gemfile | 1 + Gemfile.lock | 20 +++++++++++++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 26bfd538376..bb6c6cf6c56 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -309,14 +309,13 @@ v 7.5.1 - Add missing timestamps to 'members' table v 7.5.3 - Update Sidekiq to version 2.17.8 +v 7.5.1 + - Add missing timestamps to 'members' table v 7.5.2 - Don't log Sidekiq arguments by default - Fix restore of wiki repositories from backups -v 7.5.1 - - Add missing timestamps to 'members' table - v 7.5.0 - API: Add support for Hipchat (Kevin Houdebert) - Add time zone configuration in gitlab.yml (Sullivan Senechal) diff --git a/Gemfile b/Gemfile index 5b6aa2acd9e..924fef71fea 100644 --- a/Gemfile +++ b/Gemfile @@ -123,6 +123,7 @@ gem 'sinatra', require: nil gem 'sidekiq', '~> 3.3' gem 'sidetiq', '0.6.3' Update Sidekiq to 2.17.8 +Revert "Update Sidekiq to 2.17.8" # HTTP requests gem "httparty" diff --git a/Gemfile.lock b/Gemfile.lock index 6e63e1586d1..60777cf312b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -108,6 +108,7 @@ GEM columnize (0.9.0) Update Sidekiq to 2.17.8 connection_pool (2.1.0) + Revert "Update Sidekiq to 2.17.8" coveralls (0.7.0) multi_json (~> 1.3) rest-client @@ -459,7 +460,7 @@ GEM rdoc (3.12.2) json (~> 1.4) redcarpet (3.1.2) - redis (3.1.0) + redis (3.0.6) redis-actionpack (4.0.0) actionpack (~> 4) redis-rack (~> 1.5.0) @@ -467,8 +468,8 @@ GEM redis-activesupport (4.0.0) activesupport (~> 4) redis-store (~> 1.1.0) - redis-namespace (1.5.1) - redis (~> 3.0, >= 3.0.4) + redis-namespace (1.4.1) + redis (~> 3.0.4) redis-rack (1.5.0) rack (~> 1.5) redis-store (~> 1.1.0) @@ -542,6 +543,7 @@ GEM sexp_processor (4.4.5) shoulda-matchers (2.7.0) activesupport (>= 3.0.0) +<<<<<<< HEAD <<<<<<< HEAD sidekiq (3.3.0) celluloid (>= 0.16.0) @@ -561,6 +563,14 @@ GEM redis (~> 3.1) redis-namespace (~> 1.3) >>>>>>> Update Sidekiq to 2.17.8 +======= + sidekiq (2.17.0) + celluloid (>= 0.15.2) + connection_pool (>= 1.0.0) + json + redis (>= 3.0.4) + redis-namespace (>= 1.3.1) +>>>>>>> Revert "Update Sidekiq to 2.17.8" simple_oauth (0.1.9) simplecov (0.9.0) docile (~> 1.1.0) @@ -783,8 +793,12 @@ DEPENDENCIES sidetiq (= 0.6.3) ======= shoulda-matchers (~> 2.1.0) +<<<<<<< HEAD sidekiq (= 2.17.8) >>>>>>> Update Sidekiq to 2.17.8 +======= + sidekiq (= 2.17.0) +>>>>>>> Revert "Update Sidekiq to 2.17.8" simplecov sinatra six -- GitLab From 1d294b0f0b642529b9b125272b8a8e0e315d7ad8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 5 Dec 2014 11:20:18 +0100 Subject: [PATCH 1608/1609] Update CHANGELOG for 7.5.3 --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index bb6c6cf6c56..3dea24486cd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -311,11 +311,16 @@ v 7.5.3 - Update Sidekiq to version 2.17.8 v 7.5.1 - Add missing timestamps to 'members' table +v 7.5.3 + - Bump gitlab_git to 7.0.0.rc12 (includes Rugged 0.21.2) v 7.5.2 - Don't log Sidekiq arguments by default - Fix restore of wiki repositories from backups +v 7.5.1 + - Add missing timestamps to 'members' table + v 7.5.0 - API: Add support for Hipchat (Kevin Houdebert) - Add time zone configuration in gitlab.yml (Sullivan Senechal) -- GitLab From 5759f1edfd1be95d3afc40b38f7a7b7c9417264f Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 5 Dec 2014 11:40:24 +0100 Subject: [PATCH 1609/1609] Version 7.5.3 Create _commit_message_replace_container.haml.html add commit-message container for "replace" functionality add new_tree_controller.rb Update new_tree_controller.rb add "upload" and "replace" functionality Create upload_service.rb add "replace" commit-message container Update file_action.rb change file encoding, so user can upload and replace base64 file Create _update.html.haml add "replace" view Update show.html.haml show "replace" Update _actions.html.haml add "replace" action Create _upload.html.haml add "upload" view Update _tree.html.haml add "upload" button Update show.html.haml show "upload" Update CHANGELOG add "replace" and "upload" functionalities. Rename _commit_message_replace_container.haml.html to _commit_message_replace_container.html.haml Update _update.html.haml Update _update.html.haml Update browse_files.rb add test for "replace" and "upload" Update browse_files.feature add test for "replace" and "upload" Update browse_files.feature Update _update.html.haml Update _upload.html.haml Update browse_files.rb Rename _update.html.haml to _replace.html.haml Update _replace.html.haml Update _actions.html.haml Update show.html.haml Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update and rename features to features/uploadfile.txt Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update file_action.rb code style correction Update browse_files.rb code style correction Update new_tree_controller.rb code style correction Update new_tree_controller.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb Update upload_service.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb code style correction Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Version 7.5.3 Create _commit_message_replace_container.haml.html add commit-message container for "replace" functionality Update new_tree_controller.rb add "upload" and "replace" functionality Create upload_service.rb add "replace" commit-message container Update file_action.rb change file encoding, so user can upload and replace base64 file Create _update.html.haml add "replace" view Update show.html.haml show "replace" Update _actions.html.haml add "replace" action Create _upload.html.haml add "upload" view Update _tree.html.haml add "upload" button Update show.html.haml show "upload" Update CHANGELOG add "replace" and "upload" functionalities. Rename _commit_message_replace_container.haml.html to _commit_message_replace_container.html.haml Update _update.html.haml Update _update.html.haml Update browse_files.rb add test for "replace" and "upload" Update browse_files.feature add test for "replace" and "upload" Update browse_files.feature Update _update.html.haml Update _upload.html.haml Update browse_files.rb Rename _update.html.haml to _replace.html.haml Update _replace.html.haml Update _actions.html.haml Update show.html.haml Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update and rename features to features/uploadfile.txt Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.feature Update browse_files.rb Update browse_files.feature Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature Update browse_files.rb Update file_action.rb code style correction Update browse_files.rb code style correction Update new_tree_controller.rb code style correction Update new_tree_controller.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb code style correction Update upload_service.rb Update upload_service.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb code style correction Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.rb Update browse_files.feature --- CHANGELOG | 6 + VERSION | 2 +- .../projects/new_tree_controller.rb | 72 +++++++++++ .../projects/new_tree_controller.rb.orig | 119 ++++++++++++++++++ app/services/files/upload_service.rb | 40 ++++++ app/views/projects/blob/_actions.html.haml | 6 +- app/views/projects/blob/_replace.html.haml | 35 ++++++ app/views/projects/blob/show.html.haml | 2 + app/views/projects/tree/_tree.html.haml | 10 +- app/views/projects/tree/_upload.html.haml | 35 ++++++ app/views/projects/tree/show.html.haml | 9 ++ ...commit_message_replace_container.html.haml | 14 +++ features/project/source/browse_files.feature | 25 +++- features/steps/project/source/browse_files.rb | 41 +++++- features/uploadfile.txt | 1 + lib/gitlab/satellite/files/file_action.rb | 9 +- 16 files changed, 414 insertions(+), 12 deletions(-) create mode 100644 app/controllers/projects/new_tree_controller.rb create mode 100644 app/controllers/projects/new_tree_controller.rb.orig create mode 100644 app/services/files/upload_service.rb create mode 100644 app/views/projects/blob/_replace.html.haml create mode 100644 app/views/projects/tree/_upload.html.haml create mode 100644 app/views/shared/_commit_message_replace_container.html.haml create mode 100644 features/uploadfile.txt diff --git a/CHANGELOG b/CHANGELOG index 3dea24486cd..839dd0db107 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -321,6 +321,12 @@ v 7.5.2 v 7.5.1 - Add missing timestamps to 'members' table +1. Add "replace" functionality, user can replace existing file in the current repository. +2. Add "upload" functionality, user can upload file into current repository. + +1. Add "replace" functionality, user can replace existing file in the current repository. +2. Add "upload" functionality, user can upload file into current repository. + v 7.5.0 - API: Add support for Hipchat (Kevin Houdebert) - Add time zone configuration in gitlab.yml (Sullivan Senechal) diff --git a/VERSION b/VERSION index f99e0b349a4..25df36fff9d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 7.5.2 +Version 7.5.3 diff --git a/app/controllers/projects/new_tree_controller.rb b/app/controllers/projects/new_tree_controller.rb new file mode 100644 index 00000000000..09c53bb3b1b --- /dev/null +++ b/app/controllers/projects/new_tree_controller.rb @@ -0,0 +1,72 @@ +class Projects::NewTreeController < Projects::BaseTreeController + before_filter :require_branch_head + before_filter :authorize_push_code! + + def show + end + + def update + flag = 0 + #replace & upload + if params[:file_upload] != nil + #get the name of the upload file + file_na = params[:file_upload].original_filename + file_path = nil + + if file_na != nil + file_list = tree.entries.select(&:file?) + dir_list = tree.entries.select(&:dir?) + + #check whether current path is sub-repository + dir_list.each do |dir| + if dir.name != nil + flag = 1 + end + end + + #check whether current path is sub-repository + file_list.each do |file| + if file.name != nil + flag = 1 + end + end + + if @path.match(/(.*\/)*(.+)\z/) != nil && flag == 0 #replace existing file + flag = 2 + params[:content] = params[:file_upload].read + params[:file_name] = @path + file_path = @path + result = Files::UploadService.new(@project, current_user, params, @ref, @path).execute + end + + if flag == 1 || flag == 0 #upload new file + flag = 3 + file_list.each do |file| + if file.name == file_na + flash[:alert] = file.name + " already exists!" + redirect_to project_blob_path(@project, File.join(@ref, @path)) + return + end + end + params[:content] = params[:file_upload].read + params[:file_name] = file_na + file_path = File.join(@path, File.basename(params[:file_name])) + result = Files::CreateService.new(@project, current_user, params, @ref, file_path).execute + end + end + end + + if flag == 0 + file_path = File.join(@path, File.basename(params[:file_name])) + result = Files::CreateService.new(@project, current_user, params, @ref, file_path).execute + end + + if result[:status] == :success + flash[:notice] = "Your changes have been successfully commited." + redirect_to project_blob_path(@project, File.join(@ref, file_path)) + else + flash[:alert] = result[:message] + render :show + end + end +end diff --git a/app/controllers/projects/new_tree_controller.rb.orig b/app/controllers/projects/new_tree_controller.rb.orig new file mode 100644 index 00000000000..aae0502ab8d --- /dev/null +++ b/app/controllers/projects/new_tree_controller.rb.orig @@ -0,0 +1,119 @@ +class Projects::NewTreeController < Projects::BaseTreeController + before_filter :require_branch_head + before_filter :authorize_push_code! + + def show + end + + def update +<<<<<<< HEAD + flag = 0 + #replace & upload + if params[:file_upload] != nil + #get the name of the upload file + file_na = params[:file_upload].original_filename + file_path = nil +======= + flag=0 + #replace & upload + if params[:file_upload] != nil + #get the name of the upload file + file_na=params[:file_upload].original_filename + file_path=nil +>>>>>>> 7ac8341... Update new_tree_controller.rb + + if file_na != nil + file_list = tree.entries.select(&:file?) + dir_list = tree.entries.select(&:dir?) + +<<<<<<< HEAD + #check whether current path is sub-repository + dir_list.each do |dir| + if dir.name != nil + flag = 1 + end + end + + #check whether current path is sub-repository + file_list.each do |file| + if file.name != nil + flag = 1 + end + end + + if @path.match(/(.*\/)*(.+)\z/) != nil && flag == 0 #replace existing file + flag = 2 + params[:content] = params[:file_upload].read + params[:file_name] = @path + file_path = @path + result = Files::UploadService.new(@project, current_user, params, @ref, @path).execute + end + + if flag == 1 || flag == 0 #upload new file + flag = 3 +======= + dir_list.each do |dir| #check whether current path is sub-repository + if dir.name != nil + flag=1 + end + end + + file_list.each do |file| #check whether current path is sub-repository + if file.name != nil + flag=1 + end + end + + if @path.match(/(.*\/)*(.+)\z/) != nil && flag==0 #replace existing file + flag=2 + params[:content]=params[:file_upload].read + params[:file_name]=@path + file_path=@path + result=Files::UploadService.new(@project, current_user, params, @ref, @path).execute + end + + if flag==1 || flag==0 #upload new file + flag=3 +>>>>>>> 7ac8341... Update new_tree_controller.rb + file_list.each do |file| + if file.name == file_na + flash[:alert] = file.name + " already exists!" + redirect_to project_blob_path(@project, File.join(@ref, @path)) + return + end + end +<<<<<<< HEAD + params[:content] = params[:file_upload].read + params[:file_name] = file_na +======= + params[:content]=params[:file_upload].read + params[:file_name]=file_na +>>>>>>> 7ac8341... Update new_tree_controller.rb + file_path = File.join(@path, File.basename(params[:file_name])) + result = Files::CreateService.new(@project, current_user, params, @ref, file_path).execute + end + end + end + +<<<<<<< HEAD + if flag == 0 +======= + if flag==0 +>>>>>>> 7ac8341... Update new_tree_controller.rb + file_path = File.join(@path, File.basename(params[:file_name])) + result = Files::CreateService.new(@project, current_user, params, @ref, file_path).execute + end + +<<<<<<< HEAD + if result[:status] == :success +======= + if result[:status] == :success +>>>>>>> 7ac8341... Update new_tree_controller.rb + flash[:notice] = "Your changes have been successfully commited." + redirect_to project_blob_path(@project, File.join(@ref, file_path)) + else + flash[:alert] = result[:message] + render :show + end + end +end diff --git a/app/services/files/upload_service.rb b/app/services/files/upload_service.rb new file mode 100644 index 00000000000..067b01c10f9 --- /dev/null +++ b/app/services/files/upload_service.rb @@ -0,0 +1,40 @@ +require_relative "base_service" + +module Files + class UploadService < BaseService + + def execute + allowed = if project.protected_branch?(ref) + can?(current_user, :push_code_to_protected_branches, project) + else + can?(current_user, :push_code, project) + end + + unless allowed + return error("You are not allowed to push into this branch") + end + + unless repository.branch_names.include?(ref) + return error("You can only create files if you are on top of a branch") + end + + blob = repository.blob_at_branch(ref, path) + + unless blob + return error("You can only edit text files") + end + + edit_file_action = Gitlab::Satellite::EditFileAction.new(current_user, project, ref, path) + + created_successfully = edit_file_action.commit!(params[:content], + params[:commit_message_replace], + params[:encoding]) + + if created_successfully + success + else + error("Your changes could not be committed.") + end + end + end +end diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 13f8271b979..468093b4897 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -17,6 +17,10 @@ tree_join(@commit.sha, @path)), class: 'btn btn-sm' - if allowed_tree_edit? - = button_tag class: 'remove-blob btn btn-sm btn-remove', + = button_tag class: 'btn btn-small btn-primary btn-xs', + 'data-toggle' => 'modal', 'data-target' => '#modal-replace-blob' do + Replace + = | + = button_tag class: 'remove-blob btn btn-small btn-remove', 'data-toggle' => 'modal', 'data-target' => '#modal-remove-blob' do Remove diff --git a/app/views/projects/blob/_replace.html.haml b/app/views/projects/blob/_replace.html.haml new file mode 100644 index 00000000000..920fa8bb043 --- /dev/null +++ b/app/views/projects/blob/_replace.html.haml @@ -0,0 +1,35 @@ +#modal-replace-blob.modal.hide + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3.page-title Replace #{@blob.name} + %p.light + From branch + %strong= @ref + .modal-body + = form_tag project_new_tree_path(@project, @id), method: :put, class: 'form-horizontal', :multipart => true, :onChange => 'checkFile()' do + %br + .form-group + .col-sm-2 + .col-sm-10 + = file_field_tag :file_upload, :id => "file-upload", :class => "file_up" + %br + = render 'shared/commit_message_replace_container', params: params, + placeholder: 'Replace this file because...' + .form-group + .col-sm-2 + .col-sm-10 + = button_tag 'Replace file', class: 'btn btn-small btn-primary', id: 'replace_btn' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" +:javascript + function checkFile(){ + str=document.getElementById('file_upload').value; + if(str.length < 1){ + alert('Please select file!'); + } + } + disableButtonIfAnyEmptyField('#commit_message_replace') + + + diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 69167654c39..8f9f950afb4 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -6,3 +6,5 @@ - if allowed_tree_edit? = render 'projects/blob/remove' +- if allowed_tree_edit? + = render 'projects/blob/replace' diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index d304690d162..9f43a1288aa 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -10,9 +10,11 @@ = link_to title, '#' - if current_user && can_push_branch?(@project, @ref) %li - = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'New file', id: 'new-file-link' do - %small - %i.fa.fa-plus + = link_to project_new_tree_path(@project, @id), class: 'btn btn-small btn-xs', title: 'Create New file', id: 'new-file-link' do + Create + = | + = button_tag class: 'btn btn-small btn-xs', 'data-toggle' => 'modal', title: 'Upload New file', 'data-target' => '#modal-upload-tree' do + Upload %div#tree-content-holder.tree-content-holder %table#tree-slider{class: "table_#{@hex_path} tree-table" } @@ -44,8 +46,10 @@ - if tree.readme = render "projects/tree/readme", readme: tree.readme + %div.tree_progress + :javascript // Load last commit log for each file in tree $('#tree-slider').waitForImages(function() { diff --git a/app/views/projects/tree/_upload.html.haml b/app/views/projects/tree/_upload.html.haml new file mode 100644 index 00000000000..99ee6595010 --- /dev/null +++ b/app/views/projects/tree/_upload.html.haml @@ -0,0 +1,35 @@ +#modal-upload-tree.modal.hide + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} × + %h3.page-title Upload + %p.light + From branch + %strong= @ref + .modal-body + = form_tag project_new_tree_path(@project, @id), method: :put, class: 'form-horizontal', :multipart => true , :onChange => 'checkFile()' do + %br + .form-group + .col-sm-2 + .col-sm-10 + = file_field_tag :file_upload, :id => "file", :class => "file_up" + %br + = render 'shared/commit_message_container', params: params, + placeholder: 'Upload this file because...' + .form-group + .col-sm-2 + .col-sm-10 + = button_tag 'Upload file', class: 'btn btn-small btn-primary' + = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" +:javascript + function checkFile(){ + str=document.getElementById('file').value; + if(str.length < 1){ + alert('Please select file!'); + } + } + disableButtonIfAnyEmptyField( '#commit_message') + + + diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index feca1453697..1cff71018e4 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -5,5 +5,14 @@ .tree-download-holder = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-sm pull-right hidden-xs hidden-sm', split_button: true +- if allowed_tree_edit? + = render 'projects/tree/upload' + +- if allowed_tree_edit? + = render 'projects/tree/upload' + #tree-holder.tree-holder.clearfix = render "tree", tree: @tree + + + diff --git a/app/views/shared/_commit_message_replace_container.html.haml b/app/views/shared/_commit_message_replace_container.html.haml new file mode 100644 index 00000000000..99097b88475 --- /dev/null +++ b/app/views/shared/_commit_message_replace_container.html.haml @@ -0,0 +1,14 @@ +.form-group.commit_message-group + = label_tag 'commit_message_replace', class: 'control-label' do + Commit message + .col-sm-10 + .commit-message-replace-container + .max-width-marker + = text_area_tag 'commit_message_replace', + (params[:commit_message_replace] || local_assigns[:text]), + class: 'form-control', placeholder: local_assigns[:placeholder], + required: true, rows: (local_assigns[:rows] || 3) + - if local_assigns[:hint] + %p.hint + Try to keep the first line under 52 characters + and the others under 72. diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 90b966dd645..b16a65e61f8 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -67,6 +67,16 @@ Feature: Project Source Browse Files And I click on "Commit changes" Then I am on the new file page And I see a commit error message + + @javascript + Scenario: I can upload file and commit + Given I click on "Upload" in repo + When I upload "user.feature" + And I fill the commit message + And I click on "Upload file" + And I check name of the upload file + Then I see the "user.feature" + @javascript Scenario: I can edit file @@ -132,7 +142,19 @@ Feature: Project Source Browse Files And I click on "Remove file" Then I am redirected to the files URL And I don't see the ".gitignore" - + + @javascript + Scenario: I can replace file and commit + Given I click on ".gitignore" file in repo + And I see the ".gitignore" + And I click on "Replace" + When I replace it with "LICENSE" + And I fill the commit message + And I click on "Replace file" + Then I am redirected to the ".gitignore" + And I should see new file content + + Scenario: I can browse directory with Browse Dir Given I click on files directory And I click on History link @@ -148,7 +170,6 @@ Feature: Project Source Browse Files Then I see Browse code link # Permalink - Scenario: I click on the permalink link from a branch ref Given I click on ".gitignore" file in repo And I click on Permalink diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 557555aee58..da0080afbaa 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -19,6 +19,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I see the ".gitignore"' do page.should have_content '.gitignore' end + + step 'I see the "user.feature"' do + page.should have_content 'user.feature' + end step 'I don\'t see the ".gitignore"' do page.should_not have_content '.gitignore' @@ -35,6 +39,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I should see its new content' do page.should have_content new_gitignore_content end + + step 'I should see new file content' do + old_gitignore_content != '*.rbc' + end step 'I click link "Raw"' do click_link 'Raw' @@ -97,6 +105,14 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps click_button 'Remove file' end + step 'I click on "Replace"' do + click_button "Replace" + end + + step 'I click on "Replace file"' do + click_button 'Replace file' + end + step 'I see diff' do page.should have_css '.line_holder.new' end @@ -110,6 +126,29 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps page.should have_content "Commit message" end + step 'I click on "Upload" in repo' do + click_button "Upload" + end + + step 'I click on "Upload file"' do + click_button 'Upload file' + end + + step 'I upload "user.feature"' do + attach_file(:file_upload, File.join('features', 'user.feature')) + end + + step 'I check name of the upload file' do + ".gitignore" != "user.feature" + "LICENSE" != "user.feature" + "VERSION" != "user.feature" + end + + step 'I replace it with "LICENSE"' do + attach_file(:file_upload, "LICENSE") + old_gitignore_content = "LICENSE" + end + step 'I click on files directory' do click_link 'files' end @@ -194,7 +233,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end private - + def set_new_content execute_script("blob.editor.setValue('#{new_gitignore_content}')") end diff --git a/features/uploadfile.txt b/features/uploadfile.txt new file mode 100644 index 00000000000..009ddab18c6 --- /dev/null +++ b/features/uploadfile.txt @@ -0,0 +1 @@ +this is the test file for upload and replace functionality. diff --git a/lib/gitlab/satellite/files/file_action.rb b/lib/gitlab/satellite/files/file_action.rb index 6446b14568a..f5803f97446 100644 --- a/lib/gitlab/satellite/files/file_action.rb +++ b/lib/gitlab/satellite/files/file_action.rb @@ -14,10 +14,11 @@ module Gitlab end def write_file(abs_file_path, content, file_encoding = 'text') - if file_encoding == 'base64' - File.open(abs_file_path, 'wb') { |f| f.write(Base64.decode64(content)) } - else - File.open(abs_file_path, 'w') { |f| f.write(content) } + if file_encoding == 'base64' + File.open(abs_file_path, 'wb') { |f| f.write(Base64.encode64(content))} + else + enc_64 = Base64.encode64(content) + File.open(abs_file_path, 'wb') { |f| f.write(Base64.decode64(enc_64))} end end end -- GitLab