From c231178a7ee69f13e19c5110e7176ce9f05743a7 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Fri, 1 Jul 2016 07:53:45 -0600 Subject: Upgrade oauth2 from 1.0.0 to 1.2.0. Changelog: https://github.com/intridea/oauth2/compare/v1.0.0...v1.2.0 Follow-up on !3434 since 1.2.0 doesn't limit the JWT version we can use. --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 5213a59cab0..4fac6e1e63d 100644 --- a/Gemfile +++ b/Gemfile @@ -339,7 +339,7 @@ gem 'activerecord-session_store', '~> 1.0.0' gem "nested_form", '~> 0.3.2' # OAuth -gem 'oauth2', '~> 1.0.0' +gem 'oauth2', '~> 1.2.0' # Soft deletion gem "paranoia", "~> 2.0" diff --git a/Gemfile.lock b/Gemfile.lock index f99b373dbbd..9308744bf7b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -355,7 +355,7 @@ GEM jquery-ui-rails (5.0.5) railties (>= 3.2.16) json (1.8.3) - jwt (1.5.2) + jwt (1.5.4) kaminari (0.17.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -395,7 +395,7 @@ GEM mini_portile2 (2.1.0) minitest (5.7.0) mousetrap-rails (1.4.6) - multi_json (1.11.2) + multi_json (1.12.1) multi_xml (0.5.5) multipart-post (2.0.0) mysql2 (0.3.20) @@ -408,12 +408,12 @@ GEM pkg-config (~> 1.1.7) numerizer (0.1.1) oauth (0.4.7) - oauth2 (1.0.0) + oauth2 (1.2.0) faraday (>= 0.8, < 0.10) jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) - rack (~> 1.2) + rack (>= 1.2, < 3) octokit (4.3.0) sawyer (~> 0.7.0, >= 0.5.3) omniauth (1.3.1) @@ -898,7 +898,7 @@ DEPENDENCIES net-ssh (~> 3.0.1) newrelic_rpm (~> 3.14) nokogiri (~> 1.6.7, >= 1.6.7.2) - oauth2 (~> 1.0.0) + oauth2 (~> 1.2.0) octokit (~> 4.3.0) omniauth (~> 1.3.1) omniauth-auth0 (~> 1.4.1) -- cgit v1.2.3 From edcdeb86fe40f5b66f3d1a1f57965f2299de3008 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 4 Jul 2016 19:47:24 +0300 Subject: Remove icons from file edit tabs Signed-off-by: Dmitriy Zaporozhets --- app/views/projects/blob/edit.html.haml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index e4f04ca7764..b1c9895f43e 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -4,12 +4,10 @@ %ul.nav-links.no-bottom.js-edit-mode %li.active = link_to '#editor' do - = icon('edit') Edit File %li = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do - = icon('eye') = editing_preview_title(@blob.name) = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do -- cgit v1.2.3 From f51af496769f2fe181d4633f810b85103efd181e Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 15 Jun 2016 11:15:01 +0530 Subject: Support wildcard matches for protected branches at the model level. 1. The main implementation is in the `ProtectedBranch` model. The wildcard is converted to a Regex and compared. This has been tested thoroughly. - While `Project#protected_branch?` is the main entry point, `project#open_branches` and `project#developers_can_push_to_protected_branch?` have also been modified to work with wildcard protected branches. - The regex is memoized (within the `ProtectedBranch` instance) 2. Improve the performance of `Project#protected_branch?` - This method is called from `Project#open_branches` once _per branch_ in the project, to check if that branch is protected or not. - Before, `#protected_branch?` was making a database call every time it was invoked (in the above case, that amounts to once per branch), which is expensive. - This commit caches the list of protected branches in memory, which reduces the number of database calls down to 1. - A downside to this approach is that `#protected_branch?` _could_ return a stale value (due to the caching), but this is an acceptable tradeoff. 3. Remove the (now) unused `Project#protected_branch_names` method. - This was previously used to check for protected branch status. --- app/models/project.rb | 17 ++--- app/models/protected_branch.rb | 36 ++++++++++ spec/models/project_spec.rb | 64 +++++++++++++++++- spec/models/protected_branch_spec.rb | 125 +++++++++++++++++++++++++++++++++++ 4 files changed, 227 insertions(+), 15 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index ae96f00a705..d5d57bafb98 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -803,17 +803,7 @@ class Project < ActiveRecord::Base end def open_branches - # We're using a Set here as checking values in a large Set is faster than - # checking values in a large Array. - protected_set = Set.new(protected_branch_names) - - repository.branches.reject do |branch| - protected_set.include?(branch.name) - end - end - - def protected_branch_names - @protected_branch_names ||= protected_branches.pluck(:name) + repository.branches.reject { |branch| self.protected_branch?(branch.name) } end def root_ref?(branch) @@ -830,11 +820,12 @@ class Project < ActiveRecord::Base # Check if current branch name is marked as protected in the system def protected_branch?(branch_name) - protected_branch_names.include?(branch_name) + @protected_branches ||= self.protected_branches.to_a + ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? end def developers_can_push_to_protected_branch?(branch_name) - protected_branches.any? { |pb| pb.name == branch_name && pb.developers_can_push } + protected_branches.matching(branch_name).any?(&:developers_can_push) end def forked? diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 33cf046fa75..3db1ab0e5f9 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -8,4 +8,40 @@ class ProtectedBranch < ActiveRecord::Base def commit project.commit(self.name) end + + # Returns all protected branches that match the given branch name. + # This realizes all records from the scope built up so far, and does + # _not_ return a relation. + # + # This method optionally takes in a list of `protected_branches` to search + # through, to avoid calling out to the database. + def self.matching(branch_name, protected_branches: nil) + (protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) } + end + + # Checks if the protected branch matches the given branch name. + def matches?(branch_name) + return false if self.name.blank? + + exact_match?(branch_name) || wildcard_match?(branch_name) + end + + protected + + def exact_match?(branch_name) + self.name == branch_name + end + + def wildcard_match?(branch_name) + wildcard_regex === branch_name + end + + def wildcard_regex + @wildcard_regex ||= begin + name = self.name.gsub('*', 'STAR_DONT_ESCAPE') + quoted_name = Regexp.quote(name) + regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') + /\A#{regex_string}\z/ + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a8c777d1e3e..117ffd551e4 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -437,6 +437,14 @@ describe Project, models: true do it { expect(project.open_branches.map(&:name)).to include('feature') } it { expect(project.open_branches.map(&:name)).not_to include('master') } + + it "does not include branches matching a protected branch wildcard" do + expect(project.open_branches.map(&:name)).to include('feature') + + create(:protected_branch, name: 'feat*', project: project) + + expect(Project.find(project.id).open_branches.map(&:name)).not_to include('feature') + end end describe '#star_count' do @@ -937,15 +945,67 @@ describe Project, models: true do describe '#protected_branch?' do let(:project) { create(:empty_project) } - it 'returns true when a branch is a protected branch' do + it 'returns true when the branch matches a protected branch via direct match' do project.protected_branches.create!(name: 'foo') expect(project.protected_branch?('foo')).to eq(true) end - it 'returns false when a branch is not a protected branch' do + it 'returns true when the branch matches a protected branch via wildcard match' do + project.protected_branches.create!(name: 'production/*') + + expect(project.protected_branch?('production/some-branch')).to eq(true) + end + + it 'returns false when the branch does not match a protected branch via direct match' do expect(project.protected_branch?('foo')).to eq(false) end + + it 'returns false when the branch does not match a protected branch via wildcard match' do + project.protected_branches.create!(name: 'production/*') + + expect(project.protected_branch?('staging/some-branch')).to eq(false) + end + end + + describe "#developers_can_push_to_protected_branch?" do + let(:project) { create(:empty_project) } + + context "when the branch matches a protected branch via direct match" do + it "returns true if 'Developers can Push' is turned on" do + create(:protected_branch, name: "production", project: project, developers_can_push: true) + + expect(project.developers_can_push_to_protected_branch?('production')).to be true + end + + it "returns false if 'Developers can Push' is turned off" do + create(:protected_branch, name: "production", project: project, developers_can_push: false) + + expect(project.developers_can_push_to_protected_branch?('production')).to be false + end + end + + context "when the branch matches a protected branch via wilcard match" do + it "returns true if 'Developers can Push' is turned on" do + create(:protected_branch, name: "production/*", project: project, developers_can_push: true) + + expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be true + end + + it "returns false if 'Developers can Push' is turned off" do + create(:protected_branch, name: "production/*", project: project, developers_can_push: false) + + expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be false + end + end + + context "when the branch does not match a protected branch" do + it "returns false" do + create(:protected_branch, name: "production/*", project: project, developers_can_push: true) + + expect(project.developers_can_push_to_protected_branch?('staging/some-branch')).to be false + end + end end describe '#container_registry_path_with_namespace' do diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index b523834c6e9..8bf0d24a128 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ProtectedBranch, models: true do + subject { build_stubbed(:protected_branch) } + describe 'Associations' do it { is_expected.to belong_to(:project) } end @@ -12,4 +14,127 @@ describe ProtectedBranch, models: true do it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:name) } end + + describe "#matches?" do + context "when the protected branch setting is not a wildcard" do + let(:protected_branch) { build(:protected_branch, name: "production/some-branch") } + + it "returns true for branch names that are an exact match" do + expect(protected_branch.matches?("production/some-branch")).to be true + end + + it "returns false for branch names that are not an exact match" do + expect(protected_branch.matches?("staging/some-branch")).to be false + end + end + + context "when the protected branch name contains wildcard(s)" do + context "when there is a single '*'" do + let(:protected_branch) { build(:protected_branch, name: "production/*") } + + it "returns true for branch names matching the wildcard" do + expect(protected_branch.matches?("production/some-branch")).to be true + expect(protected_branch.matches?("production/")).to be true + end + + it "returns false for branch names not matching the wildcard" do + expect(protected_branch.matches?("staging/some-branch")).to be false + expect(protected_branch.matches?("production")).to be false + end + end + + context "when the wildcard contains regex symbols other than a '*'" do + let(:protected_branch) { build(:protected_branch, name: "pro.duc.tion/*") } + + it "returns true for branch names matching the wildcard" do + expect(protected_branch.matches?("pro.duc.tion/some-branch")).to be true + end + + it "returns false for branch names not matching the wildcard" do + expect(protected_branch.matches?("production/some-branch")).to be false + expect(protected_branch.matches?("proXducYtion/some-branch")).to be false + end + end + + context "when there are '*'s at either end" do + let(:protected_branch) { build(:protected_branch, name: "*/production/*") } + + it "returns true for branch names matching the wildcard" do + expect(protected_branch.matches?("gitlab/production/some-branch")).to be true + expect(protected_branch.matches?("/production/some-branch")).to be true + expect(protected_branch.matches?("gitlab/production/")).to be true + expect(protected_branch.matches?("/production/")).to be true + end + + it "returns false for branch names not matching the wildcard" do + expect(protected_branch.matches?("gitlabproductionsome-branch")).to be false + expect(protected_branch.matches?("production/some-branch")).to be false + expect(protected_branch.matches?("gitlab/production")).to be false + expect(protected_branch.matches?("production")).to be false + end + end + + context "when there are arbitrarily placed '*'s" do + let(:protected_branch) { build(:protected_branch, name: "pro*duction/*/gitlab/*") } + + it "returns true for branch names matching the wildcard" do + expect(protected_branch.matches?("production/some-branch/gitlab/second-branch")).to be true + expect(protected_branch.matches?("proXYZduction/some-branch/gitlab/second-branch")).to be true + expect(protected_branch.matches?("proXYZduction/gitlab/gitlab/gitlab")).to be true + expect(protected_branch.matches?("proXYZduction//gitlab/")).to be true + expect(protected_branch.matches?("proXYZduction/some-branch/gitlab/")).to be true + expect(protected_branch.matches?("proXYZduction//gitlab/some-branch")).to be true + end + + it "returns false for branch names not matching the wildcard" do + expect(protected_branch.matches?("production/some-branch/not-gitlab/second-branch")).to be false + expect(protected_branch.matches?("prodXYZuction/some-branch/gitlab/second-branch")).to be false + expect(protected_branch.matches?("proXYZduction/gitlab/some-branch/gitlab")).to be false + expect(protected_branch.matches?("proXYZduction/gitlab//")).to be false + expect(protected_branch.matches?("proXYZduction/gitlab/")).to be false + expect(protected_branch.matches?("proXYZduction//some-branch/gitlab")).to be false + end + end + end + end + + describe "#matching" do + context "for direct matches" do + it "returns a list of protected branches matching the given branch name" do + production = create(:protected_branch, name: "production") + staging = create(:protected_branch, name: "staging") + + expect(ProtectedBranch.matching("production")).to include(production) + expect(ProtectedBranch.matching("production")).not_to include(staging) + end + + it "accepts a list of protected branches to search from, so as to avoid a DB call" do + production = build(:protected_branch, name: "production") + staging = build(:protected_branch, name: "staging") + + expect(ProtectedBranch.matching("production")).to be_empty + expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).to include(production) + expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).not_to include(staging) + end + end + + context "for wildcard matches" do + it "returns a list of protected branches matching the given branch name" do + production = create(:protected_branch, name: "production/*") + staging = create(:protected_branch, name: "staging/*") + + expect(ProtectedBranch.matching("production/some-branch")).to include(production) + expect(ProtectedBranch.matching("production/some-branch")).not_to include(staging) + end + + it "accepts a list of protected branches to search from, so as to avoid a DB call" do + production = build(:protected_branch, name: "production/*") + staging = build(:protected_branch, name: "staging/*") + + expect(ProtectedBranch.matching("production/some-branch")).to be_empty + expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).to include(production) + expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).not_to include(staging) + end + end + end end -- cgit v1.2.3 From 2a5cb7ec5259123cbbecb0577b9b4afacaf7546a Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 16 Jun 2016 13:03:30 +0530 Subject: Modify the frontend for wildcard protected branches. 1. Allow entering any branch name for a protected branch. - Either pick from a list of options, or enter it manually - You can enter wildcards. 2. Display branches matching a protected branch. - Add a `ProtectedBranches#show` page that displays the branches matching the given protected branch, or a message if there are no matches. - On the `index` page, display the last commit for an exact match, or the number of matching branches for a wildcard match. - Add an `iid` column to `protected_branches` - this is what we use for the `show` page URL. - On the off chance that this feature is unnecessary, this commit encapsulates it neatly, so it can be removed without affecting anything else. 3. Remove the "Last Commit" column from the list of protected branches. - There's no way to pull these for wildcard protected branches, so it's best left for the `show` page. - Rename the `@branches` instance variable to `@protected_branches` - Minor styling changes with the "Unprotect" button - floated right like the "Revoke" button for personal access tokens 4. Paginate the list of protected branches. 5. Move the instructions to the left side of the page. --- .../javascripts/protected_branches.js.coffee | 3 +- .../projects/protected_branches_controller.rb | 26 +++++++++-------- app/models/protected_branch.rb | 11 ++++++++ .../protected_branches/_branches_list.html.haml | 33 ++++++---------------- .../protected_branches/_matching_branch.html.haml | 9 ++++++ .../protected_branches/_protected_branch.html.haml | 21 ++++++++++++++ .../projects/protected_branches/index.html.haml | 16 ++++++++++- .../projects/protected_branches/show.html.haml | 25 ++++++++++++++++ config/routes.rb | 2 +- 9 files changed, 108 insertions(+), 38 deletions(-) create mode 100644 app/views/projects/protected_branches/_matching_branch.html.haml create mode 100644 app/views/projects/protected_branches/_protected_branch.html.haml create mode 100644 app/views/projects/protected_branches/show.html.haml diff --git a/app/assets/javascripts/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee index 5753c9d4e72..79c2306e4d2 100644 --- a/app/assets/javascripts/protected_branches.js.coffee +++ b/app/assets/javascripts/protected_branches.js.coffee @@ -11,7 +11,8 @@ $ -> dataType: "json" data: id: id - developers_can_push: checked + protected_branch: + developers_can_push: checked success: -> row = $(e.target) diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index efa7bf14d0f..026c5b74eb9 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -2,12 +2,14 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_admin_project! + before_action :load_protected_branch, only: [:show, :update, :destroy] layout "project_settings" def index - @branches = @project.protected_branches.to_a + @protected_branches = @project.protected_branches.order(:name).page(params[:page]) @protected_branch = @project.protected_branches.new + gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name } } }) end def create @@ -16,26 +18,24 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController @project) end - def update - protected_branch = @project.protected_branches.find(params[:id]) - - if protected_branch && - protected_branch.update_attributes( - developers_can_push: params[:developers_can_push] - ) + def show + @matching_branches = @protected_branch.matching(@project.repository.branches) + end + def update + if @protected_branch && @protected_branch.update_attributes(protected_branch_params) respond_to do |format| - format.json { render json: protected_branch, status: :ok } + format.json { render json: @protected_branch, status: :ok } end else respond_to do |format| - format.json { render json: protected_branch.errors, status: :unprocessable_entity } + format.json { render json: @protected_branch.errors, status: :unprocessable_entity } end end end def destroy - @project.protected_branches.find(params[:id]).destroy + @protected_branch.destroy respond_to do |format| format.html { redirect_to namespace_project_protected_branches_path } @@ -45,6 +45,10 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController private + def load_protected_branch + @protected_branch = @project.protected_branches.find(params[:id]) + end + def protected_branch_params params.require(:protected_branch).permit(:name, :developers_can_push) end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 3db1ab0e5f9..d3d5e1d98b2 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -19,6 +19,12 @@ class ProtectedBranch < ActiveRecord::Base (protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) } end + # Returns all branches (among the given list of branches [`Gitlab::Git::Branch`]) + # that match the current protected branch. + def matching(branches) + branches.select { |branch| self.matches?(branch.name) } + end + # Checks if the protected branch matches the given branch name. def matches?(branch_name) return false if self.name.blank? @@ -26,6 +32,11 @@ class ProtectedBranch < ActiveRecord::Base exact_match?(branch_name) || wildcard_match?(branch_name) end + # Checks if this protected branch contains a wildcard + def wildcard? + self.name.include?('*') + end + protected def exact_match?(branch_name) diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 565905cbe7b..97cb1a9052b 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,6 +1,6 @@ %h5.prepend-top-0 - Already Protected (#{@branches.size}) -- if @branches.empty? + Already Protected (#{@protected_branches.size}) +- if @protected_branches.empty? %p.settings-message.text-center No branches are protected, protect a branch with the form above. - else @@ -9,33 +9,18 @@ %table.table.protected-branches-list %colgroup %col{ width: "30%" } - %col{ width: "30%" } + %col{ width: "25%" } %col{ width: "25%" } - if can_admin_project %col %thead %tr - %th Branch - %th Last commit - %th Developers can push + %th Protected Branch + %th Commit + %th Developers Can Push - if can_admin_project %th %tbody - - @branches.each do |branch| - - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch) - %tr - %td - = link_to(branch.name, namespace_project_commits_path(@project.namespace, @project, branch.name)) - - if @project.root_ref?(branch.name) - %span.label.label-info.prepend-left-5 default - %td - - if commit = branch.commit - = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') - #{time_ago_with_tooltip(commit.committed_date)} - - else - (branch was removed from repository) - %td - = check_box_tag("developers_can_push", branch.id, branch.developers_can_push, data: { url: @url }) - - if can_admin_project - %td - = 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-warning btn-sm" + = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } + + = paginate @protected_branches, theme: 'gitlab' diff --git a/app/views/projects/protected_branches/_matching_branch.html.haml b/app/views/projects/protected_branches/_matching_branch.html.haml new file mode 100644 index 00000000000..8a5332ca5bb --- /dev/null +++ b/app/views/projects/protected_branches/_matching_branch.html.haml @@ -0,0 +1,9 @@ +%tr + %td + = link_to matching_branch.name, namespace_project_tree_path(@project.namespace, @project, matching_branch.name) + - if @project.root_ref?(matching_branch.name) + %span.label.label-info.prepend-left-5 default + %td + - commit = @project.commit(matching_branch.name) + = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') + = time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml new file mode 100644 index 00000000000..474aec3a97c --- /dev/null +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -0,0 +1,21 @@ +- url = namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) +%tr + %td + = protected_branch.name + - if @project.root_ref?(protected_branch.name) + %span.label.label-info.prepend-left-5 default + %td + - if protected_branch.wildcard? + - matching_branches = protected_branch.matching(repository.branches) + = link_to pluralize(matching_branches.count, "matching branch"), namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) + - else + - if commit = protected_branch.commit + = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') + = time_ago_with_tooltip(commit.committed_date) + - else + (branch was removed from repository) + %td + = check_box_tag("developers_can_push", protected_branch.id, protected_branch.developers_can_push, data: { url: url }) + - if can_admin_project + %td + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right" diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index c7d317dbaee..8eaef1f2904 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -21,7 +21,14 @@ .form-group = f.label :name, "Branch", class: "label-light" - = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: true}, {class: "select2", data: {placeholder: "Select branch"}}) + = f.text_field(:name) + %p.help-block + Wildcards such as + %code *-stable + or + %code production/* + are supported. + .form-group = f.check_box :developers_can_push, class: "pull-left" .prepend-left-20 @@ -31,3 +38,10 @@ = f.submit "Protect", class: "btn-create btn" %hr = render "branches_list" + +:javascript + $("#protected_branch_name").select2({ + placeholder: "Select branch", + createSearchChoice: function(term) { return { id: term, text: term }; }, + data: gon.open_branches + }) diff --git a/app/views/projects/protected_branches/show.html.haml b/app/views/projects/protected_branches/show.html.haml new file mode 100644 index 00000000000..4d8169815b3 --- /dev/null +++ b/app/views/projects/protected_branches/show.html.haml @@ -0,0 +1,25 @@ +- page_title @protected_branch.name, "Protected Branches" + +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + = @protected_branch.name + + .col-lg-9 + %h5 Matching Branches + - if @matching_branches.present? + .table-responsive + %table.table.protected-branches-list + %colgroup + %col{ width: "30%" } + %col{ width: "30%" } + %thead + %tr + %th Branch + %th Last commit + %tbody + - @matching_branches.each do |matching_branch| + = render partial: "matching_branch", object: matching_branch + - else + %p.settings-message.text-center + Couldn't find any matching branches. diff --git a/config/routes.rb b/config/routes.rb index 1572656b8c5..18a4ead2b37 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -720,7 +720,7 @@ Rails.application.routes.draw do resource :release, only: [:edit, :update] end - resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } + resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :variables, only: [:index, :show, :update, :create, :destroy] resources :triggers, only: [:index, :create, :destroy] -- cgit v1.2.3 From eb16e1e3c2614f385c4b992c919fd26768cfc3d8 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 17 Jun 2016 11:21:17 +0530 Subject: Improve the error message displayed when branch creation fails. Note: This feature was developed independently on master while this was in review. I've removed the conflicting bits and left the relevant additions, mainly a test for `Gitlab::Git::Hook`. The original commit message follows: 1. `gitlab-shell` outputs errors to `stderr`, but we weren't using this information, prior to this commit. Now we capture the `stderr`, and display it in the flash message when branch creation fails. 2. This can be used to display better errors for other git operation failures with small tweaks. 3. The return value of `Gitlab::Git::Hook#trigger` is changed from a simple `true`/`false` to a tuple of `[status, errors]`. All usages and tests have been updated to reflect this change. 4. This is only relevant to branch creation _from the Web UI_, since SSH and HTTP pushes access `gitlab-shell` either directly or through `gitlab-workhorse`. 5. A few minor changes need to be made on the `gitlab-shell` end. Right now, the `stderr` message it outputs is prefixed by "GitLab: ", which shows up in our flash message. This is better removed. --- .../projects/protected_branches/index.html.haml | 14 ++--- lib/gitlab/git/hook.rb | 9 +-- spec/lib/gitlab/git/hook_spec.rb | 70 ++++++++++++++++++++++ spec/support/test_env.rb | 2 +- 4 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 spec/lib/gitlab/git/hook_spec.rb diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 8eaef1f2904..75c27d85e9f 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -4,17 +4,17 @@ .col-lg-3 %h4.prepend-top-0 = page_title - %p Keep stable branches secure and force developers to use Merge Requests - .col-lg-9 - %h5.prepend-top-0 - Protect a branch - .account-well.append-bottom-default - %p.light-header.append-bottom-0 Protected branches are designed to + %p Keep stable branches secure and force developers to use merge requests. + %p.prepend-top-20 + Protected branches are designed to: %ul %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} %li prevent anyone from force pushing to the branch %li prevent anyone from deleting the branch %p.append-bottom-0 Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"} + .col-lg-9 + %h5.prepend-top-0 + Protect a branch - if can? current_user, :admin_project, @project = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| = form_errors(@protected_branch) @@ -35,7 +35,7 @@ = f.label :developers_can_push, "Developers can push", class: "label-light append-bottom-0" %p.light.append-bottom-0 Allow developers to push to this branch - = f.submit "Protect", class: "btn-create btn" + = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true %hr = render "branches_list" diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index 420c6883c45..db87d447358 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -14,7 +14,7 @@ module Gitlab end def trigger(gl_id, oldrev, newrev, ref) - return true unless exists? + return [true, nil] unless exists? case name when "pre-receive", "post-receive" @@ -68,13 +68,10 @@ module Gitlab end def call_update_hook(gl_id, oldrev, newrev, ref) - status = nil - Dir.chdir(repo_path) do - status = system({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev) + stdout, stderr, status = Open3.capture3({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev) + [status.success?, stderr.presence || stdout] end - - [status, nil] end def retrieve_error_message(stderr, stdout) diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb new file mode 100644 index 00000000000..a15aa173fbd --- /dev/null +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' +require 'fileutils' + +describe Gitlab::Git::Hook, lib: true do + describe "#trigger" do + let(:project) { create(:project) } + let(:user) { create(:user) } + + def create_hook(name) + FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) + File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + f.write('exit 0') + end + end + + def create_failing_hook(name) + FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) + File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + f.write(<<-HOOK) + echo 'regular message from the hook' + echo 'error message from the hook' 1>&2 + exit 1 + HOOK + end + end + + ['pre-receive', 'post-receive', 'update'].each do |hook_name| + + context "when triggering a #{hook_name} hook" do + context "when the hook is successful" do + it "returns success with no errors" do + create_hook(hook_name) + hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + blank = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' + + status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + expect(status).to be true + expect(errors).to be_blank + end + end + + context "when the hook is unsuccessful" do + it "returns failure with errors" do + create_failing_hook(hook_name) + hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + blank = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' + + status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + expect(status).to be false + expect(errors).to eq("error message from the hook\n") + end + end + end + end + + context "when the hook doesn't exist" do + it "returns success with no errors" do + hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path) + blank = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' + + status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + expect(status).to be true + expect(errors).to be_nil + end + end + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 9f9ef20f99b..6b99b0f24cb 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -63,7 +63,7 @@ module TestEnv end def disable_pre_receive - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true) + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) end # Clean /tmp/tests -- cgit v1.2.3 From d8475276c4344e41b3121b9ff958e1a5f0be2d7d Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Fri, 17 Jun 2016 14:19:02 +0530 Subject: Add a feature spec for protected branch creation. 1. Doesn't seem like there's an easy way to do this for the actual branch protection, since we'd have to test an actual `git push`. 2. Testing branch creation the web UI is also not straightforward, since the factory repo doesn't have any hooks, and so access checks at the `gitlab-shell` level aren't run. --- spec/features/protected_branches_spec.rb | 82 ++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 spec/features/protected_branches_spec.rb diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb new file mode 100644 index 00000000000..9a552e93c24 --- /dev/null +++ b/spec/features/protected_branches_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +feature 'Projected Branches', feature: true, js: true do + let(:user) { create(:user, :admin) } + let(:project) { create(:project) } + + before { login_as(user) } + + def set_protected_branch_name(branch_name) + page.execute_script("$('#protected_branch_name').val('#{branch_name}')") + end + + describe "explicit protected branches" do + it "allows creating explicit protected branches" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('some-branch') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('some-branch') + end + + it "displays the last commit on the matching branch if it exists" do + commit = create(:commit, project: project) + project.repository.add_branch(user, 'some-branch', commit.id) + + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) } + end + + it "displays an error message if the named branch does not exist" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('branch was removed') } + end + end + + describe "wildcard protected branches" do + it "allows creating protected branches with a wildcard" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('*-stable') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('*-stable') + end + + it "displays the number of matching branches" do + project.repository.add_branch(user, 'production-stable', 'master') + project.repository.add_branch(user, 'staging-stable', 'master') + + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content("2 matching branches") } + end + + it "displays all the branches matching the wildcard" do + project.repository.add_branch(user, 'production-stable', 'master') + project.repository.add_branch(user, 'staging-stable', 'master') + project.repository.add_branch(user, 'development', 'master') + create(:protected_branch, project: project, name: "*-stable") + + visit namespace_project_protected_branches_path(project.namespace, project) + click_on "2 matching branches" + + within(".protected-branches-list") do + expect(page).to have_content("production-stable") + expect(page).to have_content("staging-stable") + expect(page).not_to have_content("development") + end + end + end +end -- cgit v1.2.3 From 5de79c4f53e23aae4f07d9ca9d9e354db2998892 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 20 Jun 2016 13:09:33 +0530 Subject: Add documentation for wildcard protected branches. --- .../projects/protected_branches/index.html.haml | 3 ++- doc/workflow/protected_branches.md | 28 +++++++++++++++++++-- .../protected_branches/protected_branches1.png | Bin 155815 -> 195061 bytes .../protected_branches/protected_branches2.png | Bin 23208 -> 41179 bytes .../protected_branches/protected_branches3.png | Bin 0 -> 110160 bytes 5 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 doc/workflow/protected_branches/protected_branches3.png diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 75c27d85e9f..684cb175e68 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -23,7 +23,8 @@ = f.label :name, "Branch", class: "label-light" = f.text_field(:name) %p.help-block - Wildcards such as + = link_to "Wildcards", help_page_path(category: 'workflow', file: 'protected_branches', format: 'md', anchor: "wildcard-protected-branches") + such as %code *-stable or %code production/* diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md index d854ec1e025..67adfc2f43a 100644 --- a/doc/workflow/protected_branches.md +++ b/doc/workflow/protected_branches.md @@ -1,4 +1,4 @@ -# Protected branches +# Protected Branches Permissions in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches. @@ -28,4 +28,28 @@ For those workflows, you can allow everyone with write access to push to a prote On already protected branches you can also allow developers to push to the repository by selecting the `Developers can push` check box. -![Developers can push](protected_branches/protected_branches2.png) \ No newline at end of file +![Developers can push](protected_branches/protected_branches2.png) + +## Wildcard Protected Branches + +>**Note:** +This feature was added in GitLab 8.10. + +1. You can specify a wildcard protected branch, which will protect all branches matching the wildcard. For example: + + | Wildcard Protected Branch | Matching Branches | + |---------------------------+--------------------------------------------------------| + | `*-stable` | `production-stable`, `staging-stable` | + | `production/*` | `production/app-server`, `production/load-balancer` | + | `*gitlab*` | `gitlab`, `gitlab/staging`, `master/gitlab/production` | + +1. Protected branch settings (like "Developers Can Push") apply to all matching branches. + +1. Two different wildcards can potentially match the same branch. For example, `*-stable` and `production-*` would both match a `production-stable` branch. + >**Note:** + If _any_ of these protected branches have "Developers Can Push" set to true, then `production-stable` has it set to true. + +1. If you click on a protected branch's name, you will be presented with a list of all matching branches: + + ![protected branch matches](protected_branches/protected_branches3.png) + diff --git a/doc/workflow/protected_branches/protected_branches1.png b/doc/workflow/protected_branches/protected_branches1.png index bb3ab7d7913..c00443803de 100644 Binary files a/doc/workflow/protected_branches/protected_branches1.png and b/doc/workflow/protected_branches/protected_branches1.png differ diff --git a/doc/workflow/protected_branches/protected_branches2.png b/doc/workflow/protected_branches/protected_branches2.png index 58ace31ac57..a4f664d3b21 100644 Binary files a/doc/workflow/protected_branches/protected_branches2.png and b/doc/workflow/protected_branches/protected_branches2.png differ diff --git a/doc/workflow/protected_branches/protected_branches3.png b/doc/workflow/protected_branches/protected_branches3.png new file mode 100644 index 00000000000..2a50cb174bb Binary files /dev/null and b/doc/workflow/protected_branches/protected_branches3.png differ -- cgit v1.2.3 From 4ddcac8ca8a1880b82c63b94b87ce91727129f23 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 24 Jun 2016 16:37:49 +0100 Subject: Added blank state to issues --- app/assets/stylesheets/framework/blank.scss | 26 +++++++++++++++- app/helpers/appearances_helper.rb | 4 +-- app/views/projects/issues/index.html.haml | 46 +++++++++++++++++++---------- app/views/shared/icons/_issues.svg | 13 -------- app/views/shared/icons/_issues.svg.erb | 4 +++ 5 files changed, 62 insertions(+), 31 deletions(-) delete mode 100644 app/views/shared/icons/_issues.svg create mode 100644 app/views/shared/icons/_issues.svg.erb diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 40b5171a8c6..3e0ee4d66bd 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -1,3 +1,19 @@ +.blank-state-welcome { + text-align: center; + border-bottom: 1px solid $border-color; + + .blank-state-text { + margin-bottom: 0; + } +} + +.blank-state-welcome-title { + margin-top: 0; + margin-bottom: 5px; + font-size: 24px; + font-weight: normal; +} + .blank-state { padding-top: 20px; padding-bottom: 20px; @@ -6,7 +22,15 @@ .blank-state-no-icon { padding-top: 40px; - padding-bottom: 40px; + padding-bottom: 40px; +} + +.blank-state-icon { + padding-bottom: 20px; + + path { + fill: #ccc; + } } .blank-state-title { diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index f240584ccbf..950f323e383 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -31,7 +31,7 @@ module AppearancesHelper end end - def navbar_icon(icon_name) - render "shared/icons/#{icon_name}.svg" + def navbar_icon(icon_name, size: 16) + render "shared/icons/#{icon_name}.svg", size: size end end diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 7ce4c1e5555..5f06431ffec 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,21 +6,37 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") -%div{ class: container_class } - .top-area - = render 'shared/issuable/nav', type: :issues - .nav-controls - - if current_user - = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do - = icon('rss') - %span.icon-label - Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) +%div{ class: (container_class) } + - if @project.issues.nil? + .top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + - if current_user + = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do + = icon('rss') + %span.icon-label + Subscribe + = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) + - if can? current_user, :create_issue, @project + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + New Issue + = render 'shared/issuable/filter', type: :issues + + .issues-holder + = render "issues" + - else + .blank-state.blank-state-welcome + %h2.blank-state-welcome-title + Welcome to GitLab Issues + %p.blank-state-text + Code, test, and deploy together + .blank-state + .blank-state-icon + = navbar_icon("issues", size: 50) + %h3.blank-state-title + You don't have any issues right now. + %p.blank-state-text + Issues is the best way to track you project progress - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do New Issue - - = render 'shared/issuable/filter', type: :issues - - .issues-holder - = render "issues" diff --git a/app/views/shared/icons/_issues.svg b/app/views/shared/icons/_issues.svg deleted file mode 100644 index 2682c27ade9..00000000000 --- a/app/views/shared/icons/_issues.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - Group - Created with Sketch. - - - - - - - - \ No newline at end of file diff --git a/app/views/shared/icons/_issues.svg.erb b/app/views/shared/icons/_issues.svg.erb new file mode 100644 index 00000000000..fa8655b5609 --- /dev/null +++ b/app/views/shared/icons/_issues.svg.erb @@ -0,0 +1,4 @@ + + + + -- cgit v1.2.3 From d10d32a324411ba010f3b0ea9da407cc736529b4 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 24 Jun 2016 16:43:04 +0100 Subject: Uses any method instead of nil to check for issues --- app/views/projects/issues/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 5f06431ffec..a4795cddf2a 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -7,7 +7,7 @@ = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues") %div{ class: (container_class) } - - if @project.issues.nil? + - if @project.issues.any? .top-area = render 'shared/issuable/nav', type: :issues .nav-controls -- cgit v1.2.3 From 5fcf475bc62651909dc5ddffac508f407781b082 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 24 Jun 2016 17:45:55 +0100 Subject: Updated tests --- features/dashboard/dashboard.feature | 1 + features/project/merge_requests.feature | 7 ------- features/steps/shared/project.rb | 5 +++++ spec/features/issues/filter_issues_spec.rb | 4 +++- spec/features/search_spec.rb | 2 ++ 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index db73309804c..1f4c9020731 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -7,6 +7,7 @@ Feature: Dashboard And project "Shop" has CI enabled And project "Shop" has CI build And project "Shop" has labels: "bug", "feature", "enhancement" + And project "Shop" has issue: "bug report" And I visit dashboard page Scenario: I should see projects list diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 0e97e4d5954..21768c15c17 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -88,13 +88,6 @@ Feature: Project Merge Requests And I visit project "Shop" merge requests page Then The list should be sorted by "Oldest updated" - @javascript - Scenario: Visiting Issues after being sorted the list - Given I visit project "Shop" merge requests page - And I sort the list by "Oldest updated" - And I visit project "Shop" issues page - Then The list should be sorted by "Oldest updated" - @javascript Scenario: Visiting Merge Requests from a differente Project after sorting Given I visit project "Shop" merge requests page diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index b3411c03118..0b4920883b8 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -223,6 +223,11 @@ module SharedProject create(:label, project: project, title: 'enhancement') end + step 'project "Shop" has issue: "bug report"' do + project = Project.find_by(name: "Shop") + create(:issue, project: project, title: "bug report") + end + step 'project "Shop" has CI enabled' do project = Project.find_by(name: "Shop") project.enable_ci diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 006a06b8235..4b9b5394b61 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -7,6 +7,7 @@ describe 'Filter issues', feature: true do let!(:user) { create(:user)} let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } + let!(:issue1) { create(:issue, project: project) } before do project.team << [user, :master] @@ -196,6 +197,7 @@ describe 'Filter issues', feature: true do page.within '.labels-filter' do click_link 'bug' end + find('.dropdown-menu-close-icon').click page.within '.issues-list' do expect(page).to have_selector('.issue', count: 1) @@ -287,7 +289,7 @@ describe 'Filter issues', feature: true do wait_for_ajax page.within '.issues-list' do - expect(first('.issue')).to have_content('Frontend') + expect(page).to have_content('Frontend') end end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 85923f0a19d..d0a301038c4 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe "Search", feature: true do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } + let!(:issue) { create(:issue, project: project, assignee: user) } + let!(:issue2) { create(:issue, project: project, author: user) } before do login_with(user) -- cgit v1.2.3 From e486bbfd34bc962ddc6e7a0433a5b0cce74603a7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 6 Jul 2016 09:02:52 +0100 Subject: Fixed spelling Used variable for icon color --- app/assets/stylesheets/framework/blank.scss | 13 +++++-------- app/views/projects/issues/index.html.haml | 6 +++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 3e0ee4d66bd..d28cda6d62d 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -7,13 +7,6 @@ } } -.blank-state-welcome-title { - margin-top: 0; - margin-bottom: 5px; - font-size: 24px; - font-weight: normal; -} - .blank-state { padding-top: 20px; padding-bottom: 20px; @@ -29,7 +22,7 @@ padding-bottom: 20px; path { - fill: #ccc; + fill: $gray-darkest; } } @@ -45,3 +38,7 @@ margin-bottom: $gl-padding; font-size: 15px; } + +.blank-state-welcome-title { + font-size: 24px; +} diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index a4795cddf2a..312bd86ed04 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -26,7 +26,7 @@ = render "issues" - else .blank-state.blank-state-welcome - %h2.blank-state-welcome-title + %h2.blank-state-title.blank-state-welcome-title Welcome to GitLab Issues %p.blank-state-text Code, test, and deploy together @@ -36,7 +36,7 @@ %h3.blank-state-title You don't have any issues right now. %p.blank-state-text - Issues is the best way to track you project progress + Issues are the best way to track your project progress - if can? current_user, :create_issue, @project - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do New Issue -- cgit v1.2.3 From 10c446eaa2ca4b46f454bde7f9715dc839efa5b9 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Mon, 20 Jun 2016 13:11:54 +0530 Subject: Add wildcard protected branches to the CHANGELOG. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 4fac555e12a..1da85c04512 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ v 8.10.0 (unreleased) - Exclude email check from the standard health check - Fix changing issue state columns in milestone view - Add notification settings dropdown for groups + - Wildcards for protected branches. !4665 - Allow importing from Github using Personal Access Tokens. (Eric K Idema) - API: Todos !3188 (Robert Schilling) - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt) -- cgit v1.2.3 From e9bd8b615b9a0a88aaf7bcc13f5df73deef74805 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 6 Jul 2016 14:00:20 -0500 Subject: Update time format of duration --- app/helpers/time_helper.rb | 7 +++++++ app/views/projects/ci/pipelines/_pipeline.html.haml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index b04b0a5114c..d1086025ad5 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -23,4 +23,11 @@ module TimeHelper def date_from_to(from, to) "#{from.to_s(:short)} - #{to.to_s(:short)}" end + + def duration_in_numbers(finished_at, started_at) + diff_in_seconds = finished_at.to_i - started_at.to_i + time_format = diff_in_seconds < 3600 ? "%M:%S" : "%H:%M:%S" + + Time.at(diff_in_seconds).utc.strftime(time_format) + end end diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index e38d1ff5ff0..af8dd5cd02c 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -45,7 +45,7 @@ %td - if pipeline.started_at && pipeline.finished_at %p.duration - #{duration_in_words(pipeline.finished_at, pipeline.started_at)} + = duration_in_numbers(pipeline.finished_at, pipeline.started_at) %td .controls.hidden-xs.pull-right -- cgit v1.2.3 From 18a5bb05204ee437902d82e5973a427b9aac6d53 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 18:43:55 +0200 Subject: Store diff head and start commit shas on MR diffs --- app/models/merge_request_diff.rb | 17 ++++++++++------- ...8202603_add_head_commit_id_to_merge_request_diffs.rb | 5 +++++ ...224534_add_start_commit_id_to_merge_request_diffs.rb | 5 +++++ 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb create mode 100644 db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 0fcde6fc8f1..fd69f915bd2 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -24,7 +24,7 @@ class MergeRequestDiff < ActiveRecord::Base serialize :st_diffs after_create :reload_content, unless: :importing? - after_save :keep_around_commit + after_save :keep_around_commits def reload_content reload_commits @@ -147,12 +147,13 @@ class MergeRequestDiff < ActiveRecord::Base new_attributes[:st_diffs] = new_diffs - base_commit_sha = self.repository.merge_base(self.head, self.base) - new_attributes[:base_commit_sha] = base_commit_sha - - self.repository.keep_around(base_commit_sha) + new_attributes[:start_commit_sha] = self.target_branch_sha + new_attributes[:head_commit_sha] = self.source_branch_sha + new_attributes[:base_commit_sha] = branch_base_sha update_columns_serialized(new_attributes) + + keep_around_commits end # Collect array of Git::Diff objects @@ -223,7 +224,9 @@ class MergeRequestDiff < ActiveRecord::Base reload end - def keep_around_commit - self.repository.keep_around(self.base_commit_sha) + def keep_around_commits + self.repository.keep_around(target_branch_sha) + self.repository.keep_around(source_branch_sha) + self.repository.keep_around(branch_base_sha) end end diff --git a/db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb b/db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb new file mode 100644 index 00000000000..1c4d60e7234 --- /dev/null +++ b/db/migrate/20160508202603_add_head_commit_id_to_merge_request_diffs.rb @@ -0,0 +1,5 @@ +class AddHeadCommitIdToMergeRequestDiffs < ActiveRecord::Migration + def change + add_column :merge_request_diffs, :head_commit_sha, :string + end +end diff --git a/db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb b/db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb new file mode 100644 index 00000000000..b7fd76ee84b --- /dev/null +++ b/db/migrate/20160516224534_add_start_commit_id_to_merge_request_diffs.rb @@ -0,0 +1,5 @@ +class AddStartCommitIdToMergeRequestDiffs < ActiveRecord::Migration + def change + add_column :merge_request_diffs, :start_commit_sha, :string + end +end -- cgit v1.2.3 From 6ce25e7b4caa9e94de74378729178c7060d640b2 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 18:48:04 +0200 Subject: Rename MergeRequest methods that return commits or shas to be more clear and consistent --- .../projects/merge_requests_controller.rb | 18 ++-- app/helpers/merge_requests_helper.rb | 2 +- app/models/merge_request.rb | 120 ++++++++++++++------- app/models/merge_request_diff.rb | 43 ++++---- app/models/project_services/jira_service.rb | 2 +- app/services/merge_requests/merge_service.rb | 2 +- .../merge_when_build_succeeds_service.rb | 2 +- app/services/merge_requests/refresh_service.rb | 8 +- .../merge_requests/widget/_heading.html.haml | 10 +- .../merge_requests/widget/open/_accept.html.haml | 2 +- .../open/_merge_when_build_succeeds.html.haml | 2 +- features/steps/project/merge_requests.rb | 2 +- lib/api/merge_requests.rb | 4 +- lib/gitlab/github_import/pull_request_formatter.rb | 4 +- .../projects/merge_requests_controller_spec.rb | 6 +- .../merge_requests/created_from_fork_spec.rb | 2 +- .../merge_when_build_succeeds_spec.rb | 4 +- .../only_allow_merge_if_build_succeeds.rb | 2 +- .../github_import/pull_request_formatter_spec.rb | 12 +-- .../import_export/project_tree_saver_spec.rb | 2 +- spec/models/merge_request_spec.rb | 29 ++--- spec/models/project_spec.rb | 2 +- spec/models/repository_spec.rb | 4 +- spec/requests/api/merge_requests_spec.rb | 4 +- spec/services/system_note_service_spec.rb | 2 +- 25 files changed, 162 insertions(+), 128 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index dd86b940a08..a03eb8513b6 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -77,12 +77,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs apply_diff_view_cookie! - @commit = @merge_request.last_commit - @base_commit = @merge_request.diff_base_commit - - # MRs created before 8.4 don't have a diff_base_commit, - # but we need it for the "View file @ ..." link by deleted files - @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit + @commit = @merge_request.diff_head_commit + @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit @comments_target = { noteable_type: 'MergeRequest', @@ -134,7 +130,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @target_project = merge_request.target_project @source_project = merge_request.source_project @commits = @merge_request.compare_commits.reverse - @commit = @merge_request.last_commit + @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare @diff_notes_disabled = true @@ -212,7 +208,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController return end - if params[:sha] != @merge_request.source_sha + if params[:sha] != @merge_request.diff_head_sha @status = :sha_mismatch return end @@ -274,16 +270,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController status ||= "preparing" else ci_service = @merge_request.source_project.ci_service - status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service + status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch) end end response = { title: merge_request.title, - sha: merge_request.last_commit_short_sha, + sha: merge_request.diff_head_commit.short_id, status: status, coverage: coverage } diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 1dd07a2a220..c7dedfe9254 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -27,7 +27,7 @@ module MergeRequestsHelper end def ci_build_details_path(merge_request) - build_url = merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch) + build_url = merge_request.source_project.ci_service.build_page(merge_request.diff_head_sha, merge_request.source_branch) return nil unless build_url parsed_url = URI.parse(build_url) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4f7e1d2f302..cc85421a815 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -16,7 +16,7 @@ class MergeRequest < ActiveRecord::Base serialize :merge_params, Hash - after_create :create_merge_request_diff, unless: :importing + after_create :create_merge_request_diff, unless: :importing? after_update :update_merge_request_diff delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil @@ -29,10 +29,6 @@ class MergeRequest < ActiveRecord::Base # when creating new merge request attr_accessor :can_be_created, :compare_commits, :compare - # Temporary fields to store target_sha, and base_sha to - # compare when importing pull requests from GitHub - attr_accessor :base_target_sha, :head_source_sha - state_machine :state, initial: :opened do event :close do transition [:reopened, :opened] => :closed @@ -169,28 +165,88 @@ class MergeRequest < ActiveRecord::Base reference end - def last_commit - merge_request_diff ? merge_request_diff.last_commit : compare_commits.last - end - def first_commit merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end + def last_commit + merge_request_diff ? merge_request_diff.last_commit : compare_commits.last + end + def diff_size merge_request_diff.size end def diff_base_commit - if merge_request_diff + if persisted? merge_request_diff.base_commit - elsif source_sha - self.target_project.merge_base_commit(self.source_sha, self.target_branch) + elsif diff_start_commit && diff_head_commit + self.target_project.merge_base_commit(diff_start_sha, diff_head_sha) + end + end + + # MRs created before 8.4 don't store a MergeRequestDiff#base_commit_sha, + # but we need to get a commit for the "View file @ ..." link by deleted files, + # so we find the likely one if we can't get the actual one. + # This will not be the actual base commit if the target branch was merged into + # the source branch after the merge request was created, but it is good enough + # for the specific purpose of linking to a commit. + # It is not good enough for use in Gitlab::Git::DiffRefs, which need the + # true base commit. + def likely_diff_base_commit + first_commit.parent || first_commit + end + + def diff_start_commit + if persisted? + merge_request_diff.start_commit + else + target_branch_head end end - def last_commit_short_sha - last_commit.short_id + def diff_head_commit + if persisted? + merge_request_diff.head_commit + else + source_branch_head + end + end + + def diff_start_sha + diff_start_commit.try(:sha) + end + + def diff_base_sha + diff_base_commit.try(:sha) + end + + def diff_head_sha + diff_head_commit.try(:sha) + end + + # When importing a pull request from GitHub, the old and new branches may no + # longer actually exist by those names, but we need to recreate the merge + # request diff with the right source and target shas. + # We use these attributes to force these to the intended values. + attr_writer :target_branch_sha, :source_branch_sha + + def source_branch_head + source_branch_ref = @source_branch_sha || source_branch + source_project.repository.commit(source_branch) if source_branch_ref + end + + def target_branch_head + target_branch_ref = @target_branch_sha || target_branch + target_project.repository.commit(target_branch) if target_branch_ref + end + + def target_branch_sha + target_branch_head.try(:sha) + end + + def source_branch_sha + source_branch_head.try(:sha) end def validate_branches @@ -241,7 +297,7 @@ class MergeRequest < ActiveRecord::Base return unless unchecked? can_be_merged = - !broken? && project.repository.can_be_merged?(source_sha, target_branch) + !broken? && project.repository.can_be_merged?(diff_head_sha, target_branch) if can_be_merged mark_as_mergeable @@ -293,7 +349,7 @@ class MergeRequest < ActiveRecord::Base !source_project.protected_branch?(source_branch) && !source_project.root_ref?(source_branch) && Ability.abilities.allowed?(current_user, :push_code, source_project) && - last_commit == source_project.commit(source_branch) + diff_head_commit == source_branch_head end def should_remove_source_branch? @@ -331,8 +387,8 @@ class MergeRequest < ActiveRecord::Base work_in_progress: work_in_progress? } - if last_commit - attrs.merge!(last_commit: last_commit.hook_attrs) + if diff_head_commit + attrs.merge!(last_commit: diff_head_commit.hook_attrs) end attributes.merge!(attrs) @@ -510,22 +566,6 @@ class MergeRequest < ActiveRecord::Base end end - def target_sha - return @base_target_sha if defined?(@base_target_sha) - - target_project.repository.commit(target_branch).try(:sha) - end - - def source_sha - return @head_source_sha if defined?(@head_source_sha) - - last_commit.try(:sha) || source_tip.try(:sha) - end - - def source_tip - source_branch && source_project.repository.commit(source_branch) - end - def fetch_ref target_project.repository.fetch_ref( source_project.repository.path_to_repo, @@ -558,10 +598,10 @@ class MergeRequest < ActiveRecord::Base def diverged_commits_count cache = Rails.cache.read(:"merge_request_#{id}_diverged_commits") - if cache.blank? || cache[:source_sha] != source_sha || cache[:target_sha] != target_sha + if cache.blank? || cache[:source_sha] != source_branch_sha || cache[:target_sha] != target_branch_sha cache = { - source_sha: source_sha, - target_sha: target_sha, + source_sha: source_branch_sha, + target_sha: target_branch_sha, diverged_commits_count: compute_diverged_commits_count } Rails.cache.write(:"merge_request_#{id}_diverged_commits", cache) @@ -571,9 +611,9 @@ class MergeRequest < ActiveRecord::Base end def compute_diverged_commits_count - return 0 unless source_sha && target_sha + return 0 unless source_branch_sha && target_branch_sha - Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size + Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size end private :compute_diverged_commits_count @@ -582,13 +622,13 @@ class MergeRequest < ActiveRecord::Base end def pipeline - @pipeline ||= source_project.pipeline(last_commit.id, source_branch) if last_commit && source_project end def diff_refs return nil unless diff_base_commit [diff_base_commit, last_commit] + @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project end def merge_commit diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index fd69f915bd2..60f4b44a5d1 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -7,7 +7,7 @@ class MergeRequestDiff < ActiveRecord::Base belongs_to :merge_request - delegate :head_source_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil + delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil state_machine :state, initial: :empty do state :collected @@ -40,8 +40,8 @@ class MergeRequestDiff < ActiveRecord::Base @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( self.repository.raw_repository, - self.base, - self.head, + self.target_branch_sha, + self.source_branch_sha, ) compare.diffs(options) end @@ -63,13 +63,21 @@ class MergeRequestDiff < ActiveRecord::Base end def base_commit - return nil unless self.base_commit_sha + return unless self.base_commit_sha merge_request.target_project.commit(self.base_commit_sha) end - def last_commit_short_sha - @last_commit_short_sha ||= last_commit.short_id + def start_commit + return unless self.start_commit_sha + + merge_request.target_project.commit(self.start_commit_sha) + end + + def head_commit + return last_commit unless self.head_commit_sha + + merge_request.target_project.commit(self.head_commit_sha) end def dump_commits(commits) @@ -166,23 +174,14 @@ class MergeRequestDiff < ActiveRecord::Base merge_request.target_project.repository end - def source_sha - return head_source_sha if head_source_sha.present? - - source_commit = merge_request.source_project.commit(source_branch) - source_commit.try(:sha) - end - - def target_sha - merge_request.target_sha - end + def branch_base_commit + return unless self.source_branch_sha && self.target_branch_sha - def base - self.target_sha || self.target_branch + merge_request.target_project.merge_base_commit(self.source_branch_sha, self.target_branch_sha) end - def head - self.source_sha + def branch_base_sha + branch_base_commit.try(:sha) end def compare @@ -193,8 +192,8 @@ class MergeRequestDiff < ActiveRecord::Base Gitlab::Git::Compare.new( self.repository.raw_repository, - self.base, - self.head + self.target_branch_sha, + self.source_branch_sha ) end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 27bf08bf7d9..97bcbacf2b9 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -144,7 +144,7 @@ class JiraService < IssueTrackerService commit_id = if entity.is_a?(Commit) entity.id elsif entity.is_a?(MergeRequest) - entity.last_commit.id + entity.diff_head_sha end commit_url = build_entity_url(:commit, commit_id) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 3bec66cea88..f1b1d90c457 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -34,7 +34,7 @@ module MergeRequests committer: committer } - commit_id = repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options) + commit_id = repository.merge(current_user, merge_request.diff_head_sha, merge_request.target_branch, options) merge_request.update(merge_commit_sha: commit_id) rescue GitHooksService::PreReceiveError => e merge_request.update(merge_error: e.message) diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb index 870f5705184..4ad5fb08311 100644 --- a/app/services/merge_requests/merge_when_build_succeeds_service.rb +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -12,7 +12,7 @@ module MergeRequests merge_request.merge_when_build_succeeds = true merge_request.merge_user = @current_user - SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.last_commit) + SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.diff_head_commit) end merge_request.save diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index fe0579744b4..de79c024428 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -34,10 +34,10 @@ module MergeRequests def close_merge_requests commit_ids = @commits.map(&:id) merge_requests = @project.merge_requests.opened.where(target_branch: @branch_name).to_a - merge_requests = merge_requests.select(&:last_commit) + merge_requests = merge_requests.select(&:diff_head_commit) merge_requests = merge_requests.select do |merge_request| - commit_ids.include?(merge_request.last_commit.id) + commit_ids.include?(merge_request.diff_head_sha) end merge_requests.uniq.select(&:source_project).each do |merge_request| @@ -94,12 +94,10 @@ module MergeRequests merge_request = merge_requests_for_source_branch.first return unless merge_request - last_commit = merge_request.last_commit - begin # Since any number of commits could have been made to the restored branch, # find the common root to see what has been added. - common_ref = @project.repository.merge_base(last_commit.id, @newrev) + common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @newrev) # If the a commit no longer exists in this repo, gitlab_git throws # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 08a38d283d2..489c632ae22 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -7,7 +7,7 @@ CI build = ci_label_for_status(status) for - - commit = @merge_request.last_commit + - commit = @merge_request.diff_head_commit = succeed "." do = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" %span.ci-coverage @@ -24,7 +24,7 @@ CI build = ci_label_for_status(status) for - - commit = @merge_request.last_commit + - commit = @merge_request.diff_head_commit = succeed "." do = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" %span.ci-coverage @@ -33,12 +33,12 @@ .ci_widget = icon("spinner spin") - Checking CI status for #{@merge_request.last_commit_short_sha}… + Checking CI status for #{@merge_request.diff_head_commit.short_id}… .ci_widget.ci-not_found{style: "display:none"} = icon("times-circle") - Could not find CI status for #{@merge_request.last_commit_short_sha}. + Could not find CI status for #{@merge_request.diff_head_commit.short_id}. .ci_widget.ci-error{style: "display:none"} = icon("times-circle") - Could not connect to the CI server. Please check your settings and try again. \ No newline at end of file + Could not connect to the CI server. Please check your settings and try again. diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 941513febbd..bf2e76f0083 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -2,7 +2,7 @@ = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token - = hidden_field_tag :sha, @merge_request.source_sha + = hidden_field_tag :sha, @merge_request.diff_head_sha .accept-merge-holder.clearfix.js-toggle-container .clearfix .accept-action diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index ad898ff153b..2b6b5e05e86 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -16,7 +16,7 @@ - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.source_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 640f1720a6c..3611c187202 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -519,7 +519,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step '"Bug NS-05" has CI status' do project = merge_request.source_project project.enable_ci - pipeline = create :ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch + pipeline = create :ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch create :ci_build, pipeline: pipeline end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 0e94efd4acd..4fcdf8968c9 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -233,8 +233,8 @@ module API render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? - if params[:sha] && merge_request.source_sha != params[:sha] - render_api_error!("SHA does not match HEAD of source branch: #{merge_request.source_sha}", 409) + if params[:sha] && merge_request.diff_head_sha != params[:sha] + render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) end merge_params = { diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 498b00cb658..a4ea2210abd 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -11,10 +11,10 @@ module Gitlab description: description, source_project: source_branch_project, source_branch: source_branch_name, - head_source_sha: source_branch_sha, + source_branch_sha: source_branch_sha, target_project: target_branch_project, target_branch: target_branch_name, - base_target_sha: target_branch_sha, + target_branch_sha: target_branch_sha, state: state, milestone: milestone, author_id: author_id, diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index eff74e12869..2d2fb87f14e 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -212,7 +212,7 @@ describe Projects::MergeRequestsController do context 'when the sha parameter matches the source SHA' do def merge_with_sha - post :merge, base_params.merge(sha: merge_request.source_sha) + post :merge, base_params.merge(sha: merge_request.diff_head_sha) end it 'returns :success' do @@ -229,11 +229,11 @@ describe Projects::MergeRequestsController do context 'when merge_when_build_succeeds is passed' do def merge_when_build_succeeds - post :merge, base_params.merge(sha: merge_request.source_sha, merge_when_build_succeeds: '1') + post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_build_succeeds: '1') end before do - create(:ci_empty_pipeline, project: project, sha: merge_request.source_sha, ref: merge_request.source_branch) + create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) end it 'returns :merge_when_build_succeeds' do diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index b4d2201c729..f676200ecf3 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -30,7 +30,7 @@ feature 'Merge request created from fork' do given(:pipeline) do create(:ci_pipeline_with_two_job, project: fork_project, - sha: merge_request.last_commit.id, + sha: merge_request.diff_head_sha, ref: merge_request.source_branch) end diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb index c5e6412d7bf..96f7b8c9932 100644 --- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb @@ -12,7 +12,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do end context "Active build for Merge Request" do - let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) } let!(:ci_build) { create(:ci_build, pipeline: pipeline) } before do @@ -47,7 +47,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do merge_user: user, title: "MepMep", merge_when_build_succeeds: true) end - let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) } let!(:ci_build) { create(:ci_build, pipeline: pipeline) } before do diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb index 65e9185ec24..80e8b8fc642 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb @@ -19,7 +19,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: end context 'when project has CI enabled' do - let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) } context 'when merge requests can only be merged if the build succeeds' do before do diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index 9587252b990..79931ecd134 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -43,10 +43,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', - head_source_sha: source_sha, + source_branch_sha: source_sha, target_project: project, target_branch: 'master', - base_target_sha: target_sha, + target_branch_sha: target_sha, state: 'opened', milestone: nil, author_id: project.creator_id, @@ -70,10 +70,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', - head_source_sha: source_sha, + source_branch_sha: source_sha, target_project: project, target_branch: 'master', - base_target_sha: target_sha, + target_branch_sha: target_sha, state: 'closed', milestone: nil, author_id: project.creator_id, @@ -97,10 +97,10 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do description: "*Created by: octocat*\n\nPlease pull these awesome changes", source_project: project, source_branch: 'feature', - head_source_sha: source_sha, + source_branch_sha: source_sha, target_project: project, target_branch: 'master', - base_target_sha: target_sha, + target_branch_sha: target_sha, state: 'merged', milestone: nil, author_id: project.creator_id, diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index a75eaa4d51f..1424de9e60b 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -125,7 +125,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ci_pipeline = create(:ci_pipeline, project: project, - sha: merge_request.last_commit.id, + sha: merge_request.diff_head_sha, ref: merge_request.source_branch, statuses: [commit_status]) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ceb4d64698f..bb83676cddf 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -62,7 +62,7 @@ describe MergeRequest, models: true do end end - describe '#target_sha' do + describe '#target_branch_sha' do context 'when the target branch does not exist anymore' do let(:project) { create(:project) } @@ -73,32 +73,32 @@ describe MergeRequest, models: true do end it 'returns nil' do - expect(subject.target_sha).to be_nil + expect(subject.target_branch_sha).to be_nil end end end - describe '#source_sha' do + describe '#source_branch_sha' do let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } context 'with diffs' do subject { create(:merge_request, :with_diffs) } it 'returns the sha of the source branch last commit' do - expect(subject.source_sha).to eq(last_branch_commit.sha) + expect(subject.source_branch_sha).to eq(last_branch_commit.sha) end end context 'without diffs' do subject { create(:merge_request, :without_diffs) } it 'returns the sha of the source branch last commit' do - expect(subject.source_sha).to eq(last_branch_commit.sha) + expect(subject.source_branch_sha).to eq(last_branch_commit.sha) end end context 'when the merge request is being created' do subject { build(:merge_request, source_branch: nil, compare_commits: []) } it 'returns nil' do - expect(subject.source_sha).to be_nil + expect(subject.source_branch_sha).to be_nil end end end @@ -252,12 +252,14 @@ describe MergeRequest, models: true do end it "can be removed if the last commit is the head of the source branch" do - allow(subject.source_project).to receive(:commit).and_return(subject.last_commit) + allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_commit) expect(subject.can_remove_source_branch?(user)).to be_truthy end it "cannot be removed if the last commit is not also the head of the source branch" do + subject.source_branch = "lfs" + expect(subject.can_remove_source_branch?(user)).to be_falsey end end @@ -363,7 +365,7 @@ describe MergeRequest, models: true do and_return(2) subject.diverged_commits_count - allow(subject).to receive(:source_sha).and_return('123abc') + allow(subject).to receive(:source_branch_sha).and_return('123abc') subject.diverged_commits_count end @@ -373,7 +375,7 @@ describe MergeRequest, models: true do and_return(2) subject.diverged_commits_count - allow(subject).to receive(:target_sha).and_return('123abc') + allow(subject).to receive(:target_branch_sha).and_return('123abc') subject.diverged_commits_count end end @@ -392,11 +394,10 @@ describe MergeRequest, models: true do describe '#pipeline' do describe 'when the source project exists' do - it 'returns the latest commit' do - commit = double(:commit, id: '123abc') + it 'returns the latest pipeline' do pipeline = double(:ci_pipeline, ref: 'master') - allow(subject).to receive(:last_commit).and_return(commit) + allow(subject).to receive(:diff_head_sha).and_return('123abc') expect(subject.source_project).to receive(:pipeline). with('123abc', 'master'). @@ -464,7 +465,7 @@ describe MergeRequest, models: true do context 'when it is not broken and has no conflicts' do it 'is marked as mergeable' do allow(subject).to receive(:broken?) { false } - allow(project.repository).to receive(:can_be_merged?) { true } + allow(project.repository).to receive(:can_be_merged?).and_return(true) expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged') end @@ -481,7 +482,7 @@ describe MergeRequest, models: true do context 'when it has conflicts' do before do allow(subject).to receive(:broken?) { false } - allow(project.repository).to receive(:can_be_merged?) { false } + allow(project.repository).to receive(:can_be_merged?).and_return(false) end it 'becomes unmergeable' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2e89d6de3a2..1b434a726dc 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -312,7 +312,7 @@ describe Project, models: true do 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 - expect(merge_request.last_commit.id).to eq(commit_id) + expect(merge_request.diff_head_sha).to eq(commit_id) end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7975fc64e59..24e49c8def3 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -12,8 +12,8 @@ describe Repository, models: true do end let(:merge_commit) do source_sha = repository.find_branch('feature').target - merge_commit_id = repository.merge(user, source_sha, 'master', commit_options) - repository.commit(merge_commit_id) + merge_commit_sha = repository.merge(user, source_sha, 'master', commit_options) + repository.commit(merge_commit_sha) end describe :branch_names_contains do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 5d81844fb84..4a1b5600bdf 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -439,14 +439,14 @@ describe API::API, api: true do end it "returns 409 if the SHA parameter doesn't match" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha.succ + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse expect(response).to have_http_status(409) expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') end it "succeeds if the SHA parameter matches" do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha expect(response).to have_http_status(200) end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 85dd30bf48c..43693441450 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -213,7 +213,7 @@ describe SystemNoteService, services: true do create(:merge_request, source_project: project, target_project: project) end - subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.last_commit) } + subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.diff_head_commit) } it_behaves_like 'a system note' -- cgit v1.2.3 From a9fa45f09e6b6188691f37d75883b22edce7bba1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 18:51:48 +0200 Subject: Represent DiffRefs as proper class instead of tuple array --- app/controllers/projects/blob_controller.rb | 2 +- app/controllers/projects/commit_controller.rb | 1 - app/controllers/projects/compare_controller.rb | 18 +++++++++++++----- app/helpers/diff_helper.rb | 4 ++-- app/models/commit.rb | 7 +++++++ app/models/merge_request.rb | 16 ++++++++++------ app/views/projects/commit/show.html.haml | 3 +-- app/views/projects/diffs/_diffs.html.haml | 2 +- app/views/projects/diffs/_image.html.haml | 11 +++++------ app/workers/emails_on_push_worker.rb | 16 ++++++++++++++-- lib/gitlab/diff/diff_refs.rb | 26 ++++++++++++++++++++++++++ lib/gitlab/diff/file.rb | 21 +++++++++------------ lib/gitlab/diff/highlight.rb | 10 ++++++---- lib/gitlab/email/message/repository_push.rb | 8 ++++++-- lib/gitlab/workhorse.rb | 6 ++---- spec/helpers/diff_helper_spec.rb | 2 +- spec/lib/gitlab/diff/file_spec.rb | 2 +- spec/lib/gitlab/diff/highlight_spec.rb | 6 +++--- spec/lib/gitlab/diff/parallel_diff_spec.rb | 3 +-- spec/mailers/notify_spec.rb | 4 ++-- 20 files changed, 111 insertions(+), 57 deletions(-) create mode 100644 lib/gitlab/diff/diff_refs.rb diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 7599fec3cdf..5356fdf010d 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -57,7 +57,7 @@ class Projects::BlobController < Projects::ApplicationController diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true) diff_lines = diffy.diff.scan(/.*\n/)[2..-1] diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines) - @diff_lines = Gitlab::Diff::Highlight.new(diff_lines).highlight + @diff_lines = Gitlab::Diff::Highlight.new(diff_lines, repository: @repository).highlight render layout: false end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index d162a5a3165..37d6521026c 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -121,7 +121,6 @@ class Projects::CommitController < Projects::ApplicationController opts[:ignore_whitespace_change] = true if params[:format] == 'diff' @diffs = commit.diffs(opts) - @diff_refs = [commit.parent || commit, commit] @notes_count = commit.notes.count @statuses = CommitStatus.where(pipeline: pipelines) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index af0b69a2442..d240b9fe989 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -14,14 +14,22 @@ class Projects::CompareController < Projects::ApplicationController def show compare = CompareService.new. - execute(@project, @head_ref, @project, @base_ref, diff_options) + execute(@project, @head_ref, @project, @start_ref, diff_options) if compare @commits = Commit.decorate(compare.commits, @project) + + @start_commit = @project.commit(@start_ref) @commit = @project.commit(@head_ref) - @base_commit = @project.merge_base_commit(@base_ref, @head_ref) + @base_commit = @project.merge_base_commit(@start_ref, @head_ref) + @diffs = compare.diffs(diff_options) - @diff_refs = [@base_commit, @commit] + @diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: @base_commit.try(:sha), + start_sha: @start_commit.try(:sha), + head_sha: @commit.try(:sha) + ) + @diff_notes_disabled = true @grouped_diff_notes = {} end @@ -35,12 +43,12 @@ class Projects::CompareController < Projects::ApplicationController private def assign_ref_vars - @base_ref = Addressable::URI.unescape(params[:from]) + @start_ref = Addressable::URI.unescape(params[:from]) @ref = @head_ref = Addressable::URI.unescape(params[:to]) end def merge_request @merge_request ||= @project.merge_requests.opened. - find_by(source_project: @project, source_branch: @head_ref, target_branch: @base_ref) + find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref) end end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index e22dce59d0f..a7eedafe314 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -30,8 +30,8 @@ module DiffHelper options end - def safe_diff_files(diffs, diff_refs) - diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs) } + def safe_diff_files(diffs, diff_refs: nil, repository: nil) + diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } end def generate_line_code(file_path, line) diff --git a/app/models/commit.rb b/app/models/commit.rb index 174ccbaea6c..2ef3973c160 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -214,6 +214,13 @@ class Commit @raw.short_id(7) end + def diff_refs + Gitlab::Diff::DiffRefs.new( + base_sha: self.parent_id || self.sha, + head_sha: self.sha + ) + end + def pipelines @pipeline ||= project.pipelines.where(sha: sha) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index cc85421a815..70ef275d3a5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -249,6 +249,16 @@ class MergeRequest < ActiveRecord::Base source_branch_head.try(:sha) end + def diff_refs + return nil unless diff_start_commit || diff_base_commit + + Gitlab::Diff::DiffRefs.new( + base_sha: diff_base_sha, + start_sha: diff_start_sha, + head_sha: diff_head_sha + ) + end + def validate_branches if target_project == source_project && target_branch == source_branch errors.add :branch_conflict, "You can not use same project/branch for source and target" @@ -622,12 +632,6 @@ class MergeRequest < ActiveRecord::Base end def pipeline - end - - def diff_refs - return nil unless diff_base_commit - - [diff_base_commit, last_commit] @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project end diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 401cb4f7e30..d0da2606587 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -7,8 +7,7 @@ = render "ci_menu" - else %div.block-connector -= render "projects/diffs/diffs", diffs: @diffs, project: @project, - diff_refs: @diff_refs += render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @commit.diff_refs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index f18bc8c41b3..151780addc5 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -2,7 +2,7 @@ - if diff_view == 'parallel' - fluid_layout true -- diff_files = safe_diff_files(diffs, diff_refs) +- diff_files = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository) .content-block.oneline-block.files-changed .inline-parallel-buttons diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 2731219ccad..9ec6a7aa5cd 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,9 +1,8 @@ - diff = diff_file.diff -- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) +- file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) // diff_refs will be nil for orphaned commits (e.g. first commit in repo) -- if diff_refs - - old_commit_id = diff_refs.first.id - - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) +- if diff_file.old_ref + - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) - if diff.renamed_file || diff.new_file || diff.deleted_file .image @@ -16,7 +15,7 @@ %div.two-up.view %span.wrap .frame.deleted - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path))} %img{src: old_file_raw_path} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" @@ -28,7 +27,7 @@ %span.meta-height %span.wrap .frame.added - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path))} %img{src: file_raw_path} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size file.size}" diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 971f969e25e..8551288e2f2 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -28,18 +28,30 @@ class EmailsOnPushWorker :push end + merge_base_sha = project.merge_base_commit(before_sha, after_sha).try(:sha) + diff_refs = nil compare = nil reverse_compare = false if action == :push compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - diff_refs = [project.merge_base_commit(before_sha, after_sha), project.commit(after_sha)] + + diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: merge_base_sha, + start_sha: before_sha, + head_sha: after_sha + ) return false if compare.same if compare.commits.empty? compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) - diff_refs = [project.merge_base_commit(after_sha, before_sha), project.commit(before_sha)] + + diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: merge_base_sha, + start_sha: after_sha, + head_sha: before_sha + ) reverse_compare = true diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb new file mode 100644 index 00000000000..43489ae876b --- /dev/null +++ b/lib/gitlab/diff/diff_refs.rb @@ -0,0 +1,26 @@ +module Gitlab + module Diff + class DiffRefs + attr_reader :base_sha + attr_reader :start_sha + attr_reader :head_sha + + def initialize(base_sha:, start_sha: base_sha, head_sha:) + @base_sha = base_sha + @start_sha = start_sha + @head_sha = head_sha + end + + def ==(other) + other.is_a?(self.class) && + base_sha == other.base_sha && + start_sha == other.start_sha && + head_sha == other.head_sha + end + + def complete? + start_sha && head_sha + end + end + end +end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index d2e85cabf72..e422c333341 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -1,39 +1,36 @@ module Gitlab module Diff class File - attr_reader :diff, :diff_refs + attr_reader :diff, :repository, :diff_refs delegate :new_file, :deleted_file, :renamed_file, :old_path, :new_path, to: :diff, prefix: false - def initialize(diff, diff_refs) + def initialize(diff, repository:, diff_refs: nil) @diff = diff + @repository = repository @diff_refs = diff_refs end def old_ref - diff_refs[0] if diff_refs + diff_refs.try(:base_sha) end def new_ref - diff_refs[1] if diff_refs + diff_refs.try(:head_sha) end - # Array of Gitlab::DIff::Line objects + # Array of Gitlab::Diff::Line objects def diff_lines - @lines ||= parser.parse(raw_diff.each_line).to_a - end - - def too_large? - diff.too_large? + @lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a end def highlighted_diff_lines - Gitlab::Diff::Highlight.new(self).highlight + @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight end def parallel_diff_lines - Gitlab::Diff::ParallelDiff.new(self).parallelize + @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize end def mode_changed? diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 9429b3ff88d..3ad68728d65 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -1,11 +1,13 @@ module Gitlab module Diff class Highlight - attr_reader :diff_file, :diff_lines, :raw_lines + attr_reader :diff_file, :diff_lines, :raw_lines, :repository delegate :old_path, :new_path, :old_ref, :new_ref, to: :diff_file, prefix: :diff - def initialize(diff_lines) + def initialize(diff_lines, repository: nil) + @repository = repository + if diff_lines.is_a?(Gitlab::Diff::File) @diff_file = diff_lines @diff_lines = @diff_file.diff_lines @@ -19,7 +21,7 @@ module Gitlab @diff_lines.map.with_index do |diff_line, i| diff_line = diff_line.dup # ignore highlighting for "match" lines - next diff_line if diff_line.type == 'match' || diff_line.type == 'nonewline' + next diff_line if diff_line.meta? rich_line = highlight_line(diff_line) || diff_line.text @@ -70,7 +72,7 @@ module Gitlab ref = send("diff_#{version}_ref") path = send("diff_#{version}_path") - [ref.project.repository, ref.id, path] + [self.repository, ref, path] end end end diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 047c77c6fc2..97701b0cd42 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -33,11 +33,15 @@ module Gitlab end def commits - @commits ||= (Commit.decorate(compare.commits, project) if compare) + return unless compare + + @commits ||= Commit.decorate(compare.commits, project) end def diffs - @diffs ||= (safe_diff_files(compare.diffs(max_files: 30), diff_refs) if compare) + return unless compare + + @diffs ||= safe_diff_files(compare.diffs(max_files: 30), diff_refs: diff_refs, repository: project.repository) end def diffs_count diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index ef1241f8600..41b6854cbe1 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -38,12 +38,10 @@ module Gitlab end def send_git_diff(repository, diff_refs) - from, to = diff_refs - params = { 'RepoPath' => repository.path_to_repo, - 'ShaFrom' => from.sha, - 'ShaTo' => to.sha + 'ShaFrom' => diff_refs.start_sha, + 'ShaTo' => diff_refs.head_sha } [ diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 52764f41e0d..e2db33d8345 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -9,7 +9,7 @@ describe DiffHelper do let(:diffs) { commit.diffs } let(:diff) { diffs.first } let(:diff_refs) { [commit.parent, commit] } - let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } describe 'diff_view' do it 'returns a valid value when cookie is set' do diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index a0cbef6e6a4..1cb513d5229 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::File, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } describe :diff_lines do let(:diff_lines) { diff_file.diff_lines } diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index d19bf4ac84b..fb5d50a5c68 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -6,11 +6,11 @@ describe Gitlab::Diff::Highlight, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } describe '#highlight' do context "with a diff file" do - let(:subject) { Gitlab::Diff::Highlight.new(diff_file).highlight } + let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight } it 'should return Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) @@ -41,7 +41,7 @@ describe Gitlab::Diff::Highlight, lib: true do end context "with diff lines" do - let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines).highlight } + let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight } it 'should return Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index 1c5bbc47120..5f76b70c6f5 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -8,8 +8,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do let(:commit) { project.commit(sample_commit.id) } let(:diffs) { commit.diffs } let(:diff) { diffs.first } - let(:diff_refs) { [commit.parent, commit] } - let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) } subject { described_class.new(diff_file) } let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") } diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index aa382f930d7..0a9b10bebea 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -948,7 +948,7 @@ describe Notify do let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) } let(:send_from_committer_email) { false } - let(:diff_refs) { [project.merge_base_commit(sample_image_commit.id, sample_commit.id), project.commit(sample_commit.id)] } + let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) } subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) } @@ -1049,7 +1049,7 @@ describe Notify do let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) } let(:commits) { Commit.decorate(compare.commits, nil) } let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) } - let(:diff_refs) { [project.merge_base_commit(sample_commit.parent_id, sample_commit.id), project.commit(sample_commit.id)] } + let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) } subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) } -- cgit v1.2.3 From 17ab745e40bf89776ab16de9ba00ebb44d1c85ca Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 18:52:54 +0200 Subject: Add Timeless helper module to prevent updated_at from being updated --- app/models/merge_request.rb | 7 +------ lib/gitlab/timeless.rb | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 lib/gitlab/timeless.rb diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 70ef275d3a5..04ec6d369e5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -85,12 +85,7 @@ class MergeRequest < ActiveRecord::Base 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 + Gitlab::Timeless.timeless(merge_request, &block) end end diff --git a/lib/gitlab/timeless.rb b/lib/gitlab/timeless.rb new file mode 100644 index 00000000000..b290c716f97 --- /dev/null +++ b/lib/gitlab/timeless.rb @@ -0,0 +1,16 @@ +module Gitlab + module Timeless + def self.timeless(model, &block) + original_record_timestamps = model.record_timestamps + model.record_timestamps = false + + if block.arity.abs == 1 + block.call(model) + else + block.call + end + ensure + model.record_timestamps = original_record_timestamps + end + end +end -- cgit v1.2.3 From 9fc0e11e0dbd541c8c0bca60878178ca94ad34e1 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 18:54:53 +0200 Subject: Add DiffFile#blob and #old_blob --- app/helpers/diff_helper.rb | 5 +++-- app/models/repository.rb | 10 --------- app/views/notify/repository_push_email.html.haml | 5 ++--- app/views/projects/diffs/_diffs.html.haml | 4 ++-- app/views/projects/diffs/_file.html.haml | 6 +++--- lib/gitlab/diff/file.rb | 26 +++++++++++++++++++----- 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index a7eedafe314..346b04e40f0 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -100,10 +100,11 @@ module DiffHelper end end - def diff_file_html_data(project, diff_commit, diff_file) + def diff_file_html_data(project, diff_file) + commit = diff_file.content_commit || commit_for_diff(diff_file) { blob_diff_path: namespace_project_blob_diff_path(project.namespace, project, - tree_join(diff_commit.id, diff_file.file_path)) + tree_join(commit.id, diff_file.file_path)) } end diff --git a/app/models/repository.rb b/app/models/repository.rb index 078ca8f4e13..da2fcaa9cd1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -653,16 +653,6 @@ class Repository end end - def blob_for_diff(commit, diff) - blob_at(commit.id, diff.file_path) - end - - def prev_blob_for_diff(commit, diff) - if commit.parent_id - blob_at(commit.parent_id, diff.old_path) - end - end - def refs_contains_sha(ref_type, sha) args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha}) names = Gitlab::Popen.popen(args, path_to_repo).first diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index f1532371b2e..c161ecc3463 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -72,12 +72,11 @@ The diff for this file was not included because it is too large. - else %hr - - diff_commit = diff_file.deleted_file ? @message.diff_refs.first : @message.diff_refs.last - - blob = @message.project.repository.blob_for_diff(diff_commit, diff_file) + - blob = diff_file.blob - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) %table.code.white - diff_file.highlighted_diff_lines.each do |line| - = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: nil, plain: true} + = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true - else No preview for this file type %br diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 151780addc5..8f252282692 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -23,8 +23,8 @@ .files - diff_files.each_with_index do |diff_file, index| - - diff_commit = commit_for_diff(diff_file) - - blob = project.repository.blob_for_diff(diff_commit, diff_file) + - diff_commit = diff_file.content_commit || commit_for_diff(diff_file) + - blob = diff_file.blob(diff_commit) - next unless blob - blob.load_all_data!(project.repository) unless blob.only_display_raw? diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 2395ea3c275..8fc5237e935 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,4 +1,4 @@ -.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)} +.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_file)} .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"} - if diff_file.diff.submodule? %span @@ -52,7 +52,7 @@ - elsif blob.only_display_raw? .nothing-here-block This file is too large to display. - elsif blob.image? - - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file) - = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs + - old_blob = diff_file.old_blob(diff_commit) + = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob, index: i - else .nothing-here-block No preview for this file type diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index e422c333341..8a5c19609e4 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -12,6 +12,12 @@ module Gitlab @diff_refs = diff_refs end + def content_commit + return unless diff_refs + + repository.commit(deleted_file ? old_ref : new_ref) + end + def old_ref diff_refs.try(:base_sha) end @@ -56,11 +62,7 @@ module Gitlab end def file_path - if diff.new_path.present? - diff.new_path - elsif diff.old_path.present? - diff.old_path - end + new_path.presence || old_path.presence end def added_lines @@ -70,6 +72,20 @@ module Gitlab def removed_lines diff_lines.count(&:removed?) end + + def old_blob(commit = content_commit) + return unless commit + + parent_id = commit.parent_id + return unless parent_id + + repository.blob_at(parent_id, old_path) + end + + def blob(commit = content_commit) + return unless commit + repository.blob_at(commit.id, file_path) + end end end end -- cgit v1.2.3 From 4c1bf77c7fd3e62be4b9f6fd54520db0f7fc346b Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 18:55:15 +0200 Subject: Remove unneeded div --- app/views/projects/notes/_diff_notes_with_reply.html.haml | 3 +-- app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml | 6 ++---- app/views/projects/notes/discussions/_notes.html.haml | 3 +-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml index 8144c1ba49e..ec6c4938efc 100644 --- a/app/views/projects/notes/_diff_notes_with_reply.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply.html.haml @@ -4,5 +4,4 @@ %td.notes_content %ul.notes{ data: { discussion_id: note.discussion_id } } = render partial: "projects/notes/note", collection: notes, as: :note - .discussion-reply-holder - = link_to_reply_discussion(note) + = link_to_reply_discussion(note) diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml index 45986b0d1e8..e50a4f86d03 100644 --- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml +++ b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml @@ -8,8 +8,7 @@ %ul.notes{ data: { discussion_id: note_left.discussion_id } } = render partial: "projects/notes/note", collection: notes_left, as: :note - .discussion-reply-holder - = link_to_reply_discussion(note_left, 'old') + = link_to_reply_discussion(note_left, 'old') - else %td.notes_line.old= "" %td.notes_content.parallel.old= "" @@ -20,8 +19,7 @@ %ul.notes{ data: { discussion_id: note_right.discussion_id } } = render partial: "projects/notes/note", collection: notes_right, as: :note - .discussion-reply-holder - = link_to_reply_discussion(note_right, 'new') + = link_to_reply_discussion(note_right, 'new') - else %td.notes_line.new= "" %td.notes_content.parallel.new= "" diff --git a/app/views/projects/notes/discussions/_notes.html.haml b/app/views/projects/notes/discussions/_notes.html.haml index e598e3c7c63..a785149549d 100644 --- a/app/views/projects/notes/discussions/_notes.html.haml +++ b/app/views/projects/notes/discussions/_notes.html.haml @@ -3,5 +3,4 @@ .notes{ data: { discussion_id: note.discussion_id } } %ul.notes.timeline = render partial: "projects/notes/note", collection: discussion_notes, as: :note - .discussion-reply-holder - = link_to_reply_discussion(note) + = link_to_reply_discussion(note) -- cgit v1.2.3 From 375193455aa5cb752f1035a6cc69160170a58477 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 18:57:10 +0200 Subject: Style diff and blob file headers the same way --- app/assets/stylesheets/framework/files.scss | 2 +- app/helpers/application_helper.rb | 11 ++++++ app/helpers/notes_helper.rb | 10 +++++ .../notify/note_merge_request_email.html.haml | 4 +- app/views/projects/diffs/_file.html.haml | 45 ++++++++-------------- app/views/projects/diffs/_file_header.html.haml | 25 ++++++++++++ .../notes/discussions/_diff_with_notes.html.haml | 19 ++++----- lib/gitlab/diff/file.rb | 15 +++----- 8 files changed, 77 insertions(+), 54 deletions(-) create mode 100644 app/views/projects/diffs/_file_header.html.haml diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 71a9f79be3e..71e4b50f2af 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -50,7 +50,7 @@ } a:not(.btn) { - color: $gl-dark-link-color; + color: $gl-text-color; } .left-options { diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 62d13a4b4f3..03495cf5ec4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -306,4 +306,15 @@ module ApplicationHelper def truncate_first_line(message, length = 50) truncate(message.each_line.first.chomp, length: length) if message end + + # While similarly named to Rails's `link_to_if`, this method behaves quite differently. + # If `condition` is truthy, a link will be returned with the result of the block + # as its body. If `condition` is falsy, only the result of the block will be returned. + def conditional_link_to(condition, options, html_options = {}, &block) + if condition + link_to options, html_options, &block + else + capture(&block) + end + end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index e85ba76887d..1a97f884508 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -79,4 +79,14 @@ module NotesHelper full_key = { project: note.project, user_id: note.author_id } @max_access_by_user_id[full_key] end + + def diff_note_path(note) + return unless note.diff_note? + + if note.for_merge_request? && note.active? + diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) + elsif note.for_commit? + namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) + end + end end diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml index a3643a00cfe..35c4b862bb7 100644 --- a/app/views/notify/note_merge_request_email.html.haml +++ b/app/views/notify/note_merge_request_email.html.haml @@ -1,7 +1,7 @@ -- if @note.legacy_diff_note? +- if @note.diff_note? %p.details New comment on diff for - = link_to @note.diff_file_path, @target_url + = link_to @note.diff_file.file_path, @target_url \: = render 'note_message' diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 8fc5237e935..3b758a1ec4e 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,29 +1,8 @@ .diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_file)} .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"} - - if diff_file.diff.submodule? - %span - = icon('archive fw') - %span - = submodule_link(blob, @commit.id, project.repository) - - else - = blob_icon blob.mode, blob.name - - = link_to "#diff-#{i}" do - - if diff_file.renamed_file - - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - = old_path - → - = new_path - - else - %span - = diff_file.new_path - - if diff_file.deleted_file - deleted - - - if diff_file.mode_changed? - %small - = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}" + = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{i}" + - unless diff_file.submodule? .file-actions.hidden-xs - if blob_text_viewable?(blob) = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do @@ -42,15 +21,21 @@ - return unless blob.respond_to?(:text?) - if diff_file.too_large? .nothing-here-block This diff could not be displayed because it is too large. - - elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob) - .nothing-here-block This diff was suppressed by a .gitattributes entry. - - elsif blob_text_viewable?(blob) - - if diff_view == 'parallel' - = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - - else - = render "projects/diffs/text_file", diff_file: diff_file, index: i - elsif blob.only_display_raw? .nothing-here-block This file is too large to display. + - elsif blob_text_viewable?(blob) + - if !project.repository.diffable?(blob) + .nothing-here-block This diff was suppressed by a .gitattributes entry. + - elsif diff_file.diff_lines.length > 0 + - if diff_view == 'parallel' + = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i + - else + = render "projects/diffs/text_file", diff_file: diff_file, index: i + - else + - if diff_file.mode_changed? + .nothing-here-block File mode changed + - elsif diff_file.renamed_file + .nothing-here-block File moved - elsif blob.image? - old_blob = diff_file.old_blob(diff_commit) = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob, index: i diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml new file mode 100644 index 00000000000..95a2772fd0b --- /dev/null +++ b/app/views/projects/diffs/_file_header.html.haml @@ -0,0 +1,25 @@ +- if defined?(blob) && blob && diff_file.submodule? + %span + = icon('archive fw') + %span + = submodule_link(blob, diff_commit.id, project.repository) +- else + = conditional_link_to url.present?, url do + = blob_icon diff_file.b_mode, diff_file.file_path + + - if diff_file.renamed_file + - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) + %strong + = old_path + → + %strong + = new_path + - else + %strong + = diff_file.new_path + - if diff_file.deleted_file + deleted + + - if diff_file.mode_changed? + %small + = "#{diff_file.a_mode} → #{diff_file.b_mode}" diff --git a/app/views/projects/notes/discussions/_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml index 6401245bf73..b924ed31b42 100644 --- a/app/views/projects/notes/discussions/_diff_with_notes.html.haml +++ b/app/views/projects/notes/discussions/_diff_with_notes.html.haml @@ -1,16 +1,13 @@ - note = discussion_notes.first -- diff = note.diff -- return unless diff +- diff_file = note.diff_file +- return unless diff_file + +- blob = note.blob + +.diff-file.file-holder + .file-title + = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: note.project, url: diff_note_path(note) -.diff-file - .diff-header - %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.code.js-syntax-highlight %table - note.truncated_diff_lines.each do |line| diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 8a5c19609e4..eaa6fdff8aa 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -4,7 +4,8 @@ module Gitlab attr_reader :diff, :repository, :diff_refs delegate :new_file, :deleted_file, :renamed_file, - :old_path, :new_path, to: :diff, prefix: false + :old_path, :new_path, :a_mode, :b_mode, + :submodule?, :too_large?, to: :diff, prefix: false def initialize(diff, repository:, diff_refs: nil) @diff = diff @@ -40,11 +41,7 @@ module Gitlab end def mode_changed? - !!(diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode) - end - - def parser - Gitlab::Diff::Parser.new + a_mode && b_mode && a_mode != b_mode end def raw_diff @@ -56,13 +53,11 @@ module Gitlab end def prev_line(index) - if index > 0 - diff_lines[index - 1] - end + diff_lines[index - 1] if index > 0 end def file_path - new_path.presence || old_path.presence + new_path.presence || old_path end def added_lines -- cgit v1.2.3 From a27462a5c6da0182f6b3a55c9417e6405f2c0415 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:15:44 +0200 Subject: Extract parts of LegacyDiffNote into DiffOnNote concern and move part of responsibility to other classes --- app/helpers/diff_helper.rb | 4 -- app/helpers/notes_helper.rb | 7 +-- app/models/concerns/note_on_diff.rb | 53 +++++++++++++++++ app/models/legacy_diff_note.rb | 69 ++++------------------ app/models/note.rb | 4 +- app/models/sent_notification.rb | 16 +++-- app/views/projects/diffs/_line.html.haml | 1 + .../notes/discussions/_diff_with_notes.html.haml | 4 +- lib/api/entities.rb | 6 +- lib/gitlab/diff/file.rb | 10 ++++ lib/gitlab/diff/highlight.rb | 16 ++--- lib/gitlab/diff/line.rb | 16 +++++ lib/gitlab/diff/parallel_diff.rb | 16 ++--- 13 files changed, 120 insertions(+), 102 deletions(-) create mode 100644 app/models/concerns/note_on_diff.rb diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 346b04e40f0..c7c291516fc 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -34,10 +34,6 @@ module DiffHelper diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } end - def generate_line_code(file_path, line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) - end - def unfold_bottom_class(bottom) bottom ? 'js-unfold-bottom' : '' end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 1a97f884508..721dfcf265f 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -60,10 +60,9 @@ module NotesHelper } if note.diff_note? - data.merge!( - line_code: note.line_code, - note_type: LegacyDiffNote.name - ) + data[:note_type] = note.type + + data.merge!(note.diff_attributes) end button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb new file mode 100644 index 00000000000..b511f33b8fa --- /dev/null +++ b/app/models/concerns/note_on_diff.rb @@ -0,0 +1,53 @@ +module NoteOnDiff + extend ActiveSupport::Concern + + NUMBER_OF_TRUNCATED_DIFF_LINES = 16 + + included do + delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true + end + + def diff_note? + true + end + + def diff_file + raise NotImplementedError + end + + def diff_line + raise NotImplementedError + end + + def for_line?(line) + raise NotImplementedError + end + + def diff_attributes + raise NotImplementedError + end + + def can_be_award_emoji? + false + end + + def truncated_diff_lines + prev_match_line = nil + prev_lines = [] + + highlighted_diff_lines.each do |line| + if line.meta? + prev_lines.clear + prev_match_line = line + else + prev_lines << line + + break if for_line?(line) + + prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES + end + end + + prev_lines + end +end diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 33d2a69ebaf..790dfd4d480 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -1,4 +1,6 @@ class LegacyDiffNote < Note + include NoteOnDiff + serialize :st_diff validates :line_code, presence: true, line_code: true @@ -11,12 +13,12 @@ class LegacyDiffNote < Note end end - def diff_note? + def legacy_diff_note? true end - def legacy_diff_note? - true + def diff_attributes + { line_code: line_code } end def discussion_id @@ -27,61 +29,20 @@ class LegacyDiffNote < Note line_code.split('_')[0] if line_code end - def diff_old_line - line_code.split('_')[1].to_i if line_code - end - - def diff_new_line - line_code.split('_')[2].to_i if line_code - end - def diff @diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map) end - def diff_file_path - diff.new_path.presence || diff.old_path - end - - def diff_lines - @diff_lines ||= Gitlab::Diff::Parser.new.parse(diff.diff.each_line) + def diff_file + @diff_file ||= Gitlab::Diff::File.new(diff, repository: self.project.repository) if diff end def diff_line - @diff_line ||= diff_lines.find { |line| generate_line_code(line) == self.line_code } + @diff_line ||= diff_file.line_for_line_code(self.line_code) end - def diff_line_text - diff_line.try(:text) - end - - def diff_line_type - diff_line.try(:type) - end - - def highlighted_diff_lines - Gitlab::Diff::Highlight.new(diff_lines).highlight - end - - def truncated_diff_lines - max_number_of_lines = 16 - prev_match_line = nil - prev_lines = [] - - highlighted_diff_lines.each do |line| - if line.type == "match" - prev_lines.clear - prev_match_line = line - else - prev_lines << line - - break if generate_line_code(line) == self.line_code - - prev_lines.shift if prev_lines.length >= max_number_of_lines - end - end - - prev_lines + def for_line?(line) + !line.meta? && diff_file.line_code(line) == self.line_code end # Check if this note is part of an "active" discussion @@ -102,7 +63,7 @@ class LegacyDiffNote < Note if noteable_diff parsed_lines = Gitlab::Diff::Parser.new.parse(noteable_diff.diff.each_line) - @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line_text } + @active = parsed_lines.any? { |line_obj| line_obj.text == diff_line.text } else @active = false end @@ -110,10 +71,6 @@ class LegacyDiffNote < Note @active end - def award_emoji_supported? - false - end - private def find_diff @@ -149,10 +106,6 @@ class LegacyDiffNote < Note self.class.where(attributes).last.try(:diff) end - def generate_line_code(line) - Gitlab::Diff::LineCode.generate(diff_file_path, line.new_pos, line.old_pos) - end - # Find the diff on noteable that matches our own def find_noteable_diff diffs = noteable.diffs(Commit.max_diff_options) diff --git a/app/models/note.rb b/app/models/note.rb index 81b5c47b738..0c265064630 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -193,7 +193,7 @@ class Note < ActiveRecord::Base end def award_emoji? - award_emoji_supported? && contains_emoji_only? + can_be_award_emoji? && contains_emoji_only? end def emoji_awardable? @@ -204,7 +204,7 @@ class Note < ActiveRecord::Base self.line_code = nil if self.line_code.blank? end - def award_emoji_supported? + def can_be_award_emoji? noteable.is_a?(Awardable) end diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index a2df899d012..928873fb5c3 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -20,7 +20,7 @@ class SentNotification < ActiveRecord::Base find_by(reply_key: reply_key) end - def record(noteable, recipient_id, reply_key, params = {}) + def record(noteable, recipient_id, reply_key, attrs = {}) return unless reply_key noteable_id = nil @@ -31,7 +31,7 @@ class SentNotification < ActiveRecord::Base noteable_id = noteable.id end - params.reverse_merge!( + attrs.reverse_merge!( project: noteable.project, noteable_type: noteable.class.name, noteable_id: noteable_id, @@ -40,13 +40,17 @@ class SentNotification < ActiveRecord::Base reply_key: reply_key ) - create(params) + create(attrs) end - def record_note(note, recipient_id, reply_key, params = {}) - params[:line_code] = note.line_code + def record_note(note, recipient_id, reply_key, attrs = {}) + if note.diff_note? + attrs[:note_type] = note.type - record(note.noteable, recipient_id, reply_key, params) + attrs.merge!(note.diff_attributes) + end + + record(note.noteable, recipient_id, reply_key, attrs) end end diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index f1577e8a47b..dbdbb6eb754 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,3 +1,4 @@ +- line_code = diff_file.line_code(line) - type = line.type %tr.line_holder{ id: line_code, class: type } - case type diff --git a/app/views/projects/notes/discussions/_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml index b924ed31b42..3866de0f7fa 100644 --- a/app/views/projects/notes/discussions/_diff_with_notes.html.haml +++ b/app/views/projects/notes/discussions/_diff_with_notes.html.haml @@ -12,7 +12,7 @@ %table - note.truncated_diff_lines.each do |line| - type = line.type - - line_code = generate_line_code(note.diff_file_path, line) + - line_code = diff_file.line_code(line) %tr.line_holder{ id: line_code, class: "#{type}" } - if type == "match" %td.old_line.diff-line-num= "..." @@ -23,5 +23,5 @@ %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) - - if line_code == note.line_code + - if note.for_line?(line) = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 8cc4368b5c2..db877d2eeb0 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -240,9 +240,9 @@ module API class CommitNote < Grape::Entity expose :note - expose(:path) { |note| note.diff_file_path if note.legacy_diff_note? } - expose(:line) { |note| note.diff_new_line if note.legacy_diff_note? } - expose(:line_type) { |note| note.diff_line_type if note.legacy_diff_note? } + expose(:path) { |note| note.diff_file.try(:file_path) if note.diff_note? } + expose(:line) { |note| note.diff_line.try(:new_line) if note.diff_note? } + expose(:line_type) { |note| note.diff_line.try(:type) if note.diff_note? } expose :author, using: Entities::UserBasic expose :created_at end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index eaa6fdff8aa..c73208329d5 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -13,6 +13,16 @@ module Gitlab @diff_refs = diff_refs end + def line_code(line) + return if line.meta? + + Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) + end + + def line_for_line_code(code) + diff_lines.find { |line| line_code(line) == code } + end + def content_commit return unless diff_refs diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 3ad68728d65..44ea6bf6102 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -42,10 +42,9 @@ module Gitlab line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' ' - case diff_line.type - when 'new', nil + if diff_line.unchanged? || diff_line.added? rich_line = new_lines[diff_line.new_pos - 1] - when 'old' + elsif diff_line.removed? rich_line = old_lines[diff_line.old_pos - 1] end @@ -60,19 +59,12 @@ module Gitlab def old_lines return unless diff_file - @old_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:old)) + @old_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_old_ref, diff_old_path) end def new_lines return unless diff_file - @new_lines ||= Gitlab::Highlight.highlight_lines(*processing_args(:new)) - end - - def processing_args(version) - ref = send("diff_#{version}_ref") - path = send("diff_#{version}_path") - - [self.repository, ref, path] + @new_lines ||= Gitlab::Highlight.highlight_lines(self.repository, diff_new_ref, diff_new_path) end end end diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 03730b435ad..c6189d660c2 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -9,6 +9,18 @@ module Gitlab @old_pos, @new_pos = old_pos, new_pos end + def old_line + old_pos unless added? || meta? + end + + def new_line + new_pos unless removed? || meta? + end + + def unchanged? + type.nil? + end + def added? type == 'new' end @@ -16,6 +28,10 @@ module Gitlab def removed? type == 'old' end + + def meta? + type == 'match' || type == 'nonewline' + end end end end diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb index 74f9b3c050a..2d15b64fdb0 100644 --- a/lib/gitlab/diff/parallel_diff.rb +++ b/lib/gitlab/diff/parallel_diff.rb @@ -15,7 +15,7 @@ module Gitlab highlighted_diff_lines.each do |line| full_line = line.text type = line.type - line_code = generate_line_code(diff_file.file_path, line) + line_code = diff_file.line_code(line) line_new = line.new_pos line_old = line.old_pos @@ -23,9 +23,9 @@ module Gitlab if next_line next_line = highlighted_diff_lines[next_line.index] - next_line_code = generate_line_code(diff_file.file_path, next_line) + full_next_line = next_line.text + next_line_code = diff_file.line_code(next_line) next_type = next_line.type - next_line = next_line.text end case type @@ -59,8 +59,8 @@ module Gitlab right: { type: next_type, number: line_new, - text: next_line, - line_code: next_line_code + text: full_next_line, + line_code: next_line_code, } } skip_next = true @@ -108,12 +108,6 @@ module Gitlab end lines end - - private - - def generate_line_code(file_path, line) - Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) - end end end end -- cgit v1.2.3 From 9abcc0a98f76984fa843eb0db3cf66bc1107e3ed Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:17:25 +0200 Subject: Add Gitlab::Git::Position --- lib/gitlab/diff/file.rb | 32 ++++++++++ lib/gitlab/diff/position.rb | 150 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 lib/gitlab/diff/position.rb diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index c73208329d5..3941e963c03 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -13,6 +13,20 @@ module Gitlab @diff_refs = diff_refs end + def position(line) + return unless diff_refs + + Position.new( + old_path: old_path, + new_path: new_path, + old_line: line.old_line, + new_line: line.new_line, + base_sha: diff_refs.base_sha, + start_sha: diff_refs.start_sha, + head_sha: diff_refs.head_sha + ) + end + def line_code(line) return if line.meta? @@ -23,6 +37,20 @@ module Gitlab diff_lines.find { |line| line_code(line) == code } end + def line_for_position(pos) + diff_lines.find { |line| position(line) == pos } + end + + def position_for_line_code(code) + line = line_for_line_code(code) + position(line) if line + end + + def line_code_for_position(pos) + line = line_for_position(pos) + line_code(line) if line + end + def content_commit return unless diff_refs @@ -66,6 +94,10 @@ module Gitlab diff_lines[index - 1] if index > 0 end + def paths + [old_path, new_path].compact + end + def file_path new_path.presence || old_path end diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb new file mode 100644 index 00000000000..4eff71859c3 --- /dev/null +++ b/lib/gitlab/diff/position.rb @@ -0,0 +1,150 @@ +# Defines a specific location, identified by paths and line numbers, +# within a specific diff, identified by start, head and base commit ids. +module Gitlab + module Diff + class Position + attr_reader :old_path + attr_reader :new_path + attr_reader :old_line + attr_reader :new_line + attr_reader :base_sha + attr_reader :start_sha + attr_reader :head_sha + + def initialize(attrs = {}) + @old_path = attrs[:old_path] + @new_path = attrs[:new_path] + @old_line = attrs[:old_line] + @new_line = attrs[:new_line] + + if attrs[:diff_refs] + @base_sha = attrs[:diff_refs].base_sha + @start_sha = attrs[:diff_refs].start_sha + @head_sha = attrs[:diff_refs].head_sha + else + @base_sha = attrs[:base_sha] + @start_sha = attrs[:start_sha] + @head_sha = attrs[:head_sha] + end + end + + def init_with(coder) + initialize(coder['attributes']) + + self + end + + def encode_with(coder) + coder['attributes'] = self.to_h + end + + def key + @key ||= [base_sha, start_sha, head_sha, Digest::SHA1.hexdigest(old_path || ""), Digest::SHA1.hexdigest(new_path || ""), old_line, new_line] + end + + def ==(other) + other.is_a?(self.class) && key == other.key + end + + def to_h + { + old_path: old_path, + new_path: new_path, + old_line: old_line, + new_line: new_line, + base_sha: base_sha, + start_sha: start_sha, + head_sha: head_sha + } + end + + def inspect + %(#<#{self.class}:#{object_id} #{to_h}>) + end + + def complete? + file_path.present? && + (old_line || new_line) && + diff_refs.complete? + end + + def to_json + JSON.generate(self.to_h) + end + + def type + if old_line && new_line + nil + elsif new_line + 'new' + else + 'old' + end + end + + def unchanged? + type.nil? + end + + def added? + type == 'new' + end + + def removed? + type == 'old' + end + + def paths + [old_path, new_path].compact.uniq + end + + def file_path + new_path.presence || old_path + end + + def diff_refs + @diff_refs ||= DiffRefs.new(base_sha: base_sha, start_sha: start_sha, head_sha: head_sha) + end + + def diff_file(repository) + @diff_file ||= begin + if RequestStore.active? + key = { + project_id: repository.project.id, + start_sha: start_sha, + head_sha: head_sha, + path: file_path + } + + RequestStore.fetch(key) { find_diff_file(repository) } + else + find_diff_file(repository) + end + end + end + + def diff_line(repository) + @diff_line ||= diff_file(repository).line_for_position(self) + end + + def line_code(repository) + @line_code ||= diff_file(repository).line_code_for_position(self) + end + + private + + def find_diff_file(repository) + diffs = Gitlab::Git::Compare.new( + repository.raw_repository, + start_sha, + head_sha + ).diffs(paths: paths) + + diff = diffs.first + return unless diff + + Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs) + end + end + end +end -- cgit v1.2.3 From e9e06ca627d328fb67771949e36924f73b0067c9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:17:56 +0200 Subject: Add Gitlab::Diff::LineMapper --- lib/gitlab/diff/line_mapper.rb | 64 +++++++++++++++ spec/lib/gitlab/diff/line_mapper_spec.rb | 137 +++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 lib/gitlab/diff/line_mapper.rb create mode 100644 spec/lib/gitlab/diff/line_mapper_spec.rb diff --git a/lib/gitlab/diff/line_mapper.rb b/lib/gitlab/diff/line_mapper.rb new file mode 100644 index 00000000000..bde5b4eedaa --- /dev/null +++ b/lib/gitlab/diff/line_mapper.rb @@ -0,0 +1,64 @@ +# When provided a diff for a specific file, maps old line numbers to new line +# numbers and back, to find out where a specific line in a file was moved by the +# changes. +module Gitlab + module Diff + class LineMapper + attr_accessor :diff_file + + def initialize(diff_file) + @diff_file = diff_file + end + + # Find new line number for old line number. + def old_to_new(old_line) + map_line_number(old_line, from: :old_line, to: :new_line) + end + + # Find old line number for new line number. + def new_to_old(new_line) + map_line_number(new_line, from: :new_line, to: :old_line) + end + + private + + def diff_lines + @diff_lines ||= @diff_file.diff_lines + end + + # Find old line number based on new line number. + def map_line_number(from_line, from:, to:) + # If no diff file could be found, the file wasn't changed, and the + # mapped line number is the same as the specified line number. + return from_line unless diff_file + + # To find the mapped line number for the specified line number, + # we need to find: + # - The diff line with that exact line number, if it is in the diff context + # - The first diff line with a higher line number, if it falls between diff contexts + # - The last known diff line, if it falls after the last diff context + diff_line = diff_lines.find do |diff_line| + diff_from_line = diff_line.send(from) + diff_from_line && diff_from_line >= from_line + end + diff_line ||= diff_lines.last + + # If no diff line could be found, the file wasn't changed, and the + # mapped line number is the same as the specified line number. + return from_line unless diff_line + + diff_from_line = diff_line.send(from) + diff_to_line = diff_line.send(to) + + # If the line was removed, there is no mapped line number. + return unless diff_to_line + + # Because we may not have the diff line with the exact line number + # we were looking for, we need to adjust the mapped line number. + distance = diff_from_line - from_line + + diff_to_line - distance + end + end + end +end diff --git a/spec/lib/gitlab/diff/line_mapper_spec.rb b/spec/lib/gitlab/diff/line_mapper_spec.rb new file mode 100644 index 00000000000..4e50e03bb7e --- /dev/null +++ b/spec/lib/gitlab/diff/line_mapper_spec.rb @@ -0,0 +1,137 @@ +require 'spec_helper' + +describe Gitlab::Diff::LineMapper, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:commit) { project.commit(sample_commit.id) } + let(:diffs) { commit.diffs } + let(:diff) { diffs.first } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) } + subject { described_class.new(diff_file) } + + describe '#old_to_new' do + context "with a diff file" do + let(:mapping) do + { + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + 9 => nil, + # nil => 9, + 10 => 10, + 11 => 11, + 12 => 12, + 13 => nil, + 14 => nil, + # nil => 15, + # nil => 16, + # nil => 17, + # nil => 18, + # nil => 19, + # nil => 20, + 15 => 21, + 16 => 22, + 17 => 23, + 18 => 24, + 19 => 25, + 20 => 26, + 21 => 27, + # nil => 28, + 22 => 29, + 23 => 30, + 24 => 31, + 25 => 32, + 26 => 33, + 27 => 34, + 28 => 35, + 29 => 36, + 30 => 37 + } + end + + it 'returns the new line number for the old line number' do + mapping.each do |old_line, new_line| + expect(subject.old_to_new(old_line)).to eq(new_line) + end + end + end + + context "without a diff file" do + let(:diff_file) { nil } + + it "returns the same line number" do + expect(subject.old_to_new(100)).to eq(100) + end + end + end + + describe '#new_to_old' do + context "with a diff file" do + let(:mapping) do + { + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + # nil => 9, + 9 => nil, + 10 => 10, + 11 => 11, + 12 => 12, + # nil => 13, + # nil => 14, + 13 => nil, + 14 => nil, + 15 => nil, + 16 => nil, + 17 => nil, + 18 => nil, + 19 => nil, + 20 => nil, + 21 => 15, + 22 => 16, + 23 => 17, + 24 => 18, + 25 => 19, + 26 => 20, + 27 => 21, + 28 => nil, + 29 => 22, + 30 => 23, + 31 => 24, + 32 => 25, + 33 => 26, + 34 => 27, + 35 => 28, + 36 => 29, + 37 => 30 + } + end + + it 'returns the old line number for the new line number' do + mapping.each do |new_line, old_line| + expect(subject.new_to_old(new_line)).to eq(old_line) + end + end + end + + context "without a diff file" do + let(:diff_file) { nil } + + it "returns the same line number" do + expect(subject.new_to_old(100)).to eq(100) + end + end + end +end -- cgit v1.2.3 From db65954d78899f6d48a682e0c220cbb4e6f59cb8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:18:18 +0200 Subject: Add Gitlab::Git::PositionTracer --- lib/gitlab/diff/position_tracer.rb | 168 +++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 lib/gitlab/diff/position_tracer.rb diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb new file mode 100644 index 00000000000..4d04f867268 --- /dev/null +++ b/lib/gitlab/diff/position_tracer.rb @@ -0,0 +1,168 @@ +# Finds the diff position in the new diff that corresponds to the same location +# specified by the provided position in the old diff. +module Gitlab + module Diff + class PositionTracer + attr_accessor :repository + attr_accessor :old_diff_refs + attr_accessor :new_diff_refs + attr_accessor :paths + + def initialize(repository:, old_diff_refs:, new_diff_refs:, paths: nil) + @repository = repository + @old_diff_refs = old_diff_refs + @new_diff_refs = new_diff_refs + @paths = paths + end + + def trace(old_position) + return unless old_diff_refs.complete? && new_diff_refs.complete? + return unless old_position.diff_refs == old_diff_refs + + # Suppose we have an MR with source branch `feature` and target branch `master`. + # When the MR was created, the head of `master` was commit A, and the + # head of `feature` was commit B, resulting in the original diff A->B. + # Since creation, `master` was updated to C. + # Now `feature` is being updated to D, and the newly generated MR diff is C->D. + # It is possible that C and D are direct decendants of A and B respectively, + # but this isn't necessarily the case as rebases and merges come into play. + # + # Suppose we have a diff note on the original diff A->B. Now that the MR + # is updated, we need to find out what line in C->D corresponds to the + # line the note was originally created on, so that we can update the diff note's + # records and continue to display it in the right place in the diffs. + # If we cannot find this line in the new diff, this means the diff note is now + # outdated, and we will display that fact to the user. + # + # In the new diff, the file the diff note was originally created on may + # have been renamed, deleted or even created, if the file existed in A and B, + # but was removed in C, and restored in D. + # + # Every diff note stores a Position object that defines a specific location, + # identified by paths and line numbers, within a specific diff, identified + # by start, head and base commit ids. + # + # For diff notes for diff A->B, the position looks like this: + # Position + # base_sha - ID of commit A + # head_sha - ID of commit B + # old_path - path as of A (nil if file was newly created) + # new_path - path as of B (nil if file was deleted) + # old_line - line number as of A (nil if file was newly created) + # new_line - line number as of B (nil if file was deleted) + # + # We can easily update `base_sha` and `head_sha` to hold the IDs of commits C and D, + # but need to find the paths and line numbers as of C and D. + # + # If the file was unchanged or newly created in A->B, the path as of D can be found + # by generating diff B->D ("head to head"), finding the diff file with + # `diff_file.old_path == position.new_path`, and taking `diff_file.new_path`. + # The path as of C can be found by taking diff C->D, finding the diff file + # with that same `new_path` and taking `diff_file.old_path`. + # The line number as of D can be found by using the LineMapper on diff B->D + # and providing the line number as of B. + # The line number as of C can be found by using the LineMapper on diff C->D + # and providing the line number as of D. + # + # If the file was deleted in A->B, the path as of C can be found + # by generating diff A->C ("base to base"), finding the diff file with + # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`. + # The path as of D can be found by taking diff C->D, finding the diff file + # with that same `old_path` and taking `diff_file.new_path`. + # The line number as of C can be found by using the LineMapper on diff A->C + # and providing the line number as of A. + # The line number as of D can be found by using the LineMapper on diff C->D + # and providing the line number as of C. + + results = nil + results ||= trace_added_line(old_position) if old_position.added? || old_position.unchanged? + results ||= trace_removed_line(old_position) if old_position.removed? || old_position.unchanged? + + return unless results + + file_diff, old_line, new_line = results + + Position.new( + old_path: file_diff.old_path, + new_path: file_diff.new_path, + head_sha: new_diff_refs.head_sha, + start_sha: new_diff_refs.start_sha, + base_sha: new_diff_refs.base_sha, + old_line: old_line, + new_line: new_line + ) + end + + private + + def trace_added_line(old_position) + file_path = old_position.new_path + + return unless diff_head_to_head + + file_head_to_head = diff_head_to_head.find { |diff_file| diff_file.old_path == file_path } + + file_path = file_head_to_head.new_path if file_head_to_head + + new_line = LineMapper.new(file_head_to_head).old_to_new(old_position.new_line) + + return unless new_line + + file_diff = new_diffs.find { |diff_file| diff_file.new_path == file_path } + return unless file_diff + + old_line = LineMapper.new(file_diff).new_to_old(new_line) + + [file_diff, old_line, new_line] + end + + def trace_removed_line(old_position) + file_path = old_position.old_path + + return unless diff_base_to_base + + file_base_to_base = diff_base_to_base.find { |diff_file| diff_file.old_path == file_path } + + file_path = file_base_to_base.old_path if file_base_to_base + + old_line = LineMapper.new(file_base_to_base).old_to_new(old_position.old_line) + + return unless old_line + + file_diff = new_diffs.find { |diff_file| diff_file.old_path == file_path } + return unless file_diff + + new_line = LineMapper.new(file_diff).old_to_new(old_line) + + [file_diff, old_line, new_line] + end + + def diff_base_to_base + @diff_base_to_base ||= diff_files(old_diff_refs.base_sha || old_diff_refs.start_sha, new_diff_refs.base_sha || new_diff_refs.start_sha) + end + + def diff_head_to_head + @diff_head_to_head ||= diff_files(old_diff_refs.head_sha, new_diff_refs.head_sha) + end + + def new_diffs + @new_diffs ||= diff_files(new_diff_refs.start_sha, new_diff_refs.head_sha, use_base: true) + end + + def diff_files(start_sha, head_sha, use_base: false) + base_sha = self.repository.merge_base(start_sha, head_sha) || start_sha + + diffs = self.repository.raw_repository.diff( + use_base ? base_sha : start_sha, + head_sha, + {}, + *paths + ) + + diffs.decorate! do |diff| + Gitlab::Diff::File.new(diff, repository: self.repository) + end + end + end + end +end -- cgit v1.2.3 From 2f30d00432e9727581e814062ea6117e1946a981 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:20:39 +0200 Subject: Add DiffNote model --- app/models/diff_note.rb | 99 ++++++++++++++++++++++ app/models/note.rb | 8 +- .../20160508215920_add_positions_to_diff_notes.rb | 6 ++ features/steps/project/merge_requests.rb | 3 +- spec/factories/notes.rb | 33 +++++++- spec/lib/gitlab/note_data_builder_spec.rb | 4 +- spec/lib/gitlab/url_builder_spec.rb | 8 +- spec/models/legacy_diff_note_spec.rb | 16 ++-- 8 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 app/models/diff_note.rb create mode 100644 db/migrate/20160508215920_add_positions_to_diff_notes.rb diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb new file mode 100644 index 00000000000..94c07cc535c --- /dev/null +++ b/app/models/diff_note.rb @@ -0,0 +1,99 @@ +class DiffNote < Note + include NoteOnDiff + + serialize :original_position, Gitlab::Diff::Position + serialize :position, Gitlab::Diff::Position + + validates :original_position, presence: true + validates :position, presence: true + validates :diff_line, presence: true + validates :line_code, presence: true, line_code: true + validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] } + validate :positions_complete + validate :verify_supported + + before_validation :set_original_position, on: :create + before_validation :set_line_code + + class << self + def build_discussion_id(noteable_type, noteable_id, position) + [super(noteable_type, noteable_id), *position.key].join("-") + end + end + + def new_diff_note? + true + end + + def diff_attributes + { position: position.to_json } + end + + def discussion_id + @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position) + end + + def original_discussion_id + @original_discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position) + end + + def position=(new_position) + if new_position.is_a?(String) + new_position = JSON.parse(new_position) rescue nil + end + + if new_position.is_a?(Hash) + new_position = new_position.with_indifferent_access + new_position = Gitlab::Diff::Position.new(new_position) + end + + super(new_position) + end + + def diff_file + @diff_file ||= self.original_position.diff_file(self.project.repository) + end + + def diff_line + @diff_line ||= diff_file.line_for_position(self.original_position) if diff_file + end + + def for_line?(line) + diff_file.position(line) == self.original_position + end + + def active?(diff_refs = nil) + return false unless supported? + return true if for_commit? + + diff_refs ||= self.noteable.diff_refs + + self.position.diff_refs == diff_refs + end + + private + + def supported? + !self.for_merge_request? + end + + def set_original_position + self.original_position = self.position.dup + end + + def set_line_code + self.line_code = self.position.line_code(self.project.repository) + end + + def verify_supported + return if supported? + + errors.add(:noteable, "doesn't support new-style diff notes") + end + + def positions_complete + return if self.original_position.complete? && self.position.complete? + + errors.add(:position, "is invalid") + end +end diff --git a/app/models/note.rb b/app/models/note.rb index 0c265064630..ffffd0c0838 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -56,7 +56,7 @@ class Note < ActiveRecord::Base scope :inc_author, ->{ includes(:author) } scope :inc_author_project_award_emoji, ->{ includes(:project, :author, :award_emoji) } - scope :legacy_diff_notes, ->{ where(type: 'LegacyDiffNote') } + scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) } scope :non_diff_notes, ->{ where(type: ['Note', nil]) } scope :with_associations, -> do @@ -82,7 +82,7 @@ class Note < ActiveRecord::Base end def grouped_diff_notes - legacy_diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) + diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code) end # Searches for notes matching the given query. @@ -115,6 +115,10 @@ class Note < ActiveRecord::Base false end + def new_diff_note? + false + end + def active? true end diff --git a/db/migrate/20160508215920_add_positions_to_diff_notes.rb b/db/migrate/20160508215920_add_positions_to_diff_notes.rb new file mode 100644 index 00000000000..2952c25004e --- /dev/null +++ b/db/migrate/20160508215920_add_positions_to_diff_notes.rb @@ -0,0 +1,6 @@ +class AddPositionsToDiffNotes < ActiveRecord::Migration + def change + add_column :notes, :position, :text + add_column :notes, :original_position, :text + end +end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 3611c187202..da848afd48e 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -272,10 +272,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'user "John Doe" leaves a comment like "Line is wrong" on diff' do mr = MergeRequest.find_by(title: "Bug NS-05") - create(:note_on_merge_request_diff, project: project, + create(:diff_note_on_merge_request, project: project, noteable: mr, author: user_exists("John Doe"), - line_code: sample_commit.line_code, note: 'Line is wrong') end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 696cf276e57..83e38095feb 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -10,21 +10,46 @@ FactoryGirl.define do on_issue factory :note_on_commit, traits: [:on_commit] - factory :note_on_commit_diff, traits: [:on_commit, :on_diff], class: LegacyDiffNote 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], class: LegacyDiffNote factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] + factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote + factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote + + factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do + position do + Gitlab::Diff::Position.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + diff_refs: noteable.diff_refs + ) + end + end + + factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do + position do + Gitlab::Diff::Position.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + diff_refs: project.commit(commit_id).try(:diff_refs) + ) + end + end + trait :on_commit do noteable nil - noteable_id nil noteable_type 'Commit' + noteable_id nil commit_id RepoHelpers.sample_commit.id end - trait :on_diff do + trait :legacy_diff_note do line_code "0_184_184" end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index e848d88182f..3d6bcdfd873 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -27,7 +27,7 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on commit diff' do - let(:note) { create(:note_on_commit_diff, project: project) } + let(:note) { create(:diff_note_on_commit, project: project) } it 'returns the note and commit-specific data' do expect(data).to have_key(:commit) @@ -90,7 +90,7 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end let(:note) do - create(:note_on_merge_request_diff, noteable: merge_request, + create(:diff_note_on_merge_request, noteable: merge_request, project: project) end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index bf11472407a..a826b24419a 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -43,9 +43,9 @@ describe Gitlab::UrlBuilder, lib: true do end end - context 'on a CommitDiff' do + context 'on a Commit Diff' do it 'returns a proper URL' do - note = build_stubbed(:note_on_commit_diff) + note = build_stubbed(:diff_note_on_commit) url = described_class.build(note) @@ -75,10 +75,10 @@ describe Gitlab::UrlBuilder, lib: true do end end - context 'on a MergeRequestDiff' do + context 'on a MergeRequest Diff' do it 'returns a proper URL' do merge_request = create(:merge_request, iid: 42) - note = build_stubbed(:note_on_merge_request_diff, noteable: merge_request) + note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request) url = described_class.build(note) diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb index b2d06853886..d64d89edbd3 100644 --- a/spec/models/legacy_diff_note_spec.rb +++ b/spec/models/legacy_diff_note_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe LegacyDiffNote, models: true do describe "Commit diff line notes" do - let!(:note) { create(:note_on_commit_diff, note: "+1 from me") } + let!(:note) { create(:legacy_diff_note_on_commit, note: "+1 from me") } let!(:commit) { note.noteable } it "should save a valid note" do @@ -17,7 +17,7 @@ describe LegacyDiffNote, models: true do describe '#active?' do it 'is always true when the note has no associated diff' do - note = build(:note_on_merge_request_diff) + note = build(:legacy_diff_note_on_merge_request) expect(note).to receive(:diff).and_return(nil) @@ -25,7 +25,7 @@ describe LegacyDiffNote, models: true do end it 'is never true when the note has no noteable associated' do - note = build(:note_on_merge_request_diff) + note = build(:legacy_diff_note_on_merge_request) expect(note).to receive(:diff).and_return(double) expect(note).to receive(:noteable).and_return(nil) @@ -34,7 +34,7 @@ describe LegacyDiffNote, models: true do end it 'returns the memoized value if defined' do - note = build(:note_on_merge_request_diff) + note = build(:legacy_diff_note_on_merge_request) note.instance_variable_set(:@active, 'foo') expect(note).not_to receive(:find_noteable_diff) @@ -45,7 +45,7 @@ describe LegacyDiffNote, models: true do context 'for a merge request noteable' do it 'is false when noteable has no matching diff' do merge = build_stubbed(:merge_request, :simple) - note = build(:note_on_merge_request_diff, noteable: merge) + note = build(:legacy_diff_note_on_merge_request, noteable: merge) allow(note).to receive(:diff).and_return(double) expect(note).to receive(:find_noteable_diff).and_return(nil) @@ -63,9 +63,9 @@ describe LegacyDiffNote, models: true do code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) # We're persisting in order to trigger the set_diff callback - note = create(:note_on_merge_request_diff, noteable: merge, - line_code: code, - project: merge.source_project) + note = create(:legacy_diff_note_on_merge_request, noteable: merge, + line_code: code, + project: merge.source_project) # Make sure we don't get a false positive from a guard clause expect(note).to receive(:find_noteable_diff).and_call_original -- cgit v1.2.3 From 8d7dc26d39b65b3ef6e8ec80ed5995ae307c3d3c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:22:08 +0200 Subject: Support new diff notes on MRs with diff_refs --- app/controllers/projects/merge_requests_controller.rb | 1 + app/models/deployment.rb | 6 ++++++ app/models/diff_note.rb | 2 +- app/models/merge_request.rb | 4 ++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index a03eb8513b6..ae0660078f9 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -85,6 +85,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController noteable_id: @merge_request.id } + @use_legacy_diff_notes = !@merge_request.support_new_diff_notes? @grouped_diff_notes = @merge_request.notes.grouped_diff_notes Banzai::NoteRenderer.render( diff --git a/app/models/deployment.rb b/app/models/deployment.rb index e498ca96e3c..520026c18dd 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -11,6 +11,8 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true + after_save :keep_around_commit + def commit project.commit(sha) end @@ -26,4 +28,8 @@ class Deployment < ActiveRecord::Base def last? self == environment.last_deployment end + + def keep_around_commit + project.repository.keep_around(self.sha) + end end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 94c07cc535c..70fbcb4c4e9 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -74,7 +74,7 @@ class DiffNote < Note private def supported? - !self.for_merge_request? + !self.for_merge_request? || self.noteable.support_new_diff_notes? end def set_original_position diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 04ec6d369e5..82e0f5a0b53 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -642,6 +642,10 @@ class MergeRequest < ActiveRecord::Base merge_commit end + def support_new_diff_notes? + diff_refs && diff_refs.complete? + end + def keep_around_commit project.repository.keep_around(self.merge_commit_sha) end -- cgit v1.2.3 From 710c4886911682742d439b15c5a7add7ca0bd898 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:22:39 +0200 Subject: Automatically update diff note positions when MR is pushed to --- app/models/diff_note.rb | 22 ++++++++++- app/models/merge_request.rb | 45 +++++++++++++++++++--- app/services/merge_requests/refresh_service.rb | 4 +- app/services/merge_requests/reopen_service.rb | 2 +- app/services/notes/diff_position_update_service.rb | 30 +++++++++++++++ 5 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 app/services/notes/diff_position_update_service.rb diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 70fbcb4c4e9..881ae5d1cad 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -12,7 +12,7 @@ class DiffNote < Note validate :positions_complete validate :verify_supported - before_validation :set_original_position, on: :create + before_validation :set_original_position, :update_position, on: :create before_validation :set_line_code class << self @@ -71,6 +71,26 @@ class DiffNote < Note self.position.diff_refs == diff_refs end + def update_position + return unless supported? + return if for_commit? + + return if active? + + Notes::DiffPositionUpdateService.new( + self.project, + nil, + old_diff_refs: self.position.diff_refs, + new_diff_refs: self.noteable.diff_refs, + paths: self.position.paths + ).execute(self) + end + + def update_position! + update_position && + Gitlab::Timeless.timeless(self, &:save) + end + private def supported? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 82e0f5a0b53..4624e9d36e9 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -288,14 +288,23 @@ class MergeRequest < ActiveRecord::Base def update_merge_request_diff if source_branch_changed? || target_branch_changed? - reload_code + reload_diff end end - def reload_code - if merge_request_diff && open? - merge_request_diff.reload_content - end + def reload_diff + return unless merge_request_diff && open? + + old_diff_refs = self.diff_refs + + merge_request_diff.reload_content + + new_diff_refs = self.diff_refs + + update_diff_notes_positions( + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs + ) end def check_if_can_be_merged @@ -646,6 +655,32 @@ class MergeRequest < ActiveRecord::Base diff_refs && diff_refs.complete? end + def update_diff_notes_positions(old_diff_refs:, new_diff_refs:) + return unless support_new_diff_notes? + return if new_diff_refs == old_diff_refs + + active_diff_notes = self.notes.diff_notes.select do |note| + note.new_diff_note? && note.active?(old_diff_refs) + end + + return if active_diff_notes.empty? + + paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq + + service = Notes::DiffPositionUpdateService.new( + self.project, + nil, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs, + paths: paths + ) + + active_diff_notes.each do |note| + service.execute(note) + Gitlab::Timeless.timeless(note, &:save) + end + end + def keep_around_commit project.repository.keep_around(self.merge_commit_sha) end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index de79c024428..21490ac77ea 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -60,7 +60,7 @@ module MergeRequests merge_requests.each do |merge_request| if merge_request.source_branch == @branch_name || force_push? - merge_request.reload_code + merge_request.reload_diff merge_request.mark_as_unchecked else mr_commit_ids = merge_request.commits.map(&:id) @@ -68,7 +68,7 @@ module MergeRequests matches = mr_commit_ids & push_commit_ids if matches.any? - merge_request.reload_code + merge_request.reload_diff merge_request.mark_as_unchecked else merge_request.mark_as_unchecked diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb index 8279ad2001b..eb88ae9d11c 100644 --- a/app/services/merge_requests/reopen_service.rb +++ b/app/services/merge_requests/reopen_service.rb @@ -6,7 +6,7 @@ module MergeRequests create_note(merge_request) notification_service.reopen_mr(merge_request, current_user) execute_hooks(merge_request, 'reopen') - merge_request.reload_code + merge_request.reload_diff merge_request.mark_as_unchecked end diff --git a/app/services/notes/diff_position_update_service.rb b/app/services/notes/diff_position_update_service.rb new file mode 100644 index 00000000000..0cb731f5bc3 --- /dev/null +++ b/app/services/notes/diff_position_update_service.rb @@ -0,0 +1,30 @@ +module Notes + class DiffPositionUpdateService < BaseService + def execute(note) + new_position = tracer.trace(note.position) + + # Don't update the position if the type doesn't match, since that means + # the diff line commented on was changed, and the comment is now outdated + old_position = note.position + if new_position && + new_position != old_position && + new_position.type == old_position.type + + note.position = new_position + end + + note + end + + private + + def tracer + @tracer ||= Gitlab::Diff::PositionTracer.new( + repository: project.repository, + old_diff_refs: params[:old_diff_refs], + new_diff_refs: params[:new_diff_refs], + paths: params[:paths] + ) + end + end +end -- cgit v1.2.3 From 521a0a20f7b52a2fb6ea209b39824a752f2613bd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:23:10 +0200 Subject: Allow reply-by-email with diff notes --- app/models/sent_notification.rb | 29 +++++++++++++++++++++- ..._note_type_and_position_to_sent_notification.rb | 22 ++++++++++++++++ lib/gitlab/email/receiver.rb | 10 +------- 3 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 928873fb5c3..016172c6d7e 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -1,4 +1,6 @@ class SentNotification < ActiveRecord::Base + serialize :position, Gitlab::Diff::Position + belongs_to :project belongs_to :noteable, polymorphic: true belongs_to :recipient, class_name: "User" @@ -7,7 +9,7 @@ class SentNotification < ActiveRecord::Base validates :reply_key, uniqueness: true validates :noteable_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? - validates :line_code, line_code: true, allow_blank: true + validate :note_valid after_save :keep_around_commit @@ -74,8 +76,33 @@ class SentNotification < ActiveRecord::Base self.reply_key end + def note_attributes + { + project: self.project, + author: self.recipient, + type: self.note_type, + noteable_type: self.noteable_type, + noteable_id: self.noteable_id, + commit_id: self.commit_id, + line_code: self.line_code, + position: self.position.to_json + } + end + + def create_note(note) + Notes::CreateService.new( + self.project, + self.recipient, + self.note_attributes.merge(note: note) + ).execute + end + private + def note_valid + Note.new(note_attributes.merge(note: "Test")).valid? + end + def keep_around_commit project.repository.keep_around(self.commit_id) end diff --git a/db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb b/db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb new file mode 100644 index 00000000000..4eef16c9408 --- /dev/null +++ b/db/migrate/20160522215720_add_note_type_and_position_to_sent_notification.rb @@ -0,0 +1,22 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddNoteTypeAndPositionToSentNotification < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :sent_notifications, :note_type, :string + add_column :sent_notifications, :position, :text + end +end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 97ef9851d71..1c671a7487b 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -104,15 +104,7 @@ module Gitlab end def create_note(reply) - Notes::CreateService.new( - sent_notification.project, - sent_notification.recipient, - note: reply, - noteable_type: sent_notification.noteable_type, - noteable_id: sent_notification.noteable_id, - commit_id: sent_notification.commit_id, - line_code: sent_notification.line_code - ).execute + sent_notification.create_note(reply) end end end -- cgit v1.2.3 From 29d574868a044fbfdf7a2458fbb3d951cfe58171 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:23:46 +0200 Subject: Display new diff notes and allow creation through the web interface --- app/assets/javascripts/notes.js.coffee | 13 +- app/assets/stylesheets/pages/diff.scss | 10 - app/controllers/projects/notes_controller.rb | 21 +- app/helpers/notes_helper.rb | 56 ++- app/mailers/emails/projects.rb | 3 +- app/views/projects/diffs/_line.html.haml | 15 +- app/views/projects/diffs/_parallel_view.html.haml | 14 +- app/views/projects/diffs/_text_file.html.haml | 13 +- app/views/projects/notes/_form.html.haml | 1 + .../notes/discussions/_diff_with_notes.html.haml | 16 +- features/steps/shared/diff_note.rb | 12 +- lib/gitlab/diff/parallel_diff.rb | 16 +- spec/features/notes_on_merge_requests_spec.rb | 6 +- spec/fixtures/parallel_diff_result.yml | 504 ++++++++++++++++++++- 14 files changed, 622 insertions(+), 78 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 7c1d943667b..0b7d8f64456 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -240,12 +240,16 @@ class @Notes @note_ids.push(note.id) form = $("#new-discussion-note-form-#{note.discussion_id}") + if note.original_discussion_id? and form.length is 0 + form = $("#new-discussion-note-form-#{note.original_discussion_id}") row = form.closest("tr") note_html = $(note.html) note_html.syntaxHighlight() # is this the first note of discussion? discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']") + if note.original_discussion_id? and discussionContainer.length is 0 + discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']") if discussionContainer.length is 0 # insert the note and the reply button after the temp row row.after note.discussion_html @@ -318,6 +322,7 @@ class @Notes form.addClass "js-main-target-form" form.find("#note_line_code").remove() + form.find("#note_position").remove() form.find("#note_type").remove() ### @@ -335,10 +340,12 @@ class @Notes new Autosave textarea, [ "Note" - form.find("#note_commit_id").val() - form.find("#note_line_code").val() form.find("#note_noteable_type").val() form.find("#note_noteable_id").val() + form.find("#note_commit_id").val() + form.find("#note_type").val() + form.find("#note_line_code").val() + form.find("#note_position").val() ] ### @@ -512,10 +519,12 @@ class @Notes setupDiscussionNoteForm: (dataHolder, form) => # setup note target form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}" + form.attr "data-line-code", dataHolder.data("lineCode") form.find("#note_type").val dataHolder.data("noteType") form.find("#line_type").val dataHolder.data("lineType") form.find("#note_commit_id").val dataHolder.data("commitId") form.find("#note_line_code").val dataHolder.data("lineCode") + form.find("#note_position").val dataHolder.attr("data-position") form.find("#note_noteable_type").val dataHolder.data("noteableType") form.find("#note_noteable_id").val dataHolder.data("noteableId") form.find('.js-note-discard') diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 5286b73cc50..21b1c223c88 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -434,13 +434,3 @@ } } } - -.discussion { - .diff-content { - .diff-line-num { - &:before { - content: attr(data-linenumber); - } - } - } -} diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index e14fe26dde7..3eacdbbd067 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -128,7 +128,7 @@ class Projects::NotesController < Projects::ApplicationController elsif note.valid? Banzai::NoteRenderer.render([note], @project, current_user) - { + attrs = { valid: true, id: note.id, discussion_id: note.discussion_id, @@ -138,6 +138,23 @@ class Projects::NotesController < Projects::ApplicationController discussion_html: note_to_discussion_html(note), discussion_with_diff_html: note_to_discussion_with_diff_html(note) } + + # The discussion_id is used to add the comment to the correct discussion + # element on the merge request page. Among other things, the discussion_id + # contains the sha of head commit of the merge request. + # When new commits are pushed into the merge request after the initial + # load of the merge request page, the discussion elements will still have + # the old discussion_ids, with the old head commit sha. The new comment, + # however, will have the new discussion_id with the new commit sha. + # To ensure that these new comments will still end up in the correct + # discussion element, we also send the original discussion_id, with the + # old commit sha, along, and fall back on this value when no discussion + # element with the new discussion_id could be found. + if note.new_diff_note? && note.position != note.original_position + attrs[:original_discussion_id] = note.original_discussion_id + end + + attrs else { valid: false, @@ -154,7 +171,7 @@ class Projects::NotesController < Projects::ApplicationController def note_params params.require(:note).permit( :note, :noteable, :noteable_id, :noteable_type, :project_id, - :attachment, :line_code, :commit_id, :type + :attachment, :line_code, :commit_id, :type, :position ) end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 721dfcf265f..2302e65c537 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -24,23 +24,55 @@ module NotesHelper }.to_json end - def link_to_new_diff_note(line_code, line_type = nil) - discussion_id = LegacyDiffNote.build_discussion_id( - @comments_target[:noteable_type], - @comments_target[:noteable_id] || @comments_target[:commit_id], - line_code - ) + def link_to_new_diff_note(line_code, position, line_type = nil) + use_legacy_diff_note = @use_legacy_diff_notes + # If the controller doesn't force the use of legacy diff notes, we + # determine this on a line-by-line basis by seeing if there already exist + # active legacy diff notes at this line, in which case newly created notes + # will use the legacy technology as well. + # We do this because the discussion_id values of legacy and "new" diff + # notes, which are used to group notes on the merge request discussion tab, + # are incompatible. + # If we didn't, diff notes that would show for the same line on the changes + # tab, would show in different discussions on the discussion tab. + use_legacy_diff_note ||= begin + line_diff_notes = @grouped_diff_notes[line_code] + line_diff_notes && line_diff_notes.any?(&:legacy_diff_note?) + end data = { noteable_type: @comments_target[:noteable_type], noteable_id: @comments_target[:noteable_id], commit_id: @comments_target[:commit_id], line_type: line_type, - line_code: line_code, - note_type: LegacyDiffNote.name, - discussion_id: discussion_id + line_code: line_code } + if use_legacy_diff_note + discussion_id = LegacyDiffNote.build_discussion_id( + @comments_target[:noteable_type], + @comments_target[:noteable_id] || @comments_target[:commit_id], + line_code + ) + + data.merge!( + note_type: LegacyDiffNote.name, + discussion_id: discussion_id + ) + else + discussion_id = DiffNote.build_discussion_id( + @comments_target[:noteable_type], + @comments_target[:noteable_id] || @comments_target[:commit_id], + position + ) + + data.merge!( + position: position.to_json, + note_type: DiffNote.name, + discussion_id: discussion_id + ) + end + button_tag(class: 'btn add-diff-note js-add-diff-note-button', data: data, title: 'Add a comment to this line') do @@ -65,8 +97,10 @@ module NotesHelper data.merge!(note.diff_attributes) end - button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', - data: data, title: 'Add a reply' + content_tag(:div, class: "discussion-reply-holder") do + button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button', + data: data, title: 'Add a reply' + end end def note_max_access_for_user(note) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index e0af7081411..236b6ab00d8 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -29,8 +29,7 @@ module Emails # used in notify layout @target_url = @message.target_url @project = Project.find(project_id) - @diff_notes_disabled = true - + add_project_headers headers['X-GitLab-Author'] = @message.author_username diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index dbdbb6eb754..2e7fdc832ed 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,4 +1,6 @@ +- plain = local_assigns.fetch(:plain, false) - line_code = diff_file.line_code(line) +- position = diff_file.position(line) - type = line.type %tr.line_holder{ id: line_code, class: type } - case type @@ -10,18 +12,19 @@ %td.new_line.diff-line-num %td.line_content.match= line.text - else - %td.old_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } + %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } } - link_text = type == "new" ? " ".html_safe : line.old_pos - - if defined?(plain) && plain + - if plain = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - - if !@diff_notes_disabled && can?(current_user, :create_note, @project) - = link_to_new_diff_note(line_code) + + - if !plain && !@diff_notes_disabled && can?(current_user, :create_note, @project) + = link_to_new_diff_note(line_code, position) %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " ".html_safe : line.new_pos - - if defined?(plain) && plain + - if plain = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code } }= diff_line_content(line.text, type) + %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code, position: position.to_json } }= diff_line_content(line.text, type) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 4ecc9528bd2..59603f6071d 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -17,27 +17,25 @@ %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if !@diff_notes_disabled && can?(current_user, :create_note, @project) - = link_to_new_diff_note(left[:line_code], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code] }}= diff_line_content(left[:text]) + = link_to_new_diff_note(left[:line_code], left[:position], 'old') + %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code], position: left[:position].to_json }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' - new_line_code = right[:line_code] + - new_position = right[:position] - else - new_line_class = nil - new_line_code = left[:line_code] + - new_position = left[:position] %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if !@diff_notes_disabled && can?(current_user, :create_note, @project) - = link_to_new_diff_note(new_line_code, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code }}= diff_line_content(right[:text]) + = link_to_new_diff_note(new_line_code, new_position, 'new') + %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code, position: new_position.to_json }}= diff_line_content(right[:text]) - unless @diff_notes_disabled - notes_left, notes_right = organize_comments(left, right) - if notes_left.present? || notes_right.present? = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right - -- if diff_file.diff.diff.blank? && diff_file.mode_changed? - .file-mode-changed - File mode changed diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 068593a7dd1..192093d1273 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -4,22 +4,17 @@ %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. %table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' } - - last_line = 0 - - diff_file.highlighted_diff_lines.each_with_index do |line, index| - - line_code = generate_line_code(diff_file.file_path, line) + - diff_file.highlighted_diff_lines.each do |line| - last_line = line.new_pos - = render "projects/diffs/line", {line: line, diff_file: diff_file, line_code: line_code} + = render "projects/diffs/line", line: line, diff_file: diff_file - unless @diff_notes_disabled - - diff_notes = @grouped_diff_notes[line_code] + - line_code = diff_file.line_code(line) + - diff_notes = @grouped_diff_notes[line_code] if line_code - if diff_notes = render "projects/notes/diff_notes_with_reply", notes: diff_notes - if last_line > 0 = render "projects/diffs/match_line", { line: "", line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file } - -- if diff_file.diff.blank? && diff_file.mode_changed? - .file-mode-changed - File mode changed diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 03b3f6935d1..7c61ba750fe 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -7,6 +7,7 @@ = f.hidden_field :noteable_id = f.hidden_field :noteable_type = f.hidden_field :type + = f.hidden_field :position = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..." diff --git a/app/views/projects/notes/discussions/_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml index 3866de0f7fa..4a69b8f8840 100644 --- a/app/views/projects/notes/discussions/_diff_with_notes.html.haml +++ b/app/views/projects/notes/discussions/_diff_with_notes.html.haml @@ -11,17 +11,7 @@ .diff-content.code.js-syntax-highlight %table - note.truncated_diff_lines.each do |line| - - type = line.type - - line_code = diff_file.line_code(line) - %tr.line_holder{ id: line_code, class: "#{type}" } - - if type == "match" - %td.old_line.diff-line-num= "..." - %td.new_line.diff-line-num= "..." - %td.line_content.match= line.text - - else - %td.old_line.diff-line-num{ data: { linenumber: type == "new" ? " ".html_safe : line.old_pos } } - %td.new_line.diff-line-num{ data: { linenumber: type == "old" ? " ".html_safe : line.new_pos } } - %td.line_content{ class: ['noteable_line', type, line_code], line_code: line_code }= diff_line_content(line.text, type) + = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true - - if note.for_line?(line) - = render "projects/notes/diff_notes_with_reply", notes: discussion_notes + - if note.for_line?(line) + = render "projects/notes/diff_notes_with_reply", notes: discussion_notes diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index e8b1e4b4879..56ef44ec969 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -23,7 +23,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}-true']") do + page.within("form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Typo, please fix" find(".js-comment-button").trigger("click") sleep 0.05 @@ -33,7 +33,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do click_parallel_diff_line(sample_commit.line_code, 'old') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do + page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Old comment" find(".js-comment-button").trigger("click") end @@ -41,7 +41,7 @@ module SharedDiffNote step 'I leave a diff comment in a parallel view on the right side like "New comment"' do click_parallel_diff_line(sample_commit.line_code, 'new') - page.within("#{diff_file_selector} form[id$='#{sample_commit.line_code}-true']") do + page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "New comment" find(".js-comment-button").trigger("click") end @@ -51,7 +51,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}-true']") do + page.within("form[data-line-code='#{sample_commit.line_code}']") do fill_in "note[note]", with: "Should fix it :smile:" find('.js-md-preview-button').click end @@ -62,7 +62,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.del_line_code) - page.within("form[id$='#{sample_commit.del_line_code}-true']") do + page.within("form[data-line-code='#{sample_commit.del_line_code}']") do fill_in "note[note]", with: "DRY this up" find('.js-md-preview-button').click end @@ -91,7 +91,7 @@ module SharedDiffNote page.within(diff_file_selector) do click_diff_line(sample_commit.line_code) - page.within("form[id$='#{sample_commit.line_code}-true']") do + page.within("form[data-line-code='#{sample_commit.line_code}']") do fill_in 'note[note]', with: ':smile:' click_button('Comment') end diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb index 2d15b64fdb0..1c1fc148123 100644 --- a/lib/gitlab/diff/parallel_diff.rb +++ b/lib/gitlab/diff/parallel_diff.rb @@ -18,6 +18,7 @@ module Gitlab line_code = diff_file.line_code(line) line_new = line.new_pos line_old = line.old_pos + position = diff_file.position(line) next_line = diff_file.next_line(line.index) @@ -26,6 +27,7 @@ module Gitlab full_next_line = next_line.text next_line_code = diff_file.line_code(next_line) next_type = next_line.type + next_position = diff_file.position(next_line) end case type @@ -37,12 +39,14 @@ module Gitlab number: line_old, text: full_line, line_code: line_code, + position: position }, right: { type: type, number: line_new, text: full_line, - line_code: line_code + line_code: line_code, + position: position } } when 'old' @@ -55,12 +59,14 @@ module Gitlab number: line_old, text: full_line, line_code: line_code, + position: position }, right: { type: next_type, number: line_new, text: full_next_line, line_code: next_line_code, + position: next_position, } } skip_next = true @@ -73,12 +79,14 @@ module Gitlab number: line_old, text: full_line, line_code: line_code, + position: position }, right: { type: next_type, number: nil, text: "", - line_code: nil + line_code: nil, + position: nil } } end @@ -95,12 +103,14 @@ module Gitlab number: nil, text: "", line_code: line_code, + position: position }, right: { type: type, number: line_new, text: full_line, - line_code: line_code + line_code: line_code, + position: position } } end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 737efcef45d..5174168713c 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -166,12 +166,14 @@ describe 'Comments', feature: true do click_diff_line is_expected. - to have_css("tr[id='#{line_code}'] + .js-temp-notes-holder form", + to have_css("form[data-line-code='#{line_code}']", count: 1) end it 'should be removed when canceled' do - page.within(".diff-file form[id$='#{line_code}-true']") do + is_expected.to have_css('.js-temp-notes-holder') + + page.within("form[data-line-code='#{line_code}']") do find('.js-close-discussion-note-form').trigger('click') end diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml index a8b7907d4ba..7d01183e3ef 100644 --- a/spec/fixtures/parallel_diff_result.yml +++ b/spec/fixtures/parallel_diff_result.yml @@ -3,322 +3,818 @@ :type: match :number: 6 :text: "@@ -6,12 +6,18 @@ module Popen" - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :line_code: + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: match :number: 6 :text: "@@ -6,12 +6,18 @@ module Popen" - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :line_code: + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 6 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 6 + :new_line: 6 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 6 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 6 + :new_line: 6 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 7 :text: |2 def popen(cmd, path=nil) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 7 + :new_line: 7 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 7 :text: |2 def popen(cmd, path=nil) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 7 + :new_line: 7 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 8 :text: |2 unless cmd.is_a?(Array) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 8 + :new_line: 8 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 8 :text: |2 unless cmd.is_a?(Array) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 8 + :new_line: 8 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: old :number: 9 :text: | - raise "System commands must be given as an array of strings" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 9 + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 9 :text: | + raise RuntimeError, "System commands must be given as an array of strings" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 9 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 10 :text: |2 end :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 10 + :new_line: 10 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 10 :text: |2 end :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 10 + :new_line: 10 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 11 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 11 + :new_line: 11 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 11 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 11 + :new_line: 11 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 12 :text: |2 path ||= Dir.pwd :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 12 + :new_line: 12 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 12 :text: |2 path ||= Dir.pwd :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 12 + :new_line: 12 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: old :number: 13 :text: | - vars = { "PWD" => path } :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 13 + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: old :number: :text: '' :line_code: + :position: - :left: :type: old :number: 14 :text: | - options = { chdir: path } :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 14 + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 13 :text: | + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 13 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 14 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 14 :text: | + vars = { :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 14 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 15 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 15 :text: | + "PWD" => path :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 15 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 16 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 16 :text: | + } :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 16 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 17 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 17 :text: | + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 17 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 18 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 18 :text: | + options = { :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 18 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 19 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 19 :text: | + chdir: path :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 19 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 20 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 20 :text: | + } :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 20 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 15 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 15 + :new_line: 21 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 21 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 15 + :new_line: 21 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 16 :text: |2 unless File.directory?(path) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 16 + :new_line: 22 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 22 :text: |2 unless File.directory?(path) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 16 + :new_line: 22 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 17 :text: |2 FileUtils.mkdir_p(path) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 17 + :new_line: 23 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 23 :text: |2 FileUtils.mkdir_p(path) :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 17 + :new_line: 23 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: match :number: 19 :text: "@@ -19,6 +25,7 @@ module Popen" - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :line_code: + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: match :number: 25 :text: "@@ -19,6 +25,7 @@ module Popen" - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :line_code: + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 19 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 19 + :new_line: 25 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 25 :text: |2 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 19 + :new_line: 25 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 20 :text: |2 @cmd_output = "" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 20 + :new_line: 26 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 26 :text: |2 @cmd_output = "" :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 20 + :new_line: 26 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 21 :text: |2 @cmd_status = 0 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 21 + :new_line: 27 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 27 :text: |2 @cmd_status = 0 :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 21 + :new_line: 27 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: :text: '' :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 28 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: new :number: 28 :text: | + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: + :new_line: 28 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 22 :text: |2 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 22 + :new_line: 29 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 29 :text: |2 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 22 + :new_line: 29 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 23 :text: |2 @cmd_output << stdout.read :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 23 + :new_line: 30 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 30 :text: |2 @cmd_output << stdout.read :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 23 + :new_line: 30 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :left: :type: :number: 24 :text: |2 @cmd_output << stderr.read :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 24 + :new_line: 31 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d :right: :type: :number: 31 :text: |2 @cmd_output << stderr.read :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 + :position: !ruby/object:Gitlab::Diff::Position + attributes: + :old_path: files/ruby/popen.rb + :new_path: files/ruby/popen.rb + :old_line: 24 + :new_line: 31 + :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 + :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- cgit v1.2.3 From e0ee6f085beac9aa1ec5d135305ca0dd0dc3cbb3 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 20 Jun 2016 19:25:36 +0200 Subject: Update schema --- db/schema.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index f6465136e6a..68b9425253c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -593,6 +593,8 @@ ActiveRecord::Schema.define(version: 20160705163108) do t.datetime "updated_at" t.string "base_commit_sha" t.string "real_size" + t.string "head_commit_sha" + t.string "start_commit_sha" end add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree @@ -689,10 +691,12 @@ ActiveRecord::Schema.define(version: 20160705163108) do t.string "line_code" t.string "commit_id" t.integer "noteable_id" - t.boolean "system", default: false, null: false + t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" t.string "type" + t.text "position" + t.text "original_position" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree @@ -881,6 +885,8 @@ ActiveRecord::Schema.define(version: 20160705163108) do t.string "commit_id" t.string "reply_key", null: false t.string "line_code" + t.string "note_type" + t.text "position" end add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree -- cgit v1.2.3 From ebb5f591e3fc5d23bfd1c2707cacdeb76fa42fb6 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 1 Jul 2016 16:52:26 -0400 Subject: Add tests for PositionTracer --- spec/lib/gitlab/diff/position_tracer_spec.rb | 1758 ++++++++++++++++++++++++++ 1 file changed, 1758 insertions(+) create mode 100644 spec/lib/gitlab/diff/position_tracer_spec.rb diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb new file mode 100644 index 00000000000..08312e60f4a --- /dev/null +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -0,0 +1,1758 @@ +require 'spec_helper' + +describe Gitlab::Diff::PositionTracer, lib: true do + # Douwe's diary New York City, 2016-06-28 + # -------------------------------------------------------------------------- + # + # Dear diary, + # + # Ideally, we would have a test for every single diff scenario that can + # occur and that the PositionTracer should correctly trace a position + # through, across the following variables: + # + # - Old diff file type: created, changed, renamed, deleted, unchanged (5) + # - Old diff line type: added, removed, unchanged (3) + # - New diff file type: created, changed, renamed, deleted, unchanged (5) + # - New diff line type: added, removed, unchanged (3) + # - Old-to-new diff line change: kept, moved, undone (3) + # + # This adds up to 5 * 3 * 5 * 3 * 3 = 675 different potential scenarios, + # and 675 different tests to cover them all. In reality, it would be fewer, + # since one cannot have a removed line in a created file diff, for example, + # but for the sake of this diary entry, let's be pessimistic. + # + # Writing these tests is a manual and time consuming process, as every test + # requires the manual construction or finding of a combination of diffs that + # create the exact diff scenario we are looking for, and can take between + # 1 and 10 minutes, depending on the farfetchedness of the scenario and + # complexity of creating it. + # + # This means that writing tests to cover all of these scenarios would end up + # taking between 11 and 112 hours in total, which I do not believe is the + # best use of my time. + # + # A better course of action would be to think of scenarios that are likely + # to occur, but also potentially tricky to trace correctly, and only cover + # those, with a few more obvious scenarios thrown in to cover our bases. + # + # Unfortunately, I only came to the above realization once I was about + # 1/5th of the way through the process of writing ALL THE SPECS, having + # already wasted about 3 hours trying to be thorough. + # + # I did find 2 bugs while writing those though, so that's good. + # + # In any case, all of this means that the tests below will be extremely + # (excessively, unjustifiably) thorough for scenarios where "the file was + # created in the old diff" and then drop off to comparitively lackluster + # testing of other scenarios. + # + # I did still try to cover most of the obvious and potentially tricky + # scenarios, though. + + include RepoHelpers + + let(:project) { create(:project) } + let(:current_user) { project.owner } + let(:repository) { project.repository } + let(:file_name) { "test-file" } + let(:new_file_name) { "#{file_name}-new" } + let(:second_file_name) { "#{file_name}-2" } + let(:branch_name) { "position-tracer-test" } + + let(:old_diff_refs) { raise NotImplementedError } + let(:new_diff_refs) { raise NotImplementedError } + let(:old_position) { raise NotImplementedError } + + let(:position_tracer) { described_class.new(repository: project.repository, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs) } + subject { position_tracer.trace(old_position) } + + def diff_refs(base_commit, head_commit) + Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) + end + + def position(attrs = {}) + attrs.reverse_merge!( + diff_refs: old_diff_refs + ) + Gitlab::Diff::Position.new(attrs) + end + + def expect_new_position(attrs, new_position = subject) + if attrs.nil? + expect(new_position).to be_nil + else + expect(new_position).not_to be_nil + + expect(new_position.diff_refs).to eq(new_diff_refs) + + attrs.each do |attr, value| + expect(new_position.send(attr)).to eq(value) + end + end + end + + def create_branch(new_name, branch_name) + CreateBranchService.new(project, current_user).execute(new_name, branch_name) + end + + def create_file(branch_name, file_name, content) + Files::CreateService.new( + project, + current_user, + source_branch: branch_name, + target_branch: branch_name, + commit_message: "Create file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def update_file(branch_name, file_name, content) + Files::UpdateService.new( + project, + current_user, + source_branch: branch_name, + target_branch: branch_name, + commit_message: "Update file", + file_path: file_name, + file_content: content + ).execute + project.commit(branch_name) + end + + def delete_file(branch_name, file_name) + Files::DeleteService.new( + project, + current_user, + source_branch: branch_name, + target_branch: branch_name, + commit_message: "Delete file", + file_path: file_name + ).execute + project.commit(branch_name) + end + + let(:initial_commit) do + create_branch(branch_name, "master")[:branch].name + project.commit(branch_name) + end + + describe "#trace" do + describe "diff scenarios" do + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + CONTENT + ) + end + + let(:create_second_file_commit) do + create_file_commit + + create_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + E + CONTENT + ) + end + + let(:update_line_commit) do + create_second_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + CONTENT + ) + end + + let(:update_second_file_line_commit) do + update_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + CONTENT + ) + end + + let(:move_line_commit) do + update_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + C + CONTENT + ) + end + + let(:add_second_file_line_commit) do + move_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + EE + F + CONTENT + ) + end + + let(:move_second_file_line_commit) do + add_second_file_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + EE + CONTENT + ) + end + + let(:delete_line_commit) do + move_second_file_line_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:delete_second_file_line_commit) do + delete_line_commit + + update_file( + branch_name, + second_file_name, + <<-CONTENT.strip_heredoc + D + F + CONTENT + ) + end + + let(:delete_file_commit) do + delete_second_file_line_commit + + delete_file(branch_name, file_name) + end + + let(:rename_file_commit) do + delete_file_commit + + create_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + A + CONTENT + ) + end + + let(:update_line_again_commit) do + rename_file_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + BB + AA + CONTENT + ) + end + + let(:move_line_again_commit) do + update_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + BB + CONTENT + ) + end + + let(:delete_line_again_commit) do + move_line_again_commit + + update_file( + branch_name, + new_file_name, + <<-CONTENT.strip_heredoc + AA + CONTENT + ) + end + + context "when the file was created in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_second_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 + A + # 2 + BB + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + new_line: 1 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is renamed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 2 A + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: old_position.new_line, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: old_position.new_line, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, move_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 + AA + # 1 2 BB + # 2 - A + + it "returns the new position" do + expect_new_position( + old_path: file_name, + new_path: new_file_name, + old_line: 1, + new_line: 2 + ) + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 1 BB + # 2 - A + # 2 + AA + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_line_again_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # file_name -> new_file_name + # 1 - BB + # 2 - A + # 1 + AA + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is deleted in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # + # new diff: + # 1 - BB + # 2 - A + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + # 3 - C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 - A + # 2 - BB + # 3 - C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 - BB + # 2 - A + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is unchanged in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 B + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 1) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + BB + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was changed between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 2) } + + # old diff: + # 1 + A + # 2 + B + # 3 + C + # + # new diff: + # 1 1 A + # 2 2 BB + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when that line was deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) } + let(:old_position) { position(new_path: file_name, new_line: 3) } + + # old diff: + # 1 + BB + # 2 + A + # 3 + C + # + # new diff: + # 1 1 BB + # 2 2 A + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + end + + context "when the file was changed in the old diff" do + context "when the file is created in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + # 3 + C + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns nil" do + expect(subject).to be_nil + end + end + + context "when the position pointed at an unchanged line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 1) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 1, new_line: 2) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + BB + # 2 + A + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2, new_line: 2) } + + # old diff: + # 1 1 BB + # 2 2 A + # 3 - C + # + # new diff: + # 1 + A + # 2 + BB + # 3 + C + + it "returns the new position" do + expect_new_position( + new_path: old_position.new_path, + old_line: nil, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 + A + # 2 + B + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + end + + context "when the file is changed in the new diff" do + context "when the position pointed at an added line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: nil, + new_line: old_position.new_line + ) + end + end + + context "when the file's content was changed between the old and the new diff" do + context "when that line was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 BB + # 2 2 A + # 3 - C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 1, + new_line: 1 + ) + end + end + + context "when that line was moved between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(update_line_commit, move_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 - A + # 2 1 BB + # 2 + A + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: 2, + new_line: 1 + ) + end + end + + context "when that line was changed or deleted between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } + + # old diff: + # 1 + BB + # 1 2 A + # 2 - B + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns nil" do + expect(subject).to be_nil + end + end + end + end + + context "when the position pointed at a deleted line in the old diff" do + context "when the file's content was unchanged between the old and the new diff" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } + let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + + it "returns the new position" do + expect_new_position( + old_path: old_position.old_path, + new_path: old_position.new_path, + old_line: old_position.old_line, + new_line: nil + ) + end + end + end + end + end + end + end + + describe "typical use scenarios" do + let(:second_branch_name) { "#{branch_name}-2" } + + def expect_positions(old_attrs, new_attrs) + old_positions = old_attrs.map do |old_attrs| + position(old_attrs) + end + + new_positions = old_positions.map do |old_position| + position_tracer.trace(old_position) + end + + new_positions.zip(new_attrs).each do |new_position, new_attrs| + expect_new_position(new_attrs, new_position) + end + end + + let(:create_file_commit) do + initial_commit + + create_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + B + C + D + E + F + CONTENT + ) + end + + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + B + C + D + E + F + CONTENT + ) + end + + let(:update_file_commit) do + second_create_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + describe "simple push of new commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { old_path: file_name, old_line: 4, new_line: 4 }, + nil, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { old_path: file_name, old_line: 6 }, + { new_path: file_name, new_line: 7 }, + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to overwrite last commit" do + let(:second_create_file_commit) do + create_file_commit + + create_branch(second_branch_name, branch_name) + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + # + # new diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, # C + { old_path: file_name, old_line: 4 }, # - D + { new_path: file_name, new_line: 3 }, # + DD + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, # E + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, # F + { new_path: file_name, new_line: 6 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, + { old_path: file_name, old_line: 4, new_line: 4 }, + nil, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, + { old_path: file_name, old_line: 6 }, + { new_path: file_name, new_line: 7 }, + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "force push to delete last commit" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 - B + # 3 2 C + # 4 - D + # 3 + DD + # 5 4 E + # 6 5 F + # 6 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + { old_path: file_name, old_line: 2 }, + nil, + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, + { old_path: file_name, old_line: 4 }, + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, + { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, + nil, + { new_path: file_name, new_line: 6 }, + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "rebase on top of target branch" do + let(:second_update_file_commit) do + update_file_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + C + DD + E + F + G + CONTENT + ) + end + + let(:update_file_again_commit) do + second_update_file_commit + + update_file( + branch_name, + file_name, + <<-CONTENT.strip_heredoc + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:overwrite_update_file_again_commit) do + update_file_again_commit + + update_file( + second_branch_name, + file_name, + <<-CONTENT.strip_heredoc + Z + Z + Z + A + BB + C + D + E + FF + G + CONTENT + ) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 }, # + G + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "merge of target branch" do + let(:merge_commit) do + update_file_again_commit + + committer = repository.user_to_committer(current_user) + + options = { + message: "Merge branches", + author: committer, + committer: committer + } + + repository.merge(current_user, second_create_file_commit.sha, branch_name, options) + project.commit(branch_name) + end + + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 + Z + # 2 + Z + # 3 + Z + # 1 4 A + # 2 - B + # 5 + BB + # 3 6 C + # 4 7 D + # 5 8 E + # 6 - F + # 9 + FF + # 0 + G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 4 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 5 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 6 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 7 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 8 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 9 }, # + FF + { new_path: file_name, new_line: 10 }, # + G + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + + describe "changing target branch" do + let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + + # old diff: + # 1 1 A + # 2 - B + # 2 + BB + # 3 3 C + # 4 4 D + # 5 5 E + # 6 - F + # 6 + FF + # 7 + G + # + # new diff: + # 1 1 A + # 2 + BB + # 2 3 C + # 3 - DD + # 4 + D + # 4 5 E + # 5 - F + # 6 + FF + # 7 G + + it "returns the new positions" do + old_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, # A + { old_path: file_name, old_line: 2 }, # - B + { new_path: file_name, new_line: 2 }, # + BB + { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, # C + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 4 }, # D + { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, # E + { old_path: file_name, old_line: 6 }, # - F + { new_path: file_name, new_line: 6 }, # + FF + { new_path: file_name, new_line: 7 }, # + G + ] + + new_position_attrs = [ + { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, + nil, + { new_path: file_name, new_line: 2 }, + { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, + { new_path: file_name, new_line: 4 }, + { old_path: file_name, new_path: file_name, old_line: 4, new_line: 5 }, + { old_path: file_name, old_line: 5 }, + { new_path: file_name, new_line: 6 }, + { new_path: file_name, new_line: 7 }, + ] + + expect_positions(old_position_attrs, new_position_attrs) + end + end + end +end -- cgit v1.2.3 From e0ecfe5bd17911b04514bd5601f848d21fa61dbd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 1 Jul 2016 16:52:43 -0400 Subject: Add tests for Position --- spec/lib/gitlab/diff/position_spec.rb | 341 ++++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 spec/lib/gitlab/diff/position_spec.rb diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb new file mode 100644 index 00000000000..cf28628cb96 --- /dev/null +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -0,0 +1,341 @@ +require 'spec_helper' + +describe Gitlab::Diff::Position, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + + describe "position for an added file" do + let(:commit) { project.commit("2ea1f3dec713d940208fb5ce4a38765ecb5d3f73") } + + subject do + described_class.new( + old_path: "files/images/wm.svg", + new_path: "files/images/wm.svg", + old_line: nil, + new_line: 5, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.new_file).to be true + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq("+ Created with Sketch.") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for a changed file" do + let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } + + describe "position for an added line" do + subject do + described_class.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq("+ vars = {") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 15) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for an unchanged line" do + subject do + described_class.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: 16, + new_line: 22, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.unchanged?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq(" unless File.directory?(path)") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for a removed line" do + subject do + described_class.new( + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: 14, + new_line: nil, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.removed?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.text).to eq("- options = { chdir: path }") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 13, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + end + + describe "position for a renamed file" do + let(:commit) { project.commit("6907208d755b60ebeacb2e9dfea74c92c3449a1f") } + + describe "position for an added line" do + subject do + described_class.new( + old_path: "files/js/commit.js.coffee", + new_path: "files/js/commit.coffee", + old_line: nil, + new_line: 4, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq("+ new CommitFile(@)") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 5) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for an unchanged line" do + subject do + described_class.new( + old_path: "files/js/commit.js.coffee", + new_path: "files/js/commit.coffee", + old_line: 3, + new_line: 3, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.unchanged?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.new_line).to eq(subject.new_line) + expect(diff_line.text).to eq(" $('.files .diff-file').each ->") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + + describe "position for a removed line" do + subject do + described_class.new( + old_path: "files/js/commit.js.coffee", + new_path: "files/js/commit.coffee", + old_line: 4, + new_line: nil, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.removed?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.text).to eq("- new CommitFile(this)") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 4, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + end + + describe "position for a deleted file" do + let(:commit) { project.commit("8634272bfad4cf321067c3e94d64d5a253f8321d") } + + subject do + described_class.new( + old_path: "LICENSE", + new_path: "LICENSE", + old_line: 3, + new_line: nil, + diff_refs: commit.diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.deleted_file).to be true + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.removed?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.text).to eq("-Copyright (c) 2014 gitlabhq") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end +end -- cgit v1.2.3 From 213f646f9a00c6f00b4cdcf4c61bf1517972ebb0 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 1 Jul 2016 16:52:55 -0400 Subject: Add tests for DiffPositionUpdateService --- .../notes/diff_position_update_service_spec.rb | 71 ++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 spec/services/notes/diff_position_update_service_spec.rb diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/notes/diff_position_update_service_spec.rb new file mode 100644 index 00000000000..0bca3ea1a09 --- /dev/null +++ b/spec/services/notes/diff_position_update_service_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe Notes::DiffPositionUpdateService, services: true do + let(:project) { create(:project) } + let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") } + let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") } + let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } + + let(:path) { "files/ruby/popen.rb" } + + let(:old_diff_refs) do + Gitlab::Diff::DiffRefs.new( + base_sha: create_commit.parent_id, + head_sha: modify_commit.sha + ) + end + + let(:new_diff_refs) do + Gitlab::Diff::DiffRefs.new( + base_sha: create_commit.parent_id, + head_sha: edit_commit.sha + ) + end + + subject do + described_class.new( + project, + nil, + old_diff_refs: old_diff_refs, + new_diff_refs: new_diff_refs, + paths: [path] + ) + end + + describe "#execute" do + let(:note) { create(:diff_note_on_merge_request, project: project, position: old_position) } + + let(:old_position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: line, + diff_refs: old_diff_refs + ) + end + + context "when the diff line is the same" do + let(:line) { 16 } + + it "updates the position" do + subject.execute(note) + + expect(note.original_position).to eq(old_position) + expect(note.position).not_to eq(old_position) + expect(note.position.new_line).to eq(22) + end + end + + context "when the diff line has changed" do + let(:line) { 9 } + + it "doesn't update the position" do + subject.execute(note) + + expect(note.original_position).to eq(old_position) + expect(note.position).to eq(old_position) + end + end + end +end -- cgit v1.2.3 From 3dff2867b7f57a783897c6fd18257f85ff53307e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 1 Jul 2016 16:53:02 -0400 Subject: Add tests for DiffNote --- spec/models/diff_note_spec.rb | 186 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 spec/models/diff_note_spec.rb diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb new file mode 100644 index 00000000000..9443f7c13f8 --- /dev/null +++ b/spec/models/diff_note_spec.rb @@ -0,0 +1,186 @@ +require 'spec_helper' + +describe DiffNote, models: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:commit) { project.commit(sample_commit.id) } + + let(:path) { "files/ruby/popen.rb" } + + let(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: nil, + new_line: 14, + diff_refs: merge_request.diff_refs + ) + end + + let(:new_position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + old_line: 16, + new_line: 22, + diff_refs: merge_request.diff_refs + ) + end + + subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + + describe "#position=" do + context "when provided a string" do + it "sets the position" do + subject.position = new_position.to_json + + expect(subject.position).to eq(new_position) + end + end + + context "when provided a hash" do + it "sets the position" do + subject.position = new_position.to_h + + expect(subject.position).to eq(new_position) + end + end + + context "when provided a position object" do + it "sets the position" do + subject.position = new_position + + expect(subject.position).to eq(new_position) + end + end + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file + + expect(diff_file.old_path).to eq(position.old_path) + expect(diff_file.new_path).to eq(position.new_path) + expect(diff_file.diff_refs).to eq(position.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line + + expect(diff_line.added?).to be true + expect(diff_line.new_line).to eq(position.new_line) + expect(diff_line.text).to eq("+ vars = {") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.new_line, 15) + + expect(subject.line_code).to eq(line_code) + end + end + + describe "#for_line?" do + context "when provided the correct diff line" do + it "returns true" do + expect(subject.for_line?(subject.diff_line)).to be true + end + end + + context "when provided a different diff line" do + it "returns false" do + some_line = subject.diff_file.diff_lines.first + + expect(subject.for_line?(some_line)).to be false + end + end + end + + describe "#active?" do + context "when noteable is a commit" do + subject { create(:diff_note_on_commit, project: project, position: position) } + + it "returns true" do + expect(subject.active?).to be true + end + end + + context "when noteable is a merge request" do + context "when the merge request's diff refs match that of the diff note" do + it "returns true" do + expect(subject.active?).to be true + end + end + + context "when the merge request's diff refs don't match that of the diff note" do + before do + allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs) + end + + it "returns false" do + expect(subject.active?).to be false + end + end + end + end + + describe "#update_position" do + context "when noteable is a commit" do + subject { create(:diff_note_on_commit, project: project, position: position) } + + it "doesn't use the DiffPositionUpdateService" do + expect(Notes::DiffPositionUpdateService).not_to receive(:new) + + subject.update_position + end + + it "doesn't update the position" do + subject.update_position + + expect(subject.original_position).to eq(position) + expect(subject.position).to eq(position) + end + end + + context "when noteable is a merge request" do + context "when the note is active" do + it "doesn't use the DiffPositionUpdateService" do + expect(Notes::DiffPositionUpdateService).not_to receive(:new) + + subject.update_position + end + + it "doesn't update the position" do + subject.update_position + + expect(subject.original_position).to eq(position) + expect(subject.position).to eq(position) + end + end + + context "when the note is outdated" do + before do + allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs) + end + + it "uses the DiffPositionUpdateService" do + expect(Notes::DiffPositionUpdateService).to receive(:new).with( + project, + nil, + old_diff_refs: position.diff_refs, + new_diff_refs: commit.diff_refs, + paths: [path] + ).and_call_original + expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(subject) + + subject.update_position + end + end + end + end +end -- cgit v1.2.3 From 5ffb848ee6ecb66d4ff1b2d2bb21968f44f33f7e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 1 Jul 2016 17:53:46 -0400 Subject: Add tests for MergeRequest#reload_diff --- spec/models/merge_request_spec.rb | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index bb83676cddf..a4b6ff8f8ad 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe MergeRequest, models: true do + include RepoHelpers + subject { create(:merge_request) } describe 'associations' do @@ -609,4 +611,42 @@ describe MergeRequest, models: true do end end end + + describe "#reload_diff" do + let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) } + + let(:commit) { subject.project.commit(sample_commit.id) } + + it "reloads the diff content" do + expect(subject.merge_request_diff).to receive(:reload_content) + + subject.reload_diff + end + + it "updates diff note positions" do + old_diff_refs = subject.diff_refs + + merge_request_diff = subject.merge_request_diff + + # Update merge_request_diff so that #diff_refs will return commit.diff_refs + allow(merge_request_diff).to receive(:reload_content) do + merge_request_diff.base_commit_sha = commit.parent_id + merge_request_diff.start_commit_sha = commit.parent_id + merge_request_diff.head_commit_sha = commit.sha + end + + expect(Notes::DiffPositionUpdateService).to receive(:new).with( + subject.project, + nil, + old_diff_refs: old_diff_refs, + new_diff_refs: commit.diff_refs, + paths: note.position.paths + ).and_call_original + expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note) + + expect_any_instance_of(DiffNote).to receive(:save).once + + subject.reload_diff + end + end end -- cgit v1.2.3 From ddec2ed0dfb0981bf1f022f705470402e20ef9bc Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 3 Jul 2016 17:01:13 -0400 Subject: Add send_git_patch helper --- app/controllers/projects/merge_requests_controller.rb | 15 +++++++++------ app/helpers/workhorse_helper.rb | 9 ++++++++- lib/gitlab/workhorse.rb | 6 +++--- .../projects/merge_requests_controller_spec.rb | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ae0660078f9..df1943dd9bb 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -58,14 +58,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html - format.json { render json: @merge_request } + + format.json do + render json: @merge_request + end + format.patch do - headers.store(*Gitlab::Workhorse.send_git_patch(@project.repository, - @merge_request.diff_base_commit.id, - @merge_request.last_commit.id)) - headers['Content-Disposition'] = 'inline' - head :ok + return render_404 unless @merge_request.diff_refs + + send_git_patch @project.repository, @merge_request.diff_refs end + format.diff do return render_404 unless @merge_request.diff_refs diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index 2bd0dbfd095..65598ad9ed3 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -1,4 +1,4 @@ -# Helpers to send Git blobs, diffs or archives through Workhorse. +# Helpers to send Git blobs, diffs, patches or archives through Workhorse. # Workhorse will also serve files when using `send_file`. module WorkhorseHelper # Send a Git blob through Workhorse @@ -16,6 +16,13 @@ module WorkhorseHelper head :ok end + # Send a Git patch through Workhorse + def send_git_patch(repository, diff_refs) + headers.store(*Gitlab::Workhorse.send_git_patch(repository, diff_refs)) + headers['Content-Disposition'] = 'inline' + head :ok + end + # Archive a Git repository and send it through Workhorse def send_git_archive(repository, ref:, format:) headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format)) diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 41b6854cbe1..bc0193a6c32 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -50,11 +50,11 @@ module Gitlab ] end - def send_git_patch(repository, from, to) + def send_git_patch(repository, diff_refs) params = { 'RepoPath' => repository.path_to_repo, - 'ShaFrom' => from, - 'ShaTo' => to + 'ShaFrom' => diff_refs.start_sha, + 'ShaTo' => diff_refs.head_sha } [ diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 2d2fb87f14e..c4b57e77804 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -103,7 +103,7 @@ describe Projects::MergeRequestsController do id: merge_request.iid, format: :patch) - expect(response.headers['Gitlab-Workhorse-Send-Data']).to start_with("git-format-patch:") + expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:") end end end -- cgit v1.2.3 From d7e8479ee8c7a127ed9cd984755a4ea3a8f1c078 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 4 Jul 2016 12:26:28 -0400 Subject: Keep around DiffNote position commits --- app/models/diff_note.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 881ae5d1cad..cdd1c4b4aef 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -14,6 +14,7 @@ class DiffNote < Note before_validation :set_original_position, :update_position, on: :create before_validation :set_line_code + after_save :keep_around_commits class << self def build_discussion_id(noteable_type, noteable_id, position) @@ -116,4 +117,16 @@ class DiffNote < Note errors.add(:position, "is invalid") end + + def keep_around_commits + project.repository.keep_around(self.original_position.base_sha) + project.repository.keep_around(self.original_position.start_sha) + project.repository.keep_around(self.original_position.head_sha) + + if self.position != self.original_position + project.repository.keep_around(self.position.base_sha) + project.repository.keep_around(self.position.start_sha) + project.repository.keep_around(self.position.head_sha) + end + end end -- cgit v1.2.3 From 0a5e387284d2090172b17e6732cbf6b424768cbd Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 4 Jul 2016 12:35:56 -0400 Subject: On MRs being imported, commits aren't available yet --- app/models/merge_request_diff.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 60f4b44a5d1..b81ab817dfe 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -24,7 +24,7 @@ class MergeRequestDiff < ActiveRecord::Base serialize :st_diffs after_create :reload_content, unless: :importing? - after_save :keep_around_commits + after_save :keep_around_commits, unless: :importing? def reload_content reload_commits -- cgit v1.2.3 From d9c75aec3adf23428e4c85a21ce2c357da006b6c Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 4 Jul 2016 13:11:33 -0400 Subject: Use stored start and head for MR diff without whitespace --- app/models/merge_request_diff.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index b81ab817dfe..7ad97a75d53 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -40,8 +40,8 @@ class MergeRequestDiff < ActiveRecord::Base @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( self.repository.raw_repository, - self.target_branch_sha, - self.source_branch_sha, + self.start_commit_sha || self.target_branch_sha, + self.head_commit_sha || self.source_branch_sha, ) compare.diffs(options) end -- cgit v1.2.3 From 228d2a4cb1943c4eda751f80990eed06a3875864 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 6 Jul 2016 19:28:13 -0400 Subject: Add some more code comments. --- app/models/concerns/note_on_diff.rb | 1 + app/models/merge_request.rb | 5 +++-- lib/gitlab/diff/diff_refs.rb | 10 ++++++++++ lib/gitlab/diff/line_mapper.rb | 2 +- lib/gitlab/diff/position.rb | 5 +++++ 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb index b511f33b8fa..6d8b9b76c84 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -31,6 +31,7 @@ module NoteOnDiff false end + # Returns an array of at most 16 highlighted lines above a diff note def truncated_diff_lines prev_match_line = nil prev_lines = [] diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4624e9d36e9..ed99142902e 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -186,8 +186,9 @@ class MergeRequest < ActiveRecord::Base # This will not be the actual base commit if the target branch was merged into # the source branch after the merge request was created, but it is good enough # for the specific purpose of linking to a commit. - # It is not good enough for use in Gitlab::Git::DiffRefs, which need the - # true base commit. + # It is not good enough for use in `Gitlab::Git::DiffRefs`, which needs the + # true base commit, so we can't simply have `#diff_base_commit` fall back on + # this method. def likely_diff_base_commit first_commit.parent || first_commit end diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb index 43489ae876b..8406ca4269c 100644 --- a/lib/gitlab/diff/diff_refs.rb +++ b/lib/gitlab/diff/diff_refs.rb @@ -18,6 +18,16 @@ module Gitlab head_sha == other.head_sha end + # There is only one case in which we will have `start_sha` and `head_sha`, + # but not `base_sha`, which is when a diff is generated between an + # orphaned branch and another branch, which means there _is_ no base, but + # we're still able to highlight it, and to create diff notes, which are + # the primary things `DiffRefs` are used for. + # `DiffRefs` are "complete" when they have `start_sha` and `head_sha`, + # because `base_sha` can always be derived from this, to return an actual + # sha, or `nil`. + # We have `base_sha` directly available on `DiffRefs` because it's faster# + # than having to look it up in the repo every time. def complete? start_sha && head_sha end diff --git a/lib/gitlab/diff/line_mapper.rb b/lib/gitlab/diff/line_mapper.rb index bde5b4eedaa..576a761423e 100644 --- a/lib/gitlab/diff/line_mapper.rb +++ b/lib/gitlab/diff/line_mapper.rb @@ -26,7 +26,7 @@ module Gitlab @diff_lines ||= @diff_file.diff_lines end - # Find old line number based on new line number. + # Find old/new line number based on its old/new counterpart line number. def map_line_number(from_line, from:, to:) # If no diff file could be found, the file wasn't changed, and the # mapped line number is the same as the specified line number. diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index 4eff71859c3..989fff8918e 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -28,6 +28,11 @@ module Gitlab end end + # `Gitlab::Diff::Position` objects are stored as serialized attributes in + # `DiffNote`, which use YAML to encode and decode objects. + # `#init_with` and `#encode_with` can be used to customize the en/decoding + # behavior. In this case, we override these to prevent memoized instance + # variables like `@diff_file` and `@diff_line` from being serialized. def init_with(coder) initialize(coder['attributes']) -- cgit v1.2.3 From 2db75f8debd96c48c6cb8548824dd72bd71fb176 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 6 Jul 2016 19:29:14 -0400 Subject: Make methods private that don't need to be public --- app/models/merge_request_diff.rb | 90 +++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 7ad97a75d53..9c898d9d7c0 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -65,42 +65,36 @@ class MergeRequestDiff < ActiveRecord::Base def base_commit return unless self.base_commit_sha - merge_request.target_project.commit(self.base_commit_sha) + project.commit(self.base_commit_sha) end def start_commit return unless self.start_commit_sha - merge_request.target_project.commit(self.start_commit_sha) + project.commit(self.start_commit_sha) end def head_commit return last_commit unless self.head_commit_sha - merge_request.target_project.commit(self.head_commit_sha) + project.commit(self.head_commit_sha) end - def dump_commits(commits) - commits.map(&:to_hash) - end - - def load_commits(array) - array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) } - end + def compare + @compare ||= + begin + # Update ref for merge request + merge_request.fetch_ref - def dump_diffs(diffs) - if diffs.respond_to?(:map) - diffs.map(&:to_hash) - end + Gitlab::Git::Compare.new( + self.repository.raw_repository, + self.target_branch_sha, + self.source_branch_sha + ) + end end - def load_diffs(raw, options) - if raw.respond_to?(:each) - Gitlab::Git::DiffCollection.new(raw, options) - else - Gitlab::Git::DiffCollection.new([]) - end - end + private # Collect array of Git::Commit objects # between target and source branches @@ -114,6 +108,14 @@ class MergeRequestDiff < ActiveRecord::Base commits end + def dump_commits(commits) + commits.map(&:to_hash) + end + + def load_commits(array) + array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) } + end + # Reload all commits related to current merge request from repo # and save it as array of hashes in st_commits db field def reload_commits @@ -128,6 +130,26 @@ class MergeRequestDiff < ActiveRecord::Base update_columns_serialized(new_attributes) end + # Collect array of Git::Diff objects + # between target and source branches + def unmerged_diffs + compare.diffs(Commit.max_diff_options) + end + + def dump_diffs(diffs) + if diffs.respond_to?(:map) + diffs.map(&:to_hash) + end + end + + def load_diffs(raw, options) + if raw.respond_to?(:each) + Gitlab::Git::DiffCollection.new(raw, options) + else + Gitlab::Git::DiffCollection.new([]) + end + end + # Reload diffs between branches related to current merge request from repo # and save it as array of hashes in st_diffs db field def reload_diffs @@ -164,42 +186,24 @@ class MergeRequestDiff < ActiveRecord::Base keep_around_commits end - # Collect array of Git::Diff objects - # between target and source branches - def unmerged_diffs - compare.diffs(Commit.max_diff_options) + def project + merge_request.target_project end def repository - merge_request.target_project.repository + project.repository end def branch_base_commit return unless self.source_branch_sha && self.target_branch_sha - merge_request.target_project.merge_base_commit(self.source_branch_sha, self.target_branch_sha) + project.merge_base_commit(self.source_branch_sha, self.target_branch_sha) end def branch_base_sha branch_base_commit.try(:sha) end - def compare - @compare ||= - begin - # Update ref for merge request - merge_request.fetch_ref - - Gitlab::Git::Compare.new( - self.repository.raw_repository, - self.target_branch_sha, - self.source_branch_sha - ) - end - end - - private - # # #save or #update_attributes providing changes on serialized attributes do a lot of # serialization and deserialization calls resulting in bad performance. -- cgit v1.2.3 From ac26b23712963b0471db9742fc340ea4fb8935a9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 6 Jul 2016 19:29:41 -0400 Subject: Remove duplication, unused methods, and some other style things --- app/helpers/diff_helper.rb | 4 +++- app/models/concerns/note_on_diff.rb | 2 -- app/models/merge_request.rb | 6 +----- app/views/projects/diffs/_diffs.html.haml | 2 +- lib/gitlab/diff/file.rb | 7 +++---- lib/gitlab/diff/highlight.rb | 11 ++++++----- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index c7c291516fc..eb57516247d 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -89,6 +89,8 @@ module DiffHelper end def commit_for_diff(diff_file) + return diff_file.content_commit if diff_file.content_commit + if diff_file.deleted_file @base_commit || @commit.parent || @commit else @@ -97,7 +99,7 @@ module DiffHelper end def diff_file_html_data(project, diff_file) - commit = diff_file.content_commit || commit_for_diff(diff_file) + commit = commit_for_diff(diff_file) { blob_diff_path: namespace_project_blob_diff_path(project.namespace, project, tree_join(commit.id, diff_file.file_path)) diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb index 6d8b9b76c84..2785fbb21c9 100644 --- a/app/models/concerns/note_on_diff.rb +++ b/app/models/concerns/note_on_diff.rb @@ -33,13 +33,11 @@ module NoteOnDiff # Returns an array of at most 16 highlighted lines above a diff note def truncated_diff_lines - prev_match_line = nil prev_lines = [] highlighted_diff_lines.each do |line| if line.meta? prev_lines.clear - prev_match_line = line else prev_lines << line diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ed99142902e..083e93f1ee7 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -164,10 +164,6 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end - def last_commit - merge_request_diff ? merge_request_diff.last_commit : compare_commits.last - end - def diff_size merge_request_diff.size end @@ -246,7 +242,7 @@ class MergeRequest < ActiveRecord::Base end def diff_refs - return nil unless diff_start_commit || diff_base_commit + return unless diff_start_commit || diff_base_commit Gitlab::Diff::DiffRefs.new( base_sha: diff_base_sha, diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 8f252282692..1975287faee 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -23,7 +23,7 @@ .files - diff_files.each_with_index do |diff_file, index| - - diff_commit = diff_file.content_commit || commit_for_diff(diff_file) + - diff_commit = commit_for_diff(diff_file) - blob = diff_file.blob(diff_commit) - next unless blob - blob.load_all_data!(project.repository) unless blob.only_display_raw? diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 3941e963c03..b0c50edba59 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -21,9 +21,7 @@ module Gitlab new_path: new_path, old_line: line.old_line, new_line: line.new_line, - base_sha: diff_refs.base_sha, - start_sha: diff_refs.start_sha, - head_sha: diff_refs.head_sha + diff_refs: diff_refs ) end @@ -53,7 +51,7 @@ module Gitlab def content_commit return unless diff_refs - + repository.commit(deleted_file ? old_ref : new_ref) end @@ -121,6 +119,7 @@ module Gitlab def blob(commit = content_commit) return unless commit + repository.blob_at(commit.id, file_path) end end diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb index 44ea6bf6102..649a265a02c 100644 --- a/lib/gitlab/diff/highlight.rb +++ b/lib/gitlab/diff/highlight.rb @@ -42,11 +42,12 @@ module Gitlab line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' ' - if diff_line.unchanged? || diff_line.added? - rich_line = new_lines[diff_line.new_pos - 1] - elsif diff_line.removed? - rich_line = old_lines[diff_line.old_pos - 1] - end + rich_line = + if diff_line.unchanged? || diff_line.added? + new_lines[diff_line.new_pos - 1] + elsif diff_line.removed? + old_lines[diff_line.old_pos - 1] + end # Only update text if line is found. This will prevent # issues with submodules given the line only exists in diff content. -- cgit v1.2.3 From d8d5424d25c1738b170d58657ef71d4dbc89ca5e Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Thu, 30 Jun 2016 13:02:05 +0530 Subject: Use the `GLDropdown` component to select protected branches. 1. Modify the component to support a callback for every key press in the filter. We need this so we can update the "Create: ).first().click()` instead of `$(selector)[0].click()`, because the latter is non-standard, and doesn't work in PhantomJS. --- app/assets/javascripts/gl_dropdown.js.coffee | 4 ++- .../javascripts/protected_branch_select.js.coffee | 40 ++++++++++++++++++++++ .../projects/protected_branches_controller.rb | 2 +- .../protected_branches/_dropdown.html.haml | 17 +++++++++ .../projects/protected_branches/index.html.haml | 9 +---- spec/features/protected_branches_spec.rb | 4 ++- 6 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 app/assets/javascripts/protected_branch_select.js.coffee create mode 100644 app/views/projects/protected_branches/_dropdown.html.haml diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index ed9dfcc917e..1c65e833d47 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -56,6 +56,7 @@ class GitLabDropdownFilter return BLUR_KEYCODES.indexOf(keyCode) >= 0 filter: (search_text) -> + @options.onFilter(search_text) if @options.onFilter data = @options.data() if data? and not @options.filterByText @@ -195,6 +196,7 @@ class GitLabDropdown @filter = new GitLabDropdownFilter @filterInput, filterInputBlur: @filterInputBlur filterByText: @options.filterByText + onFilter: @options.onFilter remote: @options.filterRemote query: @options.data keys: searchFields @@ -530,7 +532,7 @@ class GitLabDropdown if $el.length e.preventDefault() e.stopImmediatePropagation() - $(selector, @dropdown)[0].click() + $el.first().trigger('click') addArrowKeyEvent: -> ARROW_KEY_CODES = [38, 40] diff --git a/app/assets/javascripts/protected_branch_select.js.coffee b/app/assets/javascripts/protected_branch_select.js.coffee new file mode 100644 index 00000000000..6d45770ace9 --- /dev/null +++ b/app/assets/javascripts/protected_branch_select.js.coffee @@ -0,0 +1,40 @@ +class @ProtectedBranchSelect + constructor: (currentProject) -> + $('.dropdown-footer').hide(); + @dropdown = $('.js-protected-branch-select').glDropdown( + data: @getProtectedBranches + filterable: true + remote: false + search: + fields: ['title'] + selectable: true + toggleLabel: (selected) -> if (selected and 'id' of selected) then selected.title else 'Protected Branch' + fieldName: 'protected_branch[name]' + text: (protected_branch) -> _.escape(protected_branch.title) + id: (protected_branch) -> _.escape(protected_branch.id) + onFilter: @toggleCreateNewButton + clicked: () -> $('.protect-branch-btn').attr('disabled', false) + ) + + $('.create-new-protected-branch').on 'click', (event) => + # Refresh the dropdown's data, which ends up calling `getProtectedBranches` + @dropdown.data('glDropdown').remote.execute() + @dropdown.data('glDropdown').selectRowAtIndex(event, 0) + + getProtectedBranches: (term, callback) => + if @selectedBranch + callback(gon.open_branches.concat(@selectedBranch)) + else + callback(gon.open_branches) + + toggleCreateNewButton: (branchName) => + @selectedBranch = { title: branchName, id: branchName, text: branchName } + + if branchName is '' + $('.protected-branch-select-footer-list').addClass('hidden') + $('.dropdown-footer').hide(); + else + $('.create-new-protected-branch').text("Create Protected Branch: #{branchName}") + $('.protected-branch-select-footer-list').removeClass('hidden') + $('.dropdown-footer').show(); + diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 026c5b74eb9..80dad758afa 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -9,7 +9,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController def index @protected_branches = @project.protected_branches.order(:name).page(params[:page]) @protected_branch = @project.protected_branches.new - gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name } } }) + gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }) end def create diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml new file mode 100644 index 00000000000..b803d932e67 --- /dev/null +++ b/app/views/projects/protected_branches/_dropdown.html.haml @@ -0,0 +1,17 @@ += f.hidden_field(:name) + += dropdown_tag("Protected Branch", + options: { title: "Pick protected branch", toggle_class: 'js-protected-branch-select js-filter-submit', + filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected branches", + footer_content: true, + data: { show_no: true, show_any: true, show_upcoming: true, + selected: params[:protected_branch_name], + project_id: @project.try(:id) } }) do + + %ul.dropdown-footer-list.hidden.protected-branch-select-footer-list + %li + = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do + Create new + +:javascript + new ProtectedBranchSelect(); diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 684cb175e68..5669713d9a1 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -21,7 +21,7 @@ .form-group = f.label :name, "Branch", class: "label-light" - = f.text_field(:name) + = render partial: "dropdown", locals: { f: f } %p.help-block = link_to "Wildcards", help_page_path(category: 'workflow', file: 'protected_branches', format: 'md', anchor: "wildcard-protected-branches") such as @@ -39,10 +39,3 @@ = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true %hr = render "branches_list" - -:javascript - $("#protected_branch_name").select2({ - placeholder: "Select branch", - createSearchChoice: function(term) { return { id: term, text: term }; }, - data: gon.open_branches - }) diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 9a552e93c24..d94dee0c797 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -7,7 +7,9 @@ feature 'Projected Branches', feature: true, js: true do before { login_as(user) } def set_protected_branch_name(branch_name) - page.execute_script("$('#protected_branch_name').val('#{branch_name}')") + find(".js-protected-branch-select").click + find(".dropdown-input-field").set(branch_name) + click_on "Create Protected Branch: #{branch_name}" end describe "explicit protected branches" do -- cgit v1.2.3 From b1c81f849e5e5b03f56e89cdcefba029ed5c0543 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Wed, 6 Jul 2016 15:07:30 +0530 Subject: Have `Project#open_branches` return branches that are matched by a wildcard protected branch. 1. The `open_branches` method is used to provide a list of branches while creating a protected branch. 2. It makes sense to include branches which are matched by one or more wildcard protected branches, since the user might want to make exact protected branches from these as well. 3. This also provides a large performance improvement. On my machine, in a project with 5000 branches and 2000 protected branches, the `ProtectedBranches#index` page went from a 40 seconds load time to 4 seconds (10x speedup). --- app/models/project.rb | 6 +++++- app/models/protected_branch.rb | 2 +- spec/models/project_spec.rb | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index d5d57bafb98..087f4314565 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -802,8 +802,12 @@ class Project < ActiveRecord::Base @repo_exists = false end + # Branches that are not _exactly_ matched by a protected branch. def open_branches - repository.branches.reject { |branch| self.protected_branch?(branch.name) } + exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name) + branch_names = repository.branches.map(&:name) + non_open_branch_names = Set.new(exact_protected_branch_names).intersection(Set.new(branch_names)) + repository.branches.reject { |branch| non_open_branch_names.include? branch.name } end def root_ref?(branch) diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index d3d5e1d98b2..b7011d7afdf 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -34,7 +34,7 @@ class ProtectedBranch < ActiveRecord::Base # Checks if this protected branch contains a wildcard def wildcard? - self.name.include?('*') + self.name && self.name.include?('*') end protected diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 117ffd551e4..cd126027c95 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -438,12 +438,12 @@ describe Project, models: true do it { expect(project.open_branches.map(&:name)).to include('feature') } it { expect(project.open_branches.map(&:name)).not_to include('master') } - it "does not include branches matching a protected branch wildcard" do + it "includes branches matching a protected branch wildcard" do expect(project.open_branches.map(&:name)).to include('feature') create(:protected_branch, name: 'feat*', project: project) - expect(Project.find(project.id).open_branches.map(&:name)).not_to include('feature') + expect(Project.find(project.id).open_branches.map(&:name)).to include('feature') end end -- cgit v1.2.3 From 91a183bc57e0fccd12d08e861bd8a37b402d81f3 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 7 Jul 2016 09:49:46 +0200 Subject: fix log statements in import/export --- lib/gitlab/import_export/command_line_util.rb | 2 +- lib/gitlab/import_export/saver.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 62c736451b7..2249904145c 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -29,7 +29,7 @@ module Gitlab def execute(cmd) output, status = Gitlab::Popen.popen(cmd) - @shared.error(output.to_s) unless status.zero? + @shared.error(Gitlab::ImportExport::Error.new(output.to_s)) unless status.zero? status.zero? end diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index dd4fdf37309..6a60b65071f 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -17,7 +17,7 @@ module Gitlab Rails.logger.info("Saved project export #{archive_file}") archive_file else - @shared.error("Unable to save #{archive_file} into #{@shared.export_path}") + @shared.error(Gitlab::ImportExport::Error.new("Unable to save #{archive_file} into #{@shared.export_path}")) false end rescue => e -- cgit v1.2.3 From b988644ad853ea00ed23bfc57f83883f96a12016 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 7 Jul 2016 18:56:02 +0800 Subject: Use scope rather than class method --- app/models/ci/build.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5c973749975..e189dbac285 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -13,6 +13,7 @@ module Ci scope :ignore_failures, ->() { where(allow_failure: false) } scope :with_artifacts, ->() { where.not(artifacts_file: nil) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } + scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -25,10 +26,6 @@ module Ci after_create :execute_hooks class << self - def last_month - where('created_at > ?', Date.today - 1.month) - end - def first_pending pending.unstarted.order('created_at ASC').first end -- cgit v1.2.3 From 011e281604c3217591692bcfa20569eb6aef24cd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 7 Jul 2016 19:25:05 +0800 Subject: Prefer ref rather than id because id is shadowing database id Some context: http://doc.gitlab.com/ce/api/repository_files.html#get-file-from-repository http://doc.gitlab.com/ce/api/repositories.html#list-repository-tree Slack: https://gitlab.slack.com/archives/questions/p1467890450002077 --- app/models/project.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 029026a4e56..735b9542d14 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -425,8 +425,8 @@ class Project < ActiveRecord::Base container_registry_repository.tags.any? end - def commit(id = 'HEAD') - repository.commit(id) + def commit(ref = 'HEAD') + repository.commit(ref) end def merge_base_commit(first_commit_id, second_commit_id) -- cgit v1.2.3 From 93dd8b0a088b51874c45bb69a5255529f7e37dd5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 7 Jul 2016 19:33:54 +0800 Subject: Also use ref in Repository#commit --- app/models/repository.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 078ca8f4e13..3b956a30a77 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -78,9 +78,9 @@ class Repository end end - def commit(id = 'HEAD') + def commit(ref = 'HEAD') return nil unless exists? - commit = Gitlab::Git::Commit.find(raw_repository, id) + commit = Gitlab::Git::Commit.find(raw_repository, ref) commit = ::Commit.new(commit, @project) if commit commit rescue Rugged::OdbError -- cgit v1.2.3 From cd63d0a53844507fe7bbf0fabeb89dadecf85fe7 Mon Sep 17 00:00:00 2001 From: winniehell Date: Thu, 7 Jul 2016 14:28:28 +0200 Subject: Escape file extension when parsing search results (!5141) --- CHANGELOG | 1 + app/models/repository.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bc9bb7747a4..7643193451d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ v 8.10.0 (unreleased) - Wrap code blocks on Activies and Todos page. !4783 (winniehell) - Align flash messages with left side of page content !4959 (winniehell) - Display last commit of deleted branch in push events !4699 (winniehell) + - Escape file extension when parsing search results !5141 (winniehell) - Apply the trusted_proxies config to the rack request object for use with rack_attack - Add Sidekiq queue duration to transaction metrics. - Add a new column `artifacts_size` to table `ci_builds` !4964 diff --git a/app/models/repository.rb b/app/models/repository.rb index 078ca8f4e13..d232d422195 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -911,7 +911,7 @@ class Repository if line =~ /^.*:.*:\d+:/ ref, filename, startline = line.split(':') startline = startline.to_i - index - extname = File.extname(filename) + extname = Regexp.escape(File.extname(filename)) basename = filename.sub(/#{extname}$/, '') break end -- cgit v1.2.3 From dad8fc242c646161b353b5f65ab830ea689ff99e Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 7 Jul 2016 15:34:45 +0200 Subject: fix 404 error, redirect back with flash message --- app/controllers/import/gitlab_projects_controller.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 513348c39af..30df1fb2fec 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -27,10 +27,7 @@ class Import::GitlabProjectsController < Import::BaseController notice: "Project '#{@project.name}' is being imported." ) else - redirect_to( - new_import_gitlab_project_path, - alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}" - ) + redirect_back_or_default(options: { alert: "Project could not be imported: #{@project.errors.full_messages.join(', ')}" }) end end -- cgit v1.2.3 From cf46a88b8da55e015fa3af09f956880a3787bf50 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 7 Jul 2016 15:49:16 +0200 Subject: added changelog --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index bc9bb7747a4..937aefefd99 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -48,6 +48,7 @@ v 8.10.0 (unreleased) - Allow '?', or '&' for label names - Fix importer for GitHub Pull Requests when a branch was reused across Pull Requests - Add date when user joined the team on the member page + - Fix 404 redirect after validation fails importing a GitLab project v 8.9.5 - Add more debug info to import/export and memory killer. !5108 -- cgit v1.2.3 From f67984626b7422e6a110d343860f1079243dab07 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 7 Jul 2016 16:12:28 +0200 Subject: added test --- .../features/projects/import_export/import_file_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 9d66f76ef58..bc3bf53fe9d 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -42,6 +42,23 @@ feature 'project import', feature: true, js: true do expect(project.import_status).to eq('finished') end + scenario 'invalid project' do + project = create(:project, namespace_id: 2) + + visit new_project_path + + select2('2', from: '#project_namespace_id') + fill_in :project_path, with: project.name, visible: true + click_link 'GitLab export' + + attach_file('file', file) + click_on 'Import project' + + page.within('.flash-container') do + expect(page).to have_content('Project could not be imported') + end + end + def wiki_exists? wiki = ProjectWiki.new(project) File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty? -- cgit v1.2.3 From 6eb65651a71776725db35528d11f6cef4e0261bf Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jul 2016 14:14:13 -0400 Subject: We can't call private methods with `self.` --- app/models/merge_request_diff.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 9c898d9d7c0..ba235750aeb 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -39,7 +39,7 @@ class MergeRequestDiff < ActiveRecord::Base if options[:ignore_whitespace_change] @diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( - self.repository.raw_repository, + repository.raw_repository, self.start_commit_sha || self.target_branch_sha, self.head_commit_sha || self.source_branch_sha, ) @@ -87,7 +87,7 @@ class MergeRequestDiff < ActiveRecord::Base merge_request.fetch_ref Gitlab::Git::Compare.new( - self.repository.raw_repository, + repository.raw_repository, self.target_branch_sha, self.source_branch_sha ) @@ -228,8 +228,8 @@ class MergeRequestDiff < ActiveRecord::Base end def keep_around_commits - self.repository.keep_around(target_branch_sha) - self.repository.keep_around(source_branch_sha) - self.repository.keep_around(branch_base_sha) + repository.keep_around(target_branch_sha) + repository.keep_around(source_branch_sha) + repository.keep_around(branch_base_sha) end end -- cgit v1.2.3 From 338072cc4ba66cd7eb6fbd9541862946e4ae4b75 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 26 May 2016 17:55:49 -0500 Subject: Layout for Users Groups and Projects on admin area --- CHANGELOG | 1 + app/assets/javascripts/dispatcher.js.coffee | 2 +- app/assets/javascripts/namespace_select.js.coffee | 76 ++++++--- app/assets/stylesheets/framework/dropdowns.scss | 4 + app/assets/stylesheets/framework/nav.scss | 6 + app/assets/stylesheets/pages/admin.scss | 33 ++++ app/assets/stylesheets/pages/groups.scss | 33 ++++ app/assets/stylesheets/pages/projects.scss | 4 - app/assets/stylesheets/pages/search.scss | 2 +- app/controllers/admin/projects_controller.rb | 5 +- app/helpers/dropdowns_helper.rb | 2 +- app/views/admin/groups/_group.html.haml | 42 ++--- app/views/admin/groups/index.html.haml | 57 +++---- app/views/admin/projects/index.html.haml | 171 ++++++++++----------- app/views/admin/projects/show.html.haml | 8 +- app/views/admin/users/_user.html.haml | 42 +++++ app/views/admin/users/index.html.haml | 170 +++++++++----------- app/views/shared/projects/_dropdown.html.haml | 17 +- features/admin/projects.feature | 4 +- features/steps/admin/projects.rb | 9 +- spec/controllers/admin/projects_controller_spec.rb | 4 +- 21 files changed, 396 insertions(+), 296 deletions(-) create mode 100644 app/views/admin/users/_user.html.haml diff --git a/CHANGELOG b/CHANGELOG index cbda28824aa..063ddffb165 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,7 @@ v 8.10.0 (unreleased) - Fix pagination when sorting by columns with lots of ties (like priority) - Updated project header design - Exclude email check from the standard health check + - Updated layout for Projects, Groups, Users on Admin area !4424 - Fix changing issue state columns in milestone view - Add notification settings dropdown for groups - Allow importing from Github using Personal Access Tokens. (Eric K Idema) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 9493a575801..a39df421832 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -127,7 +127,7 @@ class Dispatcher when 'groups' new UsersSelect() when 'projects' - new NamespaceSelect() + new NamespaceSelects() when 'dashboard', 'root' shortcut_handler = new ShortcutsDashboardNavigation() when 'profiles' diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee index a02c4515ccc..3a4b64a18f7 100644 --- a/app/assets/javascripts/namespace_select.js.coffee +++ b/app/assets/javascripts/namespace_select.js.coffee @@ -1,25 +1,53 @@ class @NamespaceSelect - constructor: -> - namespaceFormatResult = (namespace) -> - markup = "
" - markup += "" + namespace.kind + "" - markup += "" + namespace.path + "" - markup += "
" - markup - - formatSelection = (namespace) -> - namespace.kind + ": " + namespace.path - - $('.ajax-namespace-select').each (i, select) -> - $(select).select2 - placeholder: "Search for namespace" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.namespaces query.term, (namespaces) -> - data = { results: namespaces } - query.callback(data) - - dropdownCssClass: "ajax-namespace-dropdown" - formatResult: namespaceFormatResult - formatSelection: formatSelection + constructor: (opts) -> + { + @dropdown + } = opts + + showAny = true + + fieldName = 'namespace_id' + + if @dropdown.data 'fieldName' + fieldName = @dropdown.data 'fieldName' + + @dropdown.glDropdown( + filterable: true + selectable: true + search: + fields: ['path'] + fieldName: fieldName + toggleLabel: (selected) -> + return if not selected.id? then selected.text else "#{selected.kind}: #{selected.path}" + data: (term, dataCallback) -> + Api.namespaces term, (namespaces) -> + if showAny + anyNamespace = + text: 'Any namespace' + id: null + + namespaces.unshift(anyNamespace) + namespaces.splice 1, 0, 'divider' + + dataCallback(namespaces) + text: (namespace) -> + return if not namespace.id? then namespace.text else "#{namespace.kind}: #{namespace.path}" + renderRow: @renderRow + clicked: @onSelectItem + ) + + onSelectItem: (item, el, e) => + e.preventDefault() + +class @NamespaceSelects + constructor: (opts = {}) -> + { + @$dropdowns = $('.js-namespace-select') + } = opts + + @$dropdowns.each (i, dropdown) -> + $dropdown = $(dropdown) + + new NamespaceSelect( + dropdown: $dropdown + ) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index f36736c475e..d4e900f80ef 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -68,6 +68,10 @@ color: $dropdown-toggle-hover-icon-color; } } + + &.large { + width: 200px; + } } .dropdown-menu, diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 6e5f216c894..02ea98e9d94 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -134,6 +134,11 @@ margin-bottom: 0; border-bottom: none; + &.wide { + width: 100%; + display: block; + } + li a { padding: 16px 10px 11px; } @@ -164,6 +169,7 @@ > .btn { margin-right: $gl-padding-top; display: inline-block; + vertical-align: top; &:last-child { margin-right: 0; diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index e05f14e7496..1d34a7f79ae 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -71,3 +71,36 @@ @extend .broadcast-message; margin-bottom: 20px; } + + +// Users List + +.users-list { + .user-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + + .user-details { + flex: 1 1 auto; + } + + .user-name { + display: inline-block; + font-weight: bold; + } + + .controls { + > .btn, > .dropdown { + margin-left: 5px; + } + } + + .dropdown { + .btn-block { + margin-bottom: 0; + line-height: inherit; + } + } +} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 3d79f4400e2..701b9388454 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -38,6 +38,39 @@ margin-right: 15px; } } + + &.group-admin { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + + .group-avatar, .group-details, .group-controls { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + + .group-details { + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + } + + .group-controls { + align-items: center; + + a { + margin-left: 5px; + } + } + } + +} + +.ldap-group-links { + .form-actions { + margin-bottom: $gl-padding; + } } .groups-cover-block { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 3325b586496..bce4aac3334 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -475,10 +475,6 @@ pre.light-well { a:hover { text-decoration: none; } - - > span { - margin-left: 10px; - } } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index ae524cd6bae..9e9b18fdbb8 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -208,7 +208,7 @@ margin-top: 5px; @media (min-width: $screen-sm-min) { - width: 160px; + width: 180px; margin-top: 0; } } diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 4c9c6362ffc..0d2f4f6eb38 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -5,11 +5,12 @@ class Admin::ProjectsController < Admin::ApplicationController def index @projects = Project.all @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? - @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? + @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present? - @projects = @projects.non_archived unless params[:with_archived].present? + @projects = @projects.non_archived unless params[:archived].present? + @projects = @projects.personal(current_user) if params[:personal].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]) diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 7c140538012..4566f3782cc 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -39,7 +39,7 @@ module DropdownsHelper end end - def dropdown_toggle(toggle_text, data_attr, options) + def dropdown_toggle(toggle_text, data_attr, options = {}) content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") output << icon('chevron-down') diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 9025aaac097..59fd6c3fea0 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -1,28 +1,20 @@ - css_class = '' unless local_assigns[:css_class] -- css_class += ' no-description' if group.description.blank? -%li.group-row{ class: css_class } - .controls.hidden-xs - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm' - = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove' +%li.group-row.group-admin{ class: css_class } + .group-avatar + = image_tag group_icon(group), class: 'avatar hidden-xs' + .group-details + .title + = link_to [:admin, group], class: 'group-name' do + = group.name + .group-stats + %span>= pluralize(number_with_delimiter(group.projects.count), 'project') + , + %span= pluralize(number_with_delimiter(group.users.count), 'member') - .stats - %span - = icon('bookmark') - = number_with_delimiter(group.projects.count) - - %span - = icon('users') - = number_with_delimiter(group.users.count) - - %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} - = visibility_level_icon(group.visibility_level, fw: false) - - = image_tag group_icon(group), class: 'avatar s40 hidden-xs' - .title - = link_to [:admin, group], class: 'group-name' do - = group.name - - - if group.description.present? - .description - = markdown(group.description, pipeline: :description) + - if group.description.present? + .description + = markdown(group.description, pipeline: :description) + .group-controls.hidden-xs + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn' + = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 94aa5f5a942..794f910a61f 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -3,41 +3,32 @@ = render "admin/dashboard/head" %div{ class: container_class } - %h3.page-title - Groups (#{number_with_delimiter(@groups.total_count)}) - - %p.light - Group allows you to keep projects organized. - Use groups for uniting related projects. - .top-area - .nav-search - = form_tag admin_groups_path, method: :get, class: 'form-inline' do + .prepend-top-default.append-bottom-default + = form_tag admin_groups_path, method: :get, class: 'js-search-form' do |f| = hidden_field_tag :sort, @sort - = text_field_tag :name, params[:name], class: "form-control" - = button_tag "Search", class: "btn submit btn-primary" - - .nav-controls - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to admin_groups_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to admin_groups_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to admin_groups_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to admin_groups_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - = link_to 'New Group', new_admin_group_path, class: "btn btn-new" - + .search-holder + - project_name = params[:name].present? ? params[:name] : nil + .search-field-holder + = search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name' + = icon("search", class: "search-icon") + .dropdown + - toggle_text = @sort.present? ? sort_options_hash[@sort] : sort_title_recently_created + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Sort by + %li + = link_to admin_groups_path(sort: sort_value_recently_created, name: project_name) do + = sort_title_recently_created + = link_to admin_groups_path(sort: sort_value_oldest_created, name: project_name) do + = sort_title_oldest_created + = link_to admin_groups_path(sort: sort_value_recently_updated, name: project_name) do + = sort_title_recently_updated + = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do + = sort_title_oldest_updated + = link_to new_admin_group_path, class: "btn btn-new" do + New Group %ul.content-list = render @groups diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 7d2eb423223..7fbce25b2c4 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,97 +1,94 @@ - @no_container = true - page_title "Projects" -= render 'shared/show_aside' +- params[:visibility_level] ||= [] + = render "admin/dashboard/head" %div{ class: container_class } - .row.prepend-top-default - %aside.col-md-3 - .panel.admin-filter - = 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" + .top-area + .prepend-top-default + = form_tag admin_namespaces_projects_path, method: :get do |f| + .search-holder + .search-field-holder + = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' + + - if params[:visibility_level].present? + = hidden_field_tag 'visibility_level', params[:visibility_level] + + - if params[:sort].present? + = hidden_field_tag 'sort', params[:sort] + + - if params[:personal].present? + = hidden_field_tag 'visibility_level', 'true' + + - if params[:archived].present? + = hidden_field_tag 'archived', 'true' + + = icon("search", class: "search-icon") + + .dropdown + - toggle_text = 'Search for Namespace' + - if params[:namespace_id].present? + - namespace = Namespace.find(params[:namespace_id]) + - toggle_text = "#{namespace.kind}: #{namespace.path}" + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) + .dropdown-menu.dropdown-select.dropdown-menu-align-right + = dropdown_title('Namespaces') + = dropdown_filter("Search for Namespace") + = dropdown_content + = dropdown_loading + + = button_tag "Search", class: "btn btn-primary btn-search" + + %ul.nav-links + - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path } + = nav_link(opts) do + = link_to admin_namespaces_projects_path do + All - .form-group - = label_tag :namespace_id, "Namespace" - = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large' + = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do + = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do + Private + = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do + = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do + Internal + = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do + = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do + Public - .form-group - %strong Activity - .checkbox - = label_tag :with_push do - = check_box_tag :with_push, 1, params[:with_push] - %span Projects with push events - .checkbox - = label_tag :abandoned do - = check_box_tag :abandoned, 1, params[:abandoned] - %span No activity over 6 month - .checkbox - = label_tag :with_archived do - = check_box_tag :with_archived, 1, params[:with_archived] - %span Show archived projects + .nav-controls + = render 'shared/projects/dropdown' + = link_to new_project_path, class: 'btn btn-new' do + New Project - %fieldset - %strong Visibility level: - .visibility-levels - - Project.visibility_levels.each do |label, level| - .checkbox - %label - = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s) - %span.descr - = visibility_level_icon(level) - = label - %fieldset - %strong Problems - .checkbox - = label_tag :last_repository_check_failed do - = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed] - %span Last repository check failed + .projects-list-holder + - if @projects.any? + %ul.projects-list.content-list + - @projects.each_with_index do |project| + %li.project-row + .controls.pull-right + - if project.archived + %span.label.label-warning archived + %span.label.label-gray + = repository_size(project) + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" + = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" + .title + = link_to [:admin, project.namespace.becomes(Namespace), project] do + .dash-project-avatar + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + %span.project-full-name + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name - = hidden_field_tag :sort, params[:sort] - = button_tag "Search", class: "btn submit btn-primary" - = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" + - if project.description.present? + .description + = markdown(project.description, pipeline: :description) - %section.col-md-9 - .panel.panel-default - .panel-heading - Projects (#{@projects.total_count}) - .controls - .dropdown.inline - %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do - = 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-sm btn-success" - %ul.well-list - - @projects.each do |project| - %li - .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.namespace.becomes(Namespace), project] - .pull-right - - if project.archived - %span.label.label-warning archived - %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-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" + = paginate @projects, theme: 'gitlab' + - else + .nothing-here-block No projects found diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 82d3169c6f9..7e1f5a52c6f 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -99,7 +99,13 @@ .form-group = f.label :new_namespace_id, "Namespace", class: 'control-label' .col-sm-10 - = namespace_select_tag :new_namespace_id, selected: params[:namespace_id], class: 'input-large' + .dropdown + = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' }) + .dropdown-menu.dropdown-select + = dropdown_title('Namespaces') + = dropdown_filter("Search for Namespace") + = dropdown_content + = dropdown_loading .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml new file mode 100644 index 00000000000..463993d0c17 --- /dev/null +++ b/app/views/admin/users/_user.html.haml @@ -0,0 +1,42 @@ +%li.user-row + .user-avatar + = image_tag avatar_icon(user), class: "avatar", alt: '' + .user-details + .user-name + = link_to user.name, [:admin, user] + - if user.blocked? + %span.label.label-danger blocked + - if user.admin? + %span.label.label-success Admin + - if user.external? + %span.label.label-default External + - if user == current_user + %span It's you! + .user-email + = mail_to user.email, user.email + .controls.pull-right + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' + - unless user == current_user + .dropdown.inline + %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } } + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Settings + %li + - if user.ldap_blocked? + %span.small Cannot unblock LDAP blocked users + - elsif user.blocked? + = link_to 'Unblock', unblock_admin_user_path(user), method: :put + - else + = link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put + - if user.access_locked? + %li + = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } + - if user.can_be_removed? + %li.divider + %li + = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, + class: 'btn btn-remove btn-block', + method: :delete diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 21bb99a792c..357123c2c13 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,110 +1,78 @@ - @no_container = true - page_title "Users" -= render 'shared/show_aside' = render "admin/dashboard/head" %div{ class: container_class } - .admin-filter - %ul.nav-links - %li{class: "#{'active' unless params[:filter]}"} - = link_to admin_users_path do - Active - %small.badge= number_with_delimiter(User.active.count) - %li{class: "#{'active' if params[:filter] == "admins"}"} - = link_to admin_users_path(filter: "admins") do - Admins - %small.badge= number_with_delimiter(User.admins.count) - %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} - = link_to admin_users_path(filter: 'two_factor_enabled') do - 2FA Enabled - %small.badge= number_with_delimiter(User.with_two_factor.count) - %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} - = link_to admin_users_path(filter: 'two_factor_disabled') do - 2FA Disabled - %small.badge= number_with_delimiter(User.without_two_factor.count) - %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"} - = link_to admin_users_path(filter: 'external') do - External - %small.badge= number_with_delimiter(User.external.count) - %li{class: "#{'active' if params[:filter] == "blocked"}"} - = link_to admin_users_path(filter: "blocked") do - Blocked - %small.badge= number_with_delimiter(User.blocked.count) - %li{class: "#{'active' if params[:filter] == "wop"}"} - = link_to admin_users_path(filter: "wop") do - Without projects - %small.badge= number_with_delimiter(User.without_projects.count) + .top-area + .prepend-top-default + = form_tag admin_users_path, method: :get do + - if params[:filter].present? + = hidden_field_tag "filter", h(params[:filter]) + .search-holder + .search-field-holder + = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false + = icon("search", class: "search-icon") + .dropdown + - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Sort by + %li + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do + = sort_title_name + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do + = sort_title_recently_signin + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do + = sort_title_oldest_signin + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do + = sort_title_recently_created + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do + = sort_title_oldest_created + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do + = sort_title_recently_updated + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do + = sort_title_oldest_updated + = link_to 'New User', new_admin_user_path, class: 'btn btn-new btn-search' - .row-content-block.second-block - .pull-right - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_name - %b.caret - %ul.dropdown-menu - %li - = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do - = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do - = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do - = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do - = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do - = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do - = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do - = sort_title_oldest_updated + .nav-block + %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs + .fade-left + = nav_link(html_options: { class: ('active' unless params[:filter]) }) do + = link_to admin_users_path do + Active + %small.badge= number_with_delimiter(User.active.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do + = link_to admin_users_path(filter: "admins") do + Admins + %small.badge= number_with_delimiter(User.admins.count) + = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do + = link_to admin_users_path(filter: 'two_factor_enabled') do + 2FA Enabled + %small.badge= number_with_delimiter(User.with_two_factor.count) + = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do + = link_to admin_users_path(filter: 'two_factor_disabled') do + 2FA Disabled + %small.badge= number_with_delimiter(User.without_two_factor.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do + = link_to admin_users_path(filter: 'external') do + External + %small.badge= number_with_delimiter(User.external.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do + = link_to admin_users_path(filter: "blocked") do + Blocked + %small.badge= number_with_delimiter(User.blocked.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do + = link_to admin_users_path(filter: "wop") do + Without projects + %small.badge= number_with_delimiter(User.without_projects.count) + .fade-right - = link_to 'New User', new_admin_user_path, class: "btn btn-new" - = form_tag admin_users_path, method: :get, class: 'form-inline' do - .form-group - = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false - = hidden_field_tag "filter", params[:filter] - = button_tag class: 'btn btn-primary' do - %i.fa.fa-search + %ul.users-list.content-list + - if @users.empty? + %li + .nothing-here-block No users found. + - else + = render partial: 'admin/users/user', collection: @users - - .panel.panel-default - %ul.well-list - - @users.each do |user| - %li - .list-item-name - - if user.blocked? - = icon("lock", class: "cred") - - else - = icon("user", class: "cgreen") - = link_to user.name, [:admin, user] - - if user.admin? - %strong.cred (Admin) - - if user.external? - %strong.cred (External) - - if user == current_user - %span.cred It's you! - .pull-right - %span.light - %i.fa.fa-envelope - = mail_to user.email, user.email, class: 'light' -   - .pull-right - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs' - - unless user == current_user - - if user.ldap_blocked? - = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do - %i.fa.fa-lock - Unblock - - elsif user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success' - - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning' - - if user.access_locked? - = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } - - if user.can_be_removed? - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove' = paginate @users, theme: "gitlab" diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 1169bed0382..b7f8551153b 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,31 +1,30 @@ - @sort ||= sort_value_recently_updated - personal = params[:personal] - archived = params[:archived] +- namespace_id = params[:namespace_id] .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - = projects_sort_options_hash[@sort] - %b.caret + - toggle_text = projects_sort_options_hash[@sort] + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable %li.dropdown-header Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do = title %li.divider %li - = link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do Hide archived projects %li - = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do Show archived projects - if current_user %li.divider %li - = link_to filter_projects_path(sort: @sort, personal: nil), class: ("is-active" unless personal) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil), class: ("is-active" unless personal.present?) do Owned by anyone %li - = link_to filter_projects_path(sort: @sort, personal: true), class: ("is-active" if personal) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do Owned by me diff --git a/features/admin/projects.feature b/features/admin/projects.feature index c5ee80136c8..8929bcf8d80 100644 --- a/features/admin/projects.feature +++ b/features/admin/projects.feature @@ -10,10 +10,11 @@ Feature: Admin Projects Then I should see all non-archived projects And I should not see project "Archive" + @javascript Scenario: I should see all projects in the list Given archived project "Archive" When I visit admin projects page - And I check "Show archived projects" + And I select "Show archived projects" Then I should see all projects And I should see "archived" label @@ -22,6 +23,7 @@ Feature: Admin Projects And I click on first project Then I should see project details + @javascript Scenario: Transfer project Given group 'Web' And I visit admin project page diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index a7a28755a6c..d77945a6b9c 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -18,9 +18,9 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps end end - step 'I check "Show archived projects"' do - page.check 'Show archived projects' - click_button "Search" + step 'I select "Show archived projects"' do + find(:css, '#sort-projects-dropdown').click + click_link 'Show archived projects' end step 'I should see "archived" label' do @@ -45,7 +45,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps step 'I transfer project to group \'Web\'' do allow_any_instance_of(Projects::TransferService). to receive(:move_uploads_to_new_namespace).and_return(true) - find(:xpath, "//input[@id='new_namespace_id']").set group.id + click_button 'Search for Namespace' + click_link 'group: web' click_button 'Transfer' end diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb index 4cb8b8da150..8eaacef2024 100644 --- a/spec/controllers/admin/projects_controller_spec.rb +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -11,12 +11,12 @@ describe Admin::ProjectsController do render_views it 'retrieves the project for the given visibility level' do - get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC] + get :index, visibility_level: [Gitlab::VisibilityLevel::PUBLIC] expect(response.body).to match(project.name) end it 'does not retrieve the project' do - get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL] + get :index, visibility_level: [Gitlab::VisibilityLevel::INTERNAL] expect(response.body).not_to match(project.name) end end -- cgit v1.2.3 From 35224dbf5e88936b138af7e6215a2e4e0503ae97 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 6 Jul 2016 12:04:14 -0500 Subject: Do not show Any Namespace option when needed --- app/assets/javascripts/namespace_select.js.coffee | 6 ++++-- app/views/admin/projects/show.html.haml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee index 3a4b64a18f7..70ceb4d9c7c 100644 --- a/app/assets/javascripts/namespace_select.js.coffee +++ b/app/assets/javascripts/namespace_select.js.coffee @@ -5,12 +5,14 @@ class @NamespaceSelect } = opts showAny = true - fieldName = 'namespace_id' - if @dropdown.data 'fieldName' + if @dropdown.attr 'data-field-name' fieldName = @dropdown.data 'fieldName' + if @dropdown.attr 'data-show-any' + showAny = @dropdown.data 'showAny' + @dropdown.glDropdown( filterable: true selectable: true diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 7e1f5a52c6f..2c5aba71699 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -100,7 +100,7 @@ = f.label :new_namespace_id, "Namespace", class: 'control-label' .col-sm-10 .dropdown - = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' }) + = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' }) .dropdown-menu.dropdown-select = dropdown_title('Namespaces') = dropdown_filter("Search for Namespace") -- cgit v1.2.3 From 22191fcdd188b31bb1d3c2326d862019bd5b2457 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 6 Jul 2016 12:33:20 -0500 Subject: Rephrase deletion alert message --- app/views/admin/users/_user.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 463993d0c17..d3519f616f6 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -37,6 +37,6 @@ - if user.can_be_removed? %li.divider %li - = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, + = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, class: 'btn btn-remove btn-block', method: :delete -- cgit v1.2.3 From 140b0952a8b0ba1f9620a54f4ae2ea21abe02c18 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 7 Jul 2016 13:25:10 -0500 Subject: Namespaces should filter remotely --- app/assets/javascripts/namespace_select.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee index 70ceb4d9c7c..3b419dff105 100644 --- a/app/assets/javascripts/namespace_select.js.coffee +++ b/app/assets/javascripts/namespace_select.js.coffee @@ -16,6 +16,7 @@ class @NamespaceSelect @dropdown.glDropdown( filterable: true selectable: true + filterRemote: true search: fields: ['path'] fieldName: fieldName -- cgit v1.2.3 From ea25e0918b77c2345585a968fbf5b73bb544aac7 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 7 Jul 2016 12:41:48 +0100 Subject: Exclude projects pending delete from notifications If the Sidekiq job fails for some reason, a project can be 'stuck' pending deletion. The project can't be viewed, so it shouldn't be available through the notification settings association as this will throw an exception when we try to show the link. --- CHANGELOG | 1 + app/models/notification_setting.rb | 9 ++++++++- spec/factories/notification_settings.rb | 8 ++++++++ spec/models/notification_setting_spec.rb | 17 +++++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 spec/factories/notification_settings.rb diff --git a/CHANGELOG b/CHANGELOG index bc9bb7747a4..cc2a40ce409 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ v 8.10.0 (unreleased) - Fix MR-auto-close text added to description. !4836 - Fix issue, preventing users w/o push access to sort tags !5105 (redetection) - Add Spring EmojiOne updates. + - Fix viewing notification settings when a project is pending deletion - Fix pagination when sorting by columns with lots of ties (like priority) - Updated project header design - Exclude email check from the standard health check diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index d41fc7073c6..121b598b8f3 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -5,6 +5,7 @@ class NotificationSetting < ActiveRecord::Base belongs_to :user belongs_to :source, polymorphic: true + belongs_to :project, foreign_key: 'source_id' validates :user, presence: true validates :level, presence: true @@ -13,7 +14,13 @@ class NotificationSetting < ActiveRecord::Base allow_nil: true } scope :for_groups, -> { where(source_type: 'Namespace') } - scope :for_projects, -> { where(source_type: 'Project') } + + # Exclude projects not included by the Project model's default scope (those that are + # pending delete). + # + scope :for_projects, -> do + includes(:project).references(:projects).where(source_type: 'Project').where.not(projects: { id: nil }) + end EMAIL_EVENTS = [ :new_note, diff --git a/spec/factories/notification_settings.rb b/spec/factories/notification_settings.rb new file mode 100644 index 00000000000..b5e96d18b8f --- /dev/null +++ b/spec/factories/notification_settings.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :notification_setting do + source factory: :empty_project + user + level 3 + events [] + end +end diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index df336a6effe..d58673413c8 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -38,4 +38,21 @@ RSpec.describe NotificationSetting, type: :model do end end end + + describe '#for_projects' do + let(:user) { create(:user) } + + before do + 1.upto(4) do |i| + setting = create(:notification_setting, user: user) + + setting.project.update_attributes(pending_delete: true) if i.even? + end + end + + it 'excludes projects pending delete' do + expect(user.notification_settings.for_projects).to all(have_attributes(project: an_instance_of(Project))) + expect(user.notification_settings.for_projects.map(&:project)).to all(have_attributes(pending_delete: false)) + end + end end -- cgit v1.2.3 From 70a64f6a57a2b4fd2874b0db03f4fe1f0c3794f8 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jul 2016 15:57:38 -0400 Subject: Update test with new factory name --- spec/models/event_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 6ac19756f15..b5d0d79e14e 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -56,7 +56,7 @@ describe Event, models: true do end context 'merge request diff note event' do - let(:target) { create(:note_on_merge_request_diff) } + let(:target) { create(:legacy_diff_note_on_merge_request) } it { is_expected.to be_note } end @@ -132,7 +132,7 @@ describe Event, models: true do context 'merge request diff note event' do let(:project) { create(:project, :public) } let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) } - let(:note_on_merge_request) { create(:note_on_merge_request_diff, noteable: merge_request, project: project) } + let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) } let(:target) { note_on_merge_request } it { expect(event.visible_to_user?(non_member)).to eq true } -- cgit v1.2.3 From 6dd71888b310d2615d74ea129431fc6c9791b6b7 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jul 2016 16:13:55 -0400 Subject: Make `DiffNote#update_position` private --- app/models/diff_note.rb | 33 ++++++++--------- spec/models/diff_note_spec.rb | 83 +++++++++++++++++++++++-------------------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index cdd1c4b4aef..9671955db36 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -72,6 +72,20 @@ class DiffNote < Note self.position.diff_refs == diff_refs end + private + + def supported? + !self.for_merge_request? || self.noteable.support_new_diff_notes? + end + + def set_original_position + self.original_position = self.position.dup + end + + def set_line_code + self.line_code = self.position.line_code(self.project.repository) + end + def update_position return unless supported? return if for_commit? @@ -87,25 +101,6 @@ class DiffNote < Note ).execute(self) end - def update_position! - update_position && - Gitlab::Timeless.timeless(self, &:save) - end - - private - - def supported? - !self.for_merge_request? || self.noteable.support_new_diff_notes? - end - - def set_original_position - self.original_position = self.position.dup - end - - def set_line_code - self.line_code = self.position.line_code(self.project.repository) - end - def verify_supported return if supported? diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 9443f7c13f8..af8e890ca95 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -9,7 +9,7 @@ describe DiffNote, models: true do let(:path) { "files/ruby/popen.rb" } - let(:position) do + let!(:position) do Gitlab::Diff::Position.new( old_path: path, new_path: path, @@ -19,7 +19,7 @@ describe DiffNote, models: true do ) end - let(:new_position) do + let!(:new_position) do Gitlab::Diff::Position.new( old_path: path, new_path: path, @@ -129,56 +129,61 @@ describe DiffNote, models: true do end end - describe "#update_position" do - context "when noteable is a commit" do - subject { create(:diff_note_on_commit, project: project, position: position) } - - it "doesn't use the DiffPositionUpdateService" do - expect(Notes::DiffPositionUpdateService).not_to receive(:new) - - subject.update_position - end - - it "doesn't update the position" do - subject.update_position - - expect(subject.original_position).to eq(position) - expect(subject.position).to eq(position) - end - end + describe "creation" do + describe "updating of position" do + context "when noteable is a commit" do + let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) } - context "when noteable is a merge request" do - context "when the note is active" do it "doesn't use the DiffPositionUpdateService" do expect(Notes::DiffPositionUpdateService).not_to receive(:new) - subject.update_position + diff_note end it "doesn't update the position" do - subject.update_position + diff_note - expect(subject.original_position).to eq(position) - expect(subject.position).to eq(position) + expect(diff_note.original_position).to eq(position) + expect(diff_note.position).to eq(position) end end - context "when the note is outdated" do - before do - allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs) + context "when noteable is a merge request" do + let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + + context "when the note is active" do + it "doesn't use the DiffPositionUpdateService" do + expect(Notes::DiffPositionUpdateService).not_to receive(:new) + + diff_note + end + + it "doesn't update the position" do + diff_note + + expect(diff_note.original_position).to eq(position) + expect(diff_note.position).to eq(position) + end end - it "uses the DiffPositionUpdateService" do - expect(Notes::DiffPositionUpdateService).to receive(:new).with( - project, - nil, - old_diff_refs: position.diff_refs, - new_diff_refs: commit.diff_refs, - paths: [path] - ).and_call_original - expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(subject) - - subject.update_position + context "when the note is outdated" do + before do + allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs) + end + + it "uses the DiffPositionUpdateService" do + service = instance_double("Notes::DiffPositionUpdateService") + expect(Notes::DiffPositionUpdateService).to receive(:new).with( + project, + nil, + old_diff_refs: position.diff_refs, + new_diff_refs: commit.diff_refs, + paths: [path] + ).and_return(service) + expect(service).to receive(:execute) + + diff_note + end end end end -- cgit v1.2.3 From 14c2b9683af73ffed6c9b544ff24353cf0d07e1a Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jul 2016 16:14:18 -0400 Subject: Use HAML class syntax in diff line partials --- app/views/projects/diffs/_line.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 2e7fdc832ed..22cad00240a 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -27,4 +27,4 @@ = link_text - else = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text } - %td.line_content{ class: ['noteable_line', type, line_code], data: { line_code: line_code, position: position.to_json } }= diff_line_content(line.text, type) + %td.line_content.noteable_line{ class: [type, line_code], data: { line_code: line_code, position: position.to_json } }= diff_line_content(line.text, type) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 59603f6071d..51f207dce94 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -14,11 +14,11 @@ %td.new_line.diff-line-num %td.line_content.parallel.match= left[:text] - else - %td.old_line.diff-line-num{id: left[:line_code], class: "#{left[:type]} #{'empty-cell' if !left[:number]}"} + %td.old_line.diff-line-num{id: left[:line_code], class: [left[:type], ('empty-cell' unless left[:number])]} = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code] - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(left[:line_code], left[:position], 'old') - %td.line_content{class: "parallel noteable_line #{left[:type]} #{left[:line_code]} #{'empty-cell' if left[:text].empty?}", data: { line_code: left[:line_code], position: left[:position].to_json }}= diff_line_content(left[:text]) + %td.line_content.parallel.noteable_line{class: [left[:type], left[:line_code], ('empty-cell' if left[:text].empty?)], data: { line_code: left[:line_code], position: left[:position].to_json }}= diff_line_content(left[:text]) - if right[:type] == 'new' - new_line_class = 'new' @@ -29,11 +29,11 @@ - new_line_code = left[:line_code] - new_position = left[:position] - %td.new_line.diff-line-num{id: new_line_code, class: "#{new_line_class} #{'empty-cell' if !right[:number]}", data: { linenumber: right[:number] }} + %td.new_line.diff-line-num{id: new_line_code, class: [new_line_class, ('empty-cell' unless right[:number])], data: { linenumber: right[:number] }} = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code - if !@diff_notes_disabled && can?(current_user, :create_note, @project) = link_to_new_diff_note(new_line_code, new_position, 'new') - %td.line_content.parallel{class: "noteable_line #{new_line_class} #{new_line_code} #{'empty-cell' if right[:text].empty?}", data: { line_code: new_line_code, position: new_position.to_json }}= diff_line_content(right[:text]) + %td.line_content.parallel.noteable_line{class: [new_line_class, new_line_code, ('empty-cell' if right[:text].empty?)], data: { line_code: new_line_code, position: new_position.to_json }}= diff_line_content(right[:text]) - unless @diff_notes_disabled - notes_left, notes_right = organize_comments(left, right) -- cgit v1.2.3 From c66bcf34dd2ede698a784f55bd17346e85aeaf92 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 7 Jul 2016 16:14:57 -0400 Subject: Add comment with diff to DiffPositionUpdateService spec --- .../notes/diff_position_update_service_spec.rb | 104 +++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/notes/diff_position_update_service_spec.rb index 0bca3ea1a09..110efb54fa0 100644 --- a/spec/services/notes/diff_position_update_service_spec.rb +++ b/spec/services/notes/diff_position_update_service_spec.rb @@ -32,6 +32,110 @@ describe Notes::DiffPositionUpdateService, services: true do ) end + # old diff: + # 1 + require 'fileutils' + # 2 + require 'open3' + # 3 + + # 4 + module Popen + # 5 + extend self + # 6 + + # 7 + def popen(cmd, path=nil) + # 8 + unless cmd.is_a?(Array) + # 9 + raise "System commands must be given as an array of strings" + # 10 + end + # 11 + + # 12 + path ||= Dir.pwd + # 13 + vars = { "PWD" => path } + # 14 + options = { chdir: path } + # 15 + + # 16 + unless File.directory?(path) + # 17 + FileUtils.mkdir_p(path) + # 18 + end + # 19 + + # 20 + @cmd_output = "" + # 21 + @cmd_status = 0 + # 22 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + # 23 + @cmd_output << stdout.read + # 24 + @cmd_output << stderr.read + # 25 + @cmd_status = wait_thr.value.exitstatus + # 26 + end + # 27 + + # 28 + return @cmd_output, @cmd_status + # 29 + end + # 30 + end + # + # new diff: + # 1 + require 'fileutils' + # 2 + require 'open3' + # 3 + + # 4 + module Popen + # 5 + extend self + # 6 + + # 7 + def popen(cmd, path=nil) + # 8 + unless cmd.is_a?(Array) + # 9 + raise RuntimeError, "System commands must be given as an array of strings" + # 10 + end + # 11 + + # 12 + path ||= Dir.pwd + # 13 + + # 14 + vars = { + # 15 + "PWD" => path + # 16 + } + # 17 + + # 18 + options = { + # 19 + chdir: path + # 20 + } + # 21 + + # 22 + unless File.directory?(path) + # 23 + FileUtils.mkdir_p(path) + # 24 + end + # 25 + + # 26 + @cmd_output = "" + # 27 + @cmd_status = 0 + # 28 + + # 29 + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + # 30 + @cmd_output << stdout.read + # 31 + @cmd_output << stderr.read + # 32 + @cmd_status = wait_thr.value.exitstatus + # 33 + end + # 34 + + # 35 + return @cmd_output, @cmd_status + # 36 + end + # 37 + end + # + # old->new diff: + # .. .. @@ -6,12 +6,18 @@ module Popen + # 6 6 + # 7 7 def popen(cmd, path=nil) + # 8 8 unless cmd.is_a?(Array) + # 9 - raise "System commands must be given as an array of strings" + # 9 + raise RuntimeError, "System commands must be given as an array of strings" + # 10 10 end + # 11 11 + # 12 12 path ||= Dir.pwd + # 13 - vars = { "PWD" => path } + # 14 - options = { chdir: path } + # 13 + + # 14 + vars = { + # 15 + "PWD" => path + # 16 + } + # 17 + + # 18 + options = { + # 19 + chdir: path + # 20 + } + # 15 21 + # 16 22 unless File.directory?(path) + # 17 23 FileUtils.mkdir_p(path) + # 18 24 end + # 19 25 + # 20 26 @cmd_output = "" + # 21 27 @cmd_status = 0 + # 28 + + # 22 29 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + # 23 30 @cmd_output << stdout.read + # 24 31 @cmd_output << stderr.read + # .. .. + describe "#execute" do let(:note) { create(:diff_note_on_merge_request, project: project, position: old_position) } -- cgit v1.2.3 From 4498bb78332ba5f03b23a4f8ddba2eb034830b39 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 7 Jul 2016 16:48:10 -0500 Subject: Change 3600 to 1.hour --- app/helpers/time_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index d1086025ad5..8cb82c2d5cc 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -26,7 +26,7 @@ module TimeHelper def duration_in_numbers(finished_at, started_at) diff_in_seconds = finished_at.to_i - started_at.to_i - time_format = diff_in_seconds < 3600 ? "%M:%S" : "%H:%M:%S" + time_format = diff_in_seconds < 1.hour ? "%M:%S" : "%H:%M:%S" Time.at(diff_in_seconds).utc.strftime(time_format) end -- cgit v1.2.3 From bf2a86b73cce332ff8f4392ffc8df501193f32ec Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 7 Jul 2016 18:25:05 -0400 Subject: Revert "Merge branch 'issue_3946' into 'master' " This reverts commit 68155ee73b549a4f79744bb325542c29d45c71ea, reversing changes made to 7ebd011ed1de7aee706f07a53c63c90f1c8aa5d4. --- CHANGELOG | 1 - app/assets/javascripts/dispatcher.js.coffee | 2 +- app/assets/javascripts/namespace_select.js.coffee | 79 +++------- app/assets/stylesheets/framework/dropdowns.scss | 4 - app/assets/stylesheets/framework/nav.scss | 6 - app/assets/stylesheets/pages/admin.scss | 33 ---- app/assets/stylesheets/pages/groups.scss | 33 ---- app/assets/stylesheets/pages/projects.scss | 4 + app/assets/stylesheets/pages/search.scss | 2 +- app/controllers/admin/projects_controller.rb | 5 +- app/helpers/dropdowns_helper.rb | 2 +- app/views/admin/groups/_group.html.haml | 42 +++-- app/views/admin/groups/index.html.haml | 57 ++++--- app/views/admin/projects/index.html.haml | 171 +++++++++++---------- app/views/admin/projects/show.html.haml | 8 +- app/views/admin/users/_user.html.haml | 42 ----- app/views/admin/users/index.html.haml | 170 +++++++++++--------- app/views/shared/projects/_dropdown.html.haml | 17 +- features/admin/projects.feature | 4 +- features/steps/admin/projects.rb | 9 +- spec/controllers/admin/projects_controller_spec.rb | 4 +- 21 files changed, 296 insertions(+), 399 deletions(-) delete mode 100644 app/views/admin/users/_user.html.haml diff --git a/CHANGELOG b/CHANGELOG index 491692204a6..1318d834a12 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,7 +22,6 @@ v 8.10.0 (unreleased) - Fix pagination when sorting by columns with lots of ties (like priority) - Updated project header design - Exclude email check from the standard health check - - Updated layout for Projects, Groups, Users on Admin area !4424 - Fix changing issue state columns in milestone view - Add notification settings dropdown for groups - Allow importing from Github using Personal Access Tokens. (Eric K Idema) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index a39df421832..9493a575801 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -127,7 +127,7 @@ class Dispatcher when 'groups' new UsersSelect() when 'projects' - new NamespaceSelects() + new NamespaceSelect() when 'dashboard', 'root' shortcut_handler = new ShortcutsDashboardNavigation() when 'profiles' diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee index 3b419dff105..a02c4515ccc 100644 --- a/app/assets/javascripts/namespace_select.js.coffee +++ b/app/assets/javascripts/namespace_select.js.coffee @@ -1,56 +1,25 @@ class @NamespaceSelect - constructor: (opts) -> - { - @dropdown - } = opts - - showAny = true - fieldName = 'namespace_id' - - if @dropdown.attr 'data-field-name' - fieldName = @dropdown.data 'fieldName' - - if @dropdown.attr 'data-show-any' - showAny = @dropdown.data 'showAny' - - @dropdown.glDropdown( - filterable: true - selectable: true - filterRemote: true - search: - fields: ['path'] - fieldName: fieldName - toggleLabel: (selected) -> - return if not selected.id? then selected.text else "#{selected.kind}: #{selected.path}" - data: (term, dataCallback) -> - Api.namespaces term, (namespaces) -> - if showAny - anyNamespace = - text: 'Any namespace' - id: null - - namespaces.unshift(anyNamespace) - namespaces.splice 1, 0, 'divider' - - dataCallback(namespaces) - text: (namespace) -> - return if not namespace.id? then namespace.text else "#{namespace.kind}: #{namespace.path}" - renderRow: @renderRow - clicked: @onSelectItem - ) - - onSelectItem: (item, el, e) => - e.preventDefault() - -class @NamespaceSelects - constructor: (opts = {}) -> - { - @$dropdowns = $('.js-namespace-select') - } = opts - - @$dropdowns.each (i, dropdown) -> - $dropdown = $(dropdown) - - new NamespaceSelect( - dropdown: $dropdown - ) + constructor: -> + namespaceFormatResult = (namespace) -> + markup = "
" + markup += "" + namespace.kind + "" + markup += "" + namespace.path + "" + markup += "
" + markup + + formatSelection = (namespace) -> + namespace.kind + ": " + namespace.path + + $('.ajax-namespace-select').each (i, select) -> + $(select).select2 + placeholder: "Search for namespace" + multiple: $(select).hasClass('multiselect') + minimumInputLength: 0 + query: (query) -> + Api.namespaces query.term, (namespaces) -> + data = { results: namespaces } + query.callback(data) + + dropdownCssClass: "ajax-namespace-dropdown" + formatResult: namespaceFormatResult + formatSelection: formatSelection diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d4e900f80ef..f36736c475e 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -68,10 +68,6 @@ color: $dropdown-toggle-hover-icon-color; } } - - &.large { - width: 200px; - } } .dropdown-menu, diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 02ea98e9d94..6e5f216c894 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -134,11 +134,6 @@ margin-bottom: 0; border-bottom: none; - &.wide { - width: 100%; - display: block; - } - li a { padding: 16px 10px 11px; } @@ -169,7 +164,6 @@ > .btn { margin-right: $gl-padding-top; display: inline-block; - vertical-align: top; &:last-child { margin-right: 0; diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 1d34a7f79ae..e05f14e7496 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -71,36 +71,3 @@ @extend .broadcast-message; margin-bottom: 20px; } - - -// Users List - -.users-list { - .user-row { - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } - - .user-details { - flex: 1 1 auto; - } - - .user-name { - display: inline-block; - font-weight: bold; - } - - .controls { - > .btn, > .dropdown { - margin-left: 5px; - } - } - - .dropdown { - .btn-block { - margin-bottom: 0; - line-height: inherit; - } - } -} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 701b9388454..3d79f4400e2 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -38,39 +38,6 @@ margin-right: 15px; } } - - &.group-admin { - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - - .group-avatar, .group-details, .group-controls { - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } - - .group-details { - flex: 1 1 auto; - flex-direction: column; - min-width: 0; - } - - .group-controls { - align-items: center; - - a { - margin-left: 5px; - } - } - } - -} - -.ldap-group-links { - .form-actions { - margin-bottom: $gl-padding; - } } .groups-cover-block { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index bce4aac3334..3325b586496 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -475,6 +475,10 @@ pre.light-well { a:hover { text-decoration: none; } + + > span { + margin-left: 10px; + } } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 9e9b18fdbb8..ae524cd6bae 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -208,7 +208,7 @@ margin-top: 5px; @media (min-width: $screen-sm-min) { - width: 180px; + width: 160px; margin-top: 0; } } diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 0d2f4f6eb38..4c9c6362ffc 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -5,12 +5,11 @@ class Admin::ProjectsController < Admin::ApplicationController def index @projects = Project.all @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? - @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? + @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present? - @projects = @projects.non_archived unless params[:archived].present? - @projects = @projects.personal(current_user) if params[:personal].present? + @projects = @projects.non_archived unless params[:with_archived].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]) diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 4566f3782cc..7c140538012 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -39,7 +39,7 @@ module DropdownsHelper end end - def dropdown_toggle(toggle_text, data_attr, options = {}) + def dropdown_toggle(toggle_text, data_attr, options) content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") output << icon('chevron-down') diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 59fd6c3fea0..9025aaac097 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -1,20 +1,28 @@ - css_class = '' unless local_assigns[:css_class] +- css_class += ' no-description' if group.description.blank? -%li.group-row.group-admin{ class: css_class } - .group-avatar - = image_tag group_icon(group), class: 'avatar hidden-xs' - .group-details - .title - = link_to [:admin, group], class: 'group-name' do - = group.name - .group-stats - %span>= pluralize(number_with_delimiter(group.projects.count), 'project') - , - %span= pluralize(number_with_delimiter(group.users.count), 'member') +%li.group-row{ class: css_class } + .controls.hidden-xs + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm' + = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove' - - if group.description.present? - .description - = markdown(group.description, pipeline: :description) - .group-controls.hidden-xs - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn' - = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' + .stats + %span + = icon('bookmark') + = number_with_delimiter(group.projects.count) + + %span + = icon('users') + = number_with_delimiter(group.users.count) + + %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + = visibility_level_icon(group.visibility_level, fw: false) + + = image_tag group_icon(group), class: 'avatar s40 hidden-xs' + .title + = link_to [:admin, group], class: 'group-name' do + = group.name + + - if group.description.present? + .description + = markdown(group.description, pipeline: :description) diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 794f910a61f..94aa5f5a942 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -3,32 +3,41 @@ = render "admin/dashboard/head" %div{ class: container_class } + %h3.page-title + Groups (#{number_with_delimiter(@groups.total_count)}) + + %p.light + Group allows you to keep projects organized. + Use groups for uniting related projects. + .top-area - .prepend-top-default.append-bottom-default - = form_tag admin_groups_path, method: :get, class: 'js-search-form' do |f| + .nav-search + = form_tag admin_groups_path, method: :get, class: 'form-inline' do = hidden_field_tag :sort, @sort - .search-holder - - project_name = params[:name].present? ? params[:name] : nil - .search-field-holder - = search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name' - = icon("search", class: "search-icon") - .dropdown - - toggle_text = @sort.present? ? sort_options_hash[@sort] : sort_title_recently_created - = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) - %ul.dropdown-menu.dropdown-menu-align-right - %li.dropdown-header - Sort by - %li - = link_to admin_groups_path(sort: sort_value_recently_created, name: project_name) do - = sort_title_recently_created - = link_to admin_groups_path(sort: sort_value_oldest_created, name: project_name) do - = sort_title_oldest_created - = link_to admin_groups_path(sort: sort_value_recently_updated, name: project_name) do - = sort_title_recently_updated - = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do - = sort_title_oldest_updated - = link_to new_admin_group_path, class: "btn btn-new" do - New Group + = text_field_tag :name, params[:name], class: "form-control" + = button_tag "Search", class: "btn submit btn-primary" + + .nav-controls + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu + %li + = link_to admin_groups_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to admin_groups_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to admin_groups_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to admin_groups_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + = link_to 'New Group', new_admin_group_path, class: "btn btn-new" + %ul.content-list = render @groups diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 7fbce25b2c4..7d2eb423223 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,94 +1,97 @@ - @no_container = true - page_title "Projects" -- params[:visibility_level] ||= [] - += render 'shared/show_aside' = render "admin/dashboard/head" %div{ class: container_class } - .top-area - .prepend-top-default - = form_tag admin_namespaces_projects_path, method: :get do |f| - .search-holder - .search-field-holder - = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' - - - if params[:visibility_level].present? - = hidden_field_tag 'visibility_level', params[:visibility_level] - - - if params[:sort].present? - = hidden_field_tag 'sort', params[:sort] - - - if params[:personal].present? - = hidden_field_tag 'visibility_level', 'true' - - - if params[:archived].present? - = hidden_field_tag 'archived', 'true' - - = icon("search", class: "search-icon") - - .dropdown - - toggle_text = 'Search for Namespace' - - if params[:namespace_id].present? - - namespace = Namespace.find(params[:namespace_id]) - - toggle_text = "#{namespace.kind}: #{namespace.path}" - = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) - .dropdown-menu.dropdown-select.dropdown-menu-align-right - = dropdown_title('Namespaces') - = dropdown_filter("Search for Namespace") - = dropdown_content - = dropdown_loading - - = button_tag "Search", class: "btn btn-primary btn-search" - - %ul.nav-links - - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path } - = nav_link(opts) do - = link_to admin_namespaces_projects_path do - All + .row.prepend-top-default + %aside.col-md-3 + .panel.admin-filter + = 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" - = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do - Private - = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do - Internal - = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do - = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do - Public + .form-group + = label_tag :namespace_id, "Namespace" + = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large' - .nav-controls - = render 'shared/projects/dropdown' - = link_to new_project_path, class: 'btn btn-new' do - New Project + .form-group + %strong Activity + .checkbox + = label_tag :with_push do + = check_box_tag :with_push, 1, params[:with_push] + %span Projects with push events + .checkbox + = label_tag :abandoned do + = check_box_tag :abandoned, 1, params[:abandoned] + %span No activity over 6 month + .checkbox + = label_tag :with_archived do + = check_box_tag :with_archived, 1, params[:with_archived] + %span Show archived projects - .projects-list-holder - - if @projects.any? - %ul.projects-list.content-list - - @projects.each_with_index do |project| - %li.project-row - .controls.pull-right - - if project.archived - %span.label.label-warning archived - %span.label.label-gray - = repository_size(project) - = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" - = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" - .title - = link_to [:admin, project.namespace.becomes(Namespace), project] do - .dash-project-avatar - = project_icon(project, alt: '', class: 'avatar project-avatar s40') - %span.project-full-name - %span.namespace-name - - if project.namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name + %fieldset + %strong Visibility level: + .visibility-levels + - Project.visibility_levels.each do |label, level| + .checkbox + %label + = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s) + %span.descr + = visibility_level_icon(level) + = label + %fieldset + %strong Problems + .checkbox + = label_tag :last_repository_check_failed do + = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed] + %span Last repository check failed - - if project.description.present? - .description - = markdown(project.description, pipeline: :description) + = hidden_field_tag :sort, params[:sort] + = button_tag "Search", class: "btn submit btn-primary" + = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" - = paginate @projects, theme: 'gitlab' - - else - .nothing-here-block No projects found + %section.col-md-9 + .panel.panel-default + .panel-heading + Projects (#{@projects.total_count}) + .controls + .dropdown.inline + %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu + %li + = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do + = 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-sm btn-success" + %ul.well-list + - @projects.each do |project| + %li + .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.namespace.becomes(Namespace), project] + .pull-right + - if project.archived + %span.label.label-warning archived + %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-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 2c5aba71699..82d3169c6f9 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -99,13 +99,7 @@ .form-group = f.label :new_namespace_id, "Namespace", class: 'control-label' .col-sm-10 - .dropdown - = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' }) - .dropdown-menu.dropdown-select - = dropdown_title('Namespaces') - = dropdown_filter("Search for Namespace") - = dropdown_content - = dropdown_loading + = namespace_select_tag :new_namespace_id, selected: params[:namespace_id], class: 'input-large' .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml deleted file mode 100644 index d3519f616f6..00000000000 --- a/app/views/admin/users/_user.html.haml +++ /dev/null @@ -1,42 +0,0 @@ -%li.user-row - .user-avatar - = image_tag avatar_icon(user), class: "avatar", alt: '' - .user-details - .user-name - = link_to user.name, [:admin, user] - - if user.blocked? - %span.label.label-danger blocked - - if user.admin? - %span.label.label-success Admin - - if user.external? - %span.label.label-default External - - if user == current_user - %span It's you! - .user-email - = mail_to user.email, user.email - .controls.pull-right - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' - - unless user == current_user - .dropdown.inline - %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } } - = icon('cog') - = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - %li.dropdown-header - Settings - %li - - if user.ldap_blocked? - %span.small Cannot unblock LDAP blocked users - - elsif user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put - - else - = link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put - - if user.access_locked? - %li - = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } - - if user.can_be_removed? - %li.divider - %li - = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, - class: 'btn btn-remove btn-block', - method: :delete diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 357123c2c13..21bb99a792c 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,78 +1,110 @@ - @no_container = true - page_title "Users" += render 'shared/show_aside' = render "admin/dashboard/head" %div{ class: container_class } - .top-area - .prepend-top-default - = form_tag admin_users_path, method: :get do - - if params[:filter].present? - = hidden_field_tag "filter", h(params[:filter]) - .search-holder - .search-field-holder - = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false - = icon("search", class: "search-icon") - .dropdown - - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end - = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) - %ul.dropdown-menu.dropdown-menu-align-right - %li.dropdown-header - Sort by - %li - = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do - = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do - = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do - = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do - = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do - = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do - = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do - = sort_title_oldest_updated - = link_to 'New User', new_admin_user_path, class: 'btn btn-new btn-search' + .admin-filter + %ul.nav-links + %li{class: "#{'active' unless params[:filter]}"} + = link_to admin_users_path do + Active + %small.badge= number_with_delimiter(User.active.count) + %li{class: "#{'active' if params[:filter] == "admins"}"} + = link_to admin_users_path(filter: "admins") do + Admins + %small.badge= number_with_delimiter(User.admins.count) + %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} + = link_to admin_users_path(filter: 'two_factor_enabled') do + 2FA Enabled + %small.badge= number_with_delimiter(User.with_two_factor.count) + %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} + = link_to admin_users_path(filter: 'two_factor_disabled') do + 2FA Disabled + %small.badge= number_with_delimiter(User.without_two_factor.count) + %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"} + = link_to admin_users_path(filter: 'external') do + External + %small.badge= number_with_delimiter(User.external.count) + %li{class: "#{'active' if params[:filter] == "blocked"}"} + = link_to admin_users_path(filter: "blocked") do + Blocked + %small.badge= number_with_delimiter(User.blocked.count) + %li{class: "#{'active' if params[:filter] == "wop"}"} + = link_to admin_users_path(filter: "wop") do + Without projects + %small.badge= number_with_delimiter(User.without_projects.count) - .nav-block - %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs - .fade-left - = nav_link(html_options: { class: ('active' unless params[:filter]) }) do - = link_to admin_users_path do - Active - %small.badge= number_with_delimiter(User.active.count) - = nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do - = link_to admin_users_path(filter: "admins") do - Admins - %small.badge= number_with_delimiter(User.admins.count) - = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do - = link_to admin_users_path(filter: 'two_factor_enabled') do - 2FA Enabled - %small.badge= number_with_delimiter(User.with_two_factor.count) - = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do - = link_to admin_users_path(filter: 'two_factor_disabled') do - 2FA Disabled - %small.badge= number_with_delimiter(User.without_two_factor.count) - = nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do - = link_to admin_users_path(filter: 'external') do - External - %small.badge= number_with_delimiter(User.external.count) - = nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do - = link_to admin_users_path(filter: "blocked") do - Blocked - %small.badge= number_with_delimiter(User.blocked.count) - = nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do - = link_to admin_users_path(filter: "wop") do - Without projects - %small.badge= number_with_delimiter(User.without_projects.count) - .fade-right + .row-content-block.second-block + .pull-right + .dropdown.inline + %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} + %span.light + - if @sort.present? + = sort_options_hash[@sort] + - else + = sort_title_name + %b.caret + %ul.dropdown-menu + %li + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do + = sort_title_name + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do + = sort_title_recently_signin + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do + = sort_title_oldest_signin + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do + = sort_title_recently_created + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do + = sort_title_oldest_created + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do + = sort_title_recently_updated + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do + = sort_title_oldest_updated - %ul.users-list.content-list - - if @users.empty? - %li - .nothing-here-block No users found. - - else - = render partial: 'admin/users/user', collection: @users + = link_to 'New User', new_admin_user_path, class: "btn btn-new" + = form_tag admin_users_path, method: :get, class: 'form-inline' do + .form-group + = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false + = hidden_field_tag "filter", params[:filter] + = button_tag class: 'btn btn-primary' do + %i.fa.fa-search + + .panel.panel-default + %ul.well-list + - @users.each do |user| + %li + .list-item-name + - if user.blocked? + = icon("lock", class: "cred") + - else + = icon("user", class: "cgreen") + = link_to user.name, [:admin, user] + - if user.admin? + %strong.cred (Admin) + - if user.external? + %strong.cred (External) + - if user == current_user + %span.cred It's you! + .pull-right + %span.light + %i.fa.fa-envelope + = mail_to user.email, user.email, class: 'light' +   + .pull-right + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs' + - unless user == current_user + - if user.ldap_blocked? + = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do + %i.fa.fa-lock + Unblock + - elsif user.blocked? + = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success' + - else + = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning' + - if user.access_locked? + = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } + - if user.can_be_removed? + = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove' = paginate @users, theme: "gitlab" diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index b7f8551153b..1169bed0382 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,30 +1,31 @@ - @sort ||= sort_value_recently_updated - personal = params[:personal] - archived = params[:archived] -- namespace_id = params[:namespace_id] .dropdown.inline - - toggle_text = projects_sort_options_hash[@sort] - = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light + = projects_sort_options_hash[@sort] + %b.caret %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable %li.dropdown-header Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do + = link_to filter_projects_path(sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do = title %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do + = link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do Hide archived projects %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do + = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do Show archived projects - if current_user %li.divider %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil), class: ("is-active" unless personal.present?) do + = link_to filter_projects_path(sort: @sort, personal: nil), class: ("is-active" unless personal) do Owned by anyone %li - = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do + = link_to filter_projects_path(sort: @sort, personal: true), class: ("is-active" if personal) do Owned by me diff --git a/features/admin/projects.feature b/features/admin/projects.feature index 8929bcf8d80..c5ee80136c8 100644 --- a/features/admin/projects.feature +++ b/features/admin/projects.feature @@ -10,11 +10,10 @@ Feature: Admin Projects Then I should see all non-archived projects And I should not see project "Archive" - @javascript Scenario: I should see all projects in the list Given archived project "Archive" When I visit admin projects page - And I select "Show archived projects" + And I check "Show archived projects" Then I should see all projects And I should see "archived" label @@ -23,7 +22,6 @@ Feature: Admin Projects And I click on first project Then I should see project details - @javascript Scenario: Transfer project Given group 'Web' And I visit admin project page diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index d77945a6b9c..a7a28755a6c 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -18,9 +18,9 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps end end - step 'I select "Show archived projects"' do - find(:css, '#sort-projects-dropdown').click - click_link 'Show archived projects' + step 'I check "Show archived projects"' do + page.check 'Show archived projects' + click_button "Search" end step 'I should see "archived" label' do @@ -45,8 +45,7 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps step 'I transfer project to group \'Web\'' do allow_any_instance_of(Projects::TransferService). to receive(:move_uploads_to_new_namespace).and_return(true) - click_button 'Search for Namespace' - click_link 'group: web' + find(:xpath, "//input[@id='new_namespace_id']").set group.id click_button 'Transfer' end diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb index 8eaacef2024..4cb8b8da150 100644 --- a/spec/controllers/admin/projects_controller_spec.rb +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -11,12 +11,12 @@ describe Admin::ProjectsController do render_views it 'retrieves the project for the given visibility level' do - get :index, visibility_level: [Gitlab::VisibilityLevel::PUBLIC] + get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC] expect(response.body).to match(project.name) end it 'does not retrieve the project' do - get :index, visibility_level: [Gitlab::VisibilityLevel::INTERNAL] + get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL] expect(response.body).not_to match(project.name) end end -- cgit v1.2.3 From 0de617772dfeb9bdcf3770e9acf7421db5023058 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Thu, 7 Jul 2016 23:48:02 -0400 Subject: Revert "Revert "Merge branch 'issue_3946' into 'master' "" This reverts commit bf2a86b73cce332ff8f4392ffc8df501193f32ec. --- CHANGELOG | 1 + app/assets/javascripts/dispatcher.js.coffee | 2 +- app/assets/javascripts/namespace_select.js.coffee | 79 +++++++--- app/assets/stylesheets/framework/dropdowns.scss | 4 + app/assets/stylesheets/framework/nav.scss | 6 + app/assets/stylesheets/pages/admin.scss | 33 ++++ app/assets/stylesheets/pages/groups.scss | 33 ++++ app/assets/stylesheets/pages/projects.scss | 4 - app/assets/stylesheets/pages/search.scss | 2 +- app/controllers/admin/projects_controller.rb | 5 +- app/helpers/dropdowns_helper.rb | 2 +- app/views/admin/groups/_group.html.haml | 42 ++--- app/views/admin/groups/index.html.haml | 57 +++---- app/views/admin/projects/index.html.haml | 171 ++++++++++----------- app/views/admin/projects/show.html.haml | 8 +- app/views/admin/users/_user.html.haml | 42 +++++ app/views/admin/users/index.html.haml | 170 +++++++++----------- app/views/shared/projects/_dropdown.html.haml | 17 +- features/admin/projects.feature | 4 +- features/steps/admin/projects.rb | 9 +- spec/controllers/admin/projects_controller_spec.rb | 4 +- 21 files changed, 399 insertions(+), 296 deletions(-) create mode 100644 app/views/admin/users/_user.html.haml diff --git a/CHANGELOG b/CHANGELOG index 3d79afbc39e..7fbfa5e7377 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ v 8.10.0 (unreleased) - Fix pagination when sorting by columns with lots of ties (like priority) - Updated project header design - Exclude email check from the standard health check + - Updated layout for Projects, Groups, Users on Admin area !4424 - Fix changing issue state columns in milestone view - Add notification settings dropdown for groups - Wildcards for protected branches. !4665 diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 9493a575801..a39df421832 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -127,7 +127,7 @@ class Dispatcher when 'groups' new UsersSelect() when 'projects' - new NamespaceSelect() + new NamespaceSelects() when 'dashboard', 'root' shortcut_handler = new ShortcutsDashboardNavigation() when 'profiles' diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee index a02c4515ccc..3b419dff105 100644 --- a/app/assets/javascripts/namespace_select.js.coffee +++ b/app/assets/javascripts/namespace_select.js.coffee @@ -1,25 +1,56 @@ class @NamespaceSelect - constructor: -> - namespaceFormatResult = (namespace) -> - markup = "
" - markup += "" + namespace.kind + "" - markup += "" + namespace.path + "" - markup += "
" - markup - - formatSelection = (namespace) -> - namespace.kind + ": " + namespace.path - - $('.ajax-namespace-select').each (i, select) -> - $(select).select2 - placeholder: "Search for namespace" - multiple: $(select).hasClass('multiselect') - minimumInputLength: 0 - query: (query) -> - Api.namespaces query.term, (namespaces) -> - data = { results: namespaces } - query.callback(data) - - dropdownCssClass: "ajax-namespace-dropdown" - formatResult: namespaceFormatResult - formatSelection: formatSelection + constructor: (opts) -> + { + @dropdown + } = opts + + showAny = true + fieldName = 'namespace_id' + + if @dropdown.attr 'data-field-name' + fieldName = @dropdown.data 'fieldName' + + if @dropdown.attr 'data-show-any' + showAny = @dropdown.data 'showAny' + + @dropdown.glDropdown( + filterable: true + selectable: true + filterRemote: true + search: + fields: ['path'] + fieldName: fieldName + toggleLabel: (selected) -> + return if not selected.id? then selected.text else "#{selected.kind}: #{selected.path}" + data: (term, dataCallback) -> + Api.namespaces term, (namespaces) -> + if showAny + anyNamespace = + text: 'Any namespace' + id: null + + namespaces.unshift(anyNamespace) + namespaces.splice 1, 0, 'divider' + + dataCallback(namespaces) + text: (namespace) -> + return if not namespace.id? then namespace.text else "#{namespace.kind}: #{namespace.path}" + renderRow: @renderRow + clicked: @onSelectItem + ) + + onSelectItem: (item, el, e) => + e.preventDefault() + +class @NamespaceSelects + constructor: (opts = {}) -> + { + @$dropdowns = $('.js-namespace-select') + } = opts + + @$dropdowns.each (i, dropdown) -> + $dropdown = $(dropdown) + + new NamespaceSelect( + dropdown: $dropdown + ) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index f36736c475e..d4e900f80ef 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -68,6 +68,10 @@ color: $dropdown-toggle-hover-icon-color; } } + + &.large { + width: 200px; + } } .dropdown-menu, diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 6e5f216c894..02ea98e9d94 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -134,6 +134,11 @@ margin-bottom: 0; border-bottom: none; + &.wide { + width: 100%; + display: block; + } + li a { padding: 16px 10px 11px; } @@ -164,6 +169,7 @@ > .btn { margin-right: $gl-padding-top; display: inline-block; + vertical-align: top; &:last-child { margin-right: 0; diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index e05f14e7496..1d34a7f79ae 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -71,3 +71,36 @@ @extend .broadcast-message; margin-bottom: 20px; } + + +// Users List + +.users-list { + .user-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + + .user-details { + flex: 1 1 auto; + } + + .user-name { + display: inline-block; + font-weight: bold; + } + + .controls { + > .btn, > .dropdown { + margin-left: 5px; + } + } + + .dropdown { + .btn-block { + margin-bottom: 0; + line-height: inherit; + } + } +} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 3d79f4400e2..701b9388454 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -38,6 +38,39 @@ margin-right: 15px; } } + + &.group-admin { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + + .group-avatar, .group-details, .group-controls { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + + .group-details { + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + } + + .group-controls { + align-items: center; + + a { + margin-left: 5px; + } + } + } + +} + +.ldap-group-links { + .form-actions { + margin-bottom: $gl-padding; + } } .groups-cover-block { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 3325b586496..bce4aac3334 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -475,10 +475,6 @@ pre.light-well { a:hover { text-decoration: none; } - - > span { - margin-left: 10px; - } } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index ae524cd6bae..9e9b18fdbb8 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -208,7 +208,7 @@ margin-top: 5px; @media (min-width: $screen-sm-min) { - width: 160px; + width: 180px; margin-top: 0; } } diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 4c9c6362ffc..0d2f4f6eb38 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -5,11 +5,12 @@ class Admin::ProjectsController < Admin::ApplicationController def index @projects = Project.all @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? - @projects = @projects.where("projects.visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? + @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? @projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present? - @projects = @projects.non_archived unless params[:with_archived].present? + @projects = @projects.non_archived unless params[:archived].present? + @projects = @projects.personal(current_user) if params[:personal].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]) diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 7c140538012..4566f3782cc 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -39,7 +39,7 @@ module DropdownsHelper end end - def dropdown_toggle(toggle_text, data_attr, options) + def dropdown_toggle(toggle_text, data_attr, options = {}) content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do output = content_tag(:span, toggle_text, class: "dropdown-toggle-text") output << icon('chevron-down') diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 9025aaac097..59fd6c3fea0 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -1,28 +1,20 @@ - css_class = '' unless local_assigns[:css_class] -- css_class += ' no-description' if group.description.blank? -%li.group-row{ class: css_class } - .controls.hidden-xs - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm' - = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove' +%li.group-row.group-admin{ class: css_class } + .group-avatar + = image_tag group_icon(group), class: 'avatar hidden-xs' + .group-details + .title + = link_to [:admin, group], class: 'group-name' do + = group.name + .group-stats + %span>= pluralize(number_with_delimiter(group.projects.count), 'project') + , + %span= pluralize(number_with_delimiter(group.users.count), 'member') - .stats - %span - = icon('bookmark') - = number_with_delimiter(group.projects.count) - - %span - = icon('users') - = number_with_delimiter(group.users.count) - - %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} - = visibility_level_icon(group.visibility_level, fw: false) - - = image_tag group_icon(group), class: 'avatar s40 hidden-xs' - .title - = link_to [:admin, group], class: 'group-name' do - = group.name - - - if group.description.present? - .description - = markdown(group.description, pipeline: :description) + - if group.description.present? + .description + = markdown(group.description, pipeline: :description) + .group-controls.hidden-xs + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn' + = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 94aa5f5a942..794f910a61f 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -3,41 +3,32 @@ = render "admin/dashboard/head" %div{ class: container_class } - %h3.page-title - Groups (#{number_with_delimiter(@groups.total_count)}) - - %p.light - Group allows you to keep projects organized. - Use groups for uniting related projects. - .top-area - .nav-search - = form_tag admin_groups_path, method: :get, class: 'form-inline' do + .prepend-top-default.append-bottom-default + = form_tag admin_groups_path, method: :get, class: 'js-search-form' do |f| = hidden_field_tag :sort, @sort - = text_field_tag :name, params[:name], class: "form-control" - = button_tag "Search", class: "btn submit btn-primary" - - .nav-controls - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to admin_groups_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to admin_groups_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to admin_groups_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to admin_groups_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated - = link_to 'New Group', new_admin_group_path, class: "btn btn-new" - + .search-holder + - project_name = params[:name].present? ? params[:name] : nil + .search-field-holder + = search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name' + = icon("search", class: "search-icon") + .dropdown + - toggle_text = @sort.present? ? sort_options_hash[@sort] : sort_title_recently_created + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Sort by + %li + = link_to admin_groups_path(sort: sort_value_recently_created, name: project_name) do + = sort_title_recently_created + = link_to admin_groups_path(sort: sort_value_oldest_created, name: project_name) do + = sort_title_oldest_created + = link_to admin_groups_path(sort: sort_value_recently_updated, name: project_name) do + = sort_title_recently_updated + = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do + = sort_title_oldest_updated + = link_to new_admin_group_path, class: "btn btn-new" do + New Group %ul.content-list = render @groups diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 7d2eb423223..7fbce25b2c4 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -1,97 +1,94 @@ - @no_container = true - page_title "Projects" -= render 'shared/show_aside' +- params[:visibility_level] ||= [] + = render "admin/dashboard/head" %div{ class: container_class } - .row.prepend-top-default - %aside.col-md-3 - .panel.admin-filter - = 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" + .top-area + .prepend-top-default + = form_tag admin_namespaces_projects_path, method: :get do |f| + .search-holder + .search-field-holder + = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name' + + - if params[:visibility_level].present? + = hidden_field_tag 'visibility_level', params[:visibility_level] + + - if params[:sort].present? + = hidden_field_tag 'sort', params[:sort] + + - if params[:personal].present? + = hidden_field_tag 'visibility_level', 'true' + + - if params[:archived].present? + = hidden_field_tag 'archived', 'true' + + = icon("search", class: "search-icon") + + .dropdown + - toggle_text = 'Search for Namespace' + - if params[:namespace_id].present? + - namespace = Namespace.find(params[:namespace_id]) + - toggle_text = "#{namespace.kind}: #{namespace.path}" + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { toggle_class: 'js-namespace-select large' }) + .dropdown-menu.dropdown-select.dropdown-menu-align-right + = dropdown_title('Namespaces') + = dropdown_filter("Search for Namespace") + = dropdown_content + = dropdown_loading + + = button_tag "Search", class: "btn btn-primary btn-search" + + %ul.nav-links + - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path } + = nav_link(opts) do + = link_to admin_namespaces_projects_path do + All - .form-group - = label_tag :namespace_id, "Namespace" - = namespace_select_tag :namespace_id, selected: params[:namespace_id], class: 'input-large' + = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do + = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do + Private + = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do + = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do + Internal + = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do + = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do + Public - .form-group - %strong Activity - .checkbox - = label_tag :with_push do - = check_box_tag :with_push, 1, params[:with_push] - %span Projects with push events - .checkbox - = label_tag :abandoned do - = check_box_tag :abandoned, 1, params[:abandoned] - %span No activity over 6 month - .checkbox - = label_tag :with_archived do - = check_box_tag :with_archived, 1, params[:with_archived] - %span Show archived projects + .nav-controls + = render 'shared/projects/dropdown' + = link_to new_project_path, class: 'btn btn-new' do + New Project - %fieldset - %strong Visibility level: - .visibility-levels - - Project.visibility_levels.each do |label, level| - .checkbox - %label - = check_box_tag 'visibility_levels[]', level, params[:visibility_levels].present? && params[:visibility_levels].include?(level.to_s) - %span.descr - = visibility_level_icon(level) - = label - %fieldset - %strong Problems - .checkbox - = label_tag :last_repository_check_failed do - = check_box_tag :last_repository_check_failed, 1, params[:last_repository_check_failed] - %span Last repository check failed + .projects-list-holder + - if @projects.any? + %ul.projects-list.content-list + - @projects.each_with_index do |project| + %li.project-row + .controls.pull-right + - if project.archived + %span.label.label-warning archived + %span.label.label-gray + = repository_size(project) + = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" + = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" + .title + = link_to [:admin, project.namespace.becomes(Namespace), project] do + .dash-project-avatar + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + %span.project-full-name + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name - = hidden_field_tag :sort, params[:sort] - = button_tag "Search", class: "btn submit btn-primary" - = link_to "Reset", admin_namespaces_projects_path, class: "btn btn-cancel" + - if project.description.present? + .description + = markdown(project.description, pipeline: :description) - %section.col-md-9 - .panel.panel-default - .panel-heading - Projects (#{@projects.total_count}) - .controls - .dropdown.inline - %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to admin_namespaces_projects_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to admin_namespaces_projects_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to admin_namespaces_projects_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to admin_namespaces_projects_path(sort: sort_value_oldest_updated) do - = 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-sm btn-success" - %ul.well-list - - @projects.each do |project| - %li - .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.namespace.becomes(Namespace), project] - .pull-right - - if project.archived - %span.label.label-warning archived - %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-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" + = paginate @projects, theme: 'gitlab' + - else + .nothing-here-block No projects found diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 82d3169c6f9..2c5aba71699 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -99,7 +99,13 @@ .form-group = f.label :new_namespace_id, "Namespace", class: 'control-label' .col-sm-10 - = namespace_select_tag :new_namespace_id, selected: params[:namespace_id], class: 'input-large' + .dropdown + = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id', show_any: 'false' }, { toggle_class: 'js-namespace-select large' }) + .dropdown-menu.dropdown-select + = dropdown_title('Namespaces') + = dropdown_filter("Search for Namespace") + = dropdown_content + = dropdown_loading .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml new file mode 100644 index 00000000000..d3519f616f6 --- /dev/null +++ b/app/views/admin/users/_user.html.haml @@ -0,0 +1,42 @@ +%li.user-row + .user-avatar + = image_tag avatar_icon(user), class: "avatar", alt: '' + .user-details + .user-name + = link_to user.name, [:admin, user] + - if user.blocked? + %span.label.label-danger blocked + - if user.admin? + %span.label.label-success Admin + - if user.external? + %span.label.label-default External + - if user == current_user + %span It's you! + .user-email + = mail_to user.email, user.email + .controls.pull-right + = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn' + - unless user == current_user + .dropdown.inline + %a.dropdown-new.btn.btn-default#project-settings-button{href: '#', data: { toggle: 'dropdown' } } + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Settings + %li + - if user.ldap_blocked? + %span.small Cannot unblock LDAP blocked users + - elsif user.blocked? + = link_to 'Unblock', unblock_admin_user_path(user), method: :put + - else + = link_to 'Block', block_admin_user_path(user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put + - if user.access_locked? + %li + = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } + - if user.can_be_removed? + %li.divider + %li + = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, + class: 'btn btn-remove btn-block', + method: :delete diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 21bb99a792c..357123c2c13 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -1,110 +1,78 @@ - @no_container = true - page_title "Users" -= render 'shared/show_aside' = render "admin/dashboard/head" %div{ class: container_class } - .admin-filter - %ul.nav-links - %li{class: "#{'active' unless params[:filter]}"} - = link_to admin_users_path do - Active - %small.badge= number_with_delimiter(User.active.count) - %li{class: "#{'active' if params[:filter] == "admins"}"} - = link_to admin_users_path(filter: "admins") do - Admins - %small.badge= number_with_delimiter(User.admins.count) - %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"} - = link_to admin_users_path(filter: 'two_factor_enabled') do - 2FA Enabled - %small.badge= number_with_delimiter(User.with_two_factor.count) - %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"} - = link_to admin_users_path(filter: 'two_factor_disabled') do - 2FA Disabled - %small.badge= number_with_delimiter(User.without_two_factor.count) - %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"} - = link_to admin_users_path(filter: 'external') do - External - %small.badge= number_with_delimiter(User.external.count) - %li{class: "#{'active' if params[:filter] == "blocked"}"} - = link_to admin_users_path(filter: "blocked") do - Blocked - %small.badge= number_with_delimiter(User.blocked.count) - %li{class: "#{'active' if params[:filter] == "wop"}"} - = link_to admin_users_path(filter: "wop") do - Without projects - %small.badge= number_with_delimiter(User.without_projects.count) + .top-area + .prepend-top-default + = form_tag admin_users_path, method: :get do + - if params[:filter].present? + = hidden_field_tag "filter", h(params[:filter]) + .search-holder + .search-field-holder + = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false + = icon("search", class: "search-icon") + .dropdown + - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Sort by + %li + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do + = sort_title_name + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do + = sort_title_recently_signin + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do + = sort_title_oldest_signin + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do + = sort_title_recently_created + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do + = sort_title_oldest_created + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do + = sort_title_recently_updated + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do + = sort_title_oldest_updated + = link_to 'New User', new_admin_user_path, class: 'btn btn-new btn-search' - .row-content-block.second-block - .pull-right - .dropdown.inline - %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} - %span.light - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_name - %b.caret - %ul.dropdown-menu - %li - = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do - = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do - = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do - = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do - = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do - = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do - = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do - = sort_title_oldest_updated + .nav-block + %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs + .fade-left + = nav_link(html_options: { class: ('active' unless params[:filter]) }) do + = link_to admin_users_path do + Active + %small.badge= number_with_delimiter(User.active.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do + = link_to admin_users_path(filter: "admins") do + Admins + %small.badge= number_with_delimiter(User.admins.count) + = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do + = link_to admin_users_path(filter: 'two_factor_enabled') do + 2FA Enabled + %small.badge= number_with_delimiter(User.with_two_factor.count) + = nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do + = link_to admin_users_path(filter: 'two_factor_disabled') do + 2FA Disabled + %small.badge= number_with_delimiter(User.without_two_factor.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do + = link_to admin_users_path(filter: 'external') do + External + %small.badge= number_with_delimiter(User.external.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do + = link_to admin_users_path(filter: "blocked") do + Blocked + %small.badge= number_with_delimiter(User.blocked.count) + = nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do + = link_to admin_users_path(filter: "wop") do + Without projects + %small.badge= number_with_delimiter(User.without_projects.count) + .fade-right - = link_to 'New User', new_admin_user_path, class: "btn btn-new" - = form_tag admin_users_path, method: :get, class: 'form-inline' do - .form-group - = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false - = hidden_field_tag "filter", params[:filter] - = button_tag class: 'btn btn-primary' do - %i.fa.fa-search + %ul.users-list.content-list + - if @users.empty? + %li + .nothing-here-block No users found. + - else + = render partial: 'admin/users/user', collection: @users - - .panel.panel-default - %ul.well-list - - @users.each do |user| - %li - .list-item-name - - if user.blocked? - = icon("lock", class: "cred") - - else - = icon("user", class: "cgreen") - = link_to user.name, [:admin, user] - - if user.admin? - %strong.cred (Admin) - - if user.external? - %strong.cred (External) - - if user == current_user - %span.cred It's you! - .pull-right - %span.light - %i.fa.fa-envelope - = mail_to user.email, user.email, class: 'light' -   - .pull-right - = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs' - - unless user == current_user - - if user.ldap_blocked? - = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do - %i.fa.fa-lock - Unblock - - elsif user.blocked? - = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success' - - else - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning' - - if user.access_locked? - = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } - - if user.can_be_removed? - = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove' = paginate @users, theme: "gitlab" diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 1169bed0382..b7f8551153b 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,31 +1,30 @@ - @sort ||= sort_value_recently_updated - personal = params[:personal] - archived = params[:archived] +- namespace_id = params[:namespace_id] .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light - = projects_sort_options_hash[@sort] - %b.caret + - toggle_text = projects_sort_options_hash[@sort] + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable %li.dropdown-header Sort by - projects_sort_options_hash.each do |value, title| %li - = link_to filter_projects_path(sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: value, archived: archived, personal: personal), class: ("is-active" if @sort == value) do = title %li.divider %li - = link_to filter_projects_path(sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: nil), class: ("is-active" unless params[:archived].present?) do Hide archived projects %li - = link_to filter_projects_path(sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, archived: true), class: ("is-active" if params[:archived].present?) do Show archived projects - if current_user %li.divider %li - = link_to filter_projects_path(sort: @sort, personal: nil), class: ("is-active" unless personal) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: nil), class: ("is-active" unless personal.present?) do Owned by anyone %li - = link_to filter_projects_path(sort: @sort, personal: true), class: ("is-active" if personal) do + = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do Owned by me diff --git a/features/admin/projects.feature b/features/admin/projects.feature index c5ee80136c8..8929bcf8d80 100644 --- a/features/admin/projects.feature +++ b/features/admin/projects.feature @@ -10,10 +10,11 @@ Feature: Admin Projects Then I should see all non-archived projects And I should not see project "Archive" + @javascript Scenario: I should see all projects in the list Given archived project "Archive" When I visit admin projects page - And I check "Show archived projects" + And I select "Show archived projects" Then I should see all projects And I should see "archived" label @@ -22,6 +23,7 @@ Feature: Admin Projects And I click on first project Then I should see project details + @javascript Scenario: Transfer project Given group 'Web' And I visit admin project page diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index a7a28755a6c..d77945a6b9c 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -18,9 +18,9 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps end end - step 'I check "Show archived projects"' do - page.check 'Show archived projects' - click_button "Search" + step 'I select "Show archived projects"' do + find(:css, '#sort-projects-dropdown').click + click_link 'Show archived projects' end step 'I should see "archived" label' do @@ -45,7 +45,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps step 'I transfer project to group \'Web\'' do allow_any_instance_of(Projects::TransferService). to receive(:move_uploads_to_new_namespace).and_return(true) - find(:xpath, "//input[@id='new_namespace_id']").set group.id + click_button 'Search for Namespace' + click_link 'group: web' click_button 'Transfer' end diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb index 4cb8b8da150..8eaacef2024 100644 --- a/spec/controllers/admin/projects_controller_spec.rb +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -11,12 +11,12 @@ describe Admin::ProjectsController do render_views it 'retrieves the project for the given visibility level' do - get :index, visibility_levels: [Gitlab::VisibilityLevel::PUBLIC] + get :index, visibility_level: [Gitlab::VisibilityLevel::PUBLIC] expect(response.body).to match(project.name) end it 'does not retrieve the project' do - get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL] + get :index, visibility_level: [Gitlab::VisibilityLevel::INTERNAL] expect(response.body).not_to match(project.name) end end -- cgit v1.2.3