diff options
38 files changed, 495 insertions, 132 deletions
diff --git a/.flayignore b/.flayignore index fc64b0b5892..47597025115 100644 --- a/.flayignore +++ b/.flayignore @@ -2,3 +2,4 @@ lib/gitlab/sanitizers/svg/whitelist.rb lib/gitlab/diff/position_tracer.rb app/policies/project_policy.rb +app/models/concerns/relative_positioning.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index db3d25195dc..492f5ef715d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -67,9 +67,11 @@ stages: - knapsack rspec "--color --format documentation" artifacts: expire_in: 31d + when: always paths: - - knapsack/ - coverage/ + - knapsack/ + - tmp/capybara/ .spinach-knapsack: &spinach-knapsack stage: test @@ -85,9 +87,11 @@ stages: - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' artifacts: expire_in: 31d + when: always paths: - - knapsack/ - coverage/ + - knapsack/ + - tmp/capybara/ # Prepare and merge knapsack tests diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index a0946eb392a..e5f36c84987 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -51,15 +51,15 @@ var Api = { }); }, // Return projects list. Filtered by query - projects: function(query, order, callback) { + projects: function(query, options, callback) { var url = Api.buildUrl(Api.projectsPath); return $.ajax({ url: url, - data: { + data: $.extend({ search: query, - order_by: order, - per_page: 20 - }, + per_page: 20, + membership: true + }, options), dataType: "json" }).done(function(projects) { return callback(projects); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index f80e765ce30..3c1c1e7dceb 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -35,7 +35,7 @@ if (this.groupId) { return Api.groupProjects(this.groupId, term, projectsCallback); } else { - return Api.projects(term, orderBy, projectsCallback); + return Api.projects(term, { order_by: orderBy }, projectsCallback); } }, url: function(project) { @@ -84,7 +84,7 @@ if (_this.groupId) { return Api.groupProjects(_this.groupId, query.term, projectsCallback); } else { - return Api.projects(query.term, _this.orderBy, projectsCallback); + return Api.projects(query.term, { order_by: _this.orderBy }, projectsCallback); } }; })(this), diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index e66418beeab..15f5963353a 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -47,7 +47,7 @@ fields: ['name'] }, data: function(term, callback) { - return Api.projects(term, 'id', function(data) { + return Api.projects(term, { order_by: 'id' }, function(data) { data.unshift({ name_with_namespace: 'Any' }); diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 7f506db583f..df528d10f6e 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -5,6 +5,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController def index respond_to do |format| format.html do + @milestone_states = GlobalMilestone.states_count(@projects) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 0d872c86c8a..43102596201 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -6,6 +6,7 @@ class Groups::MilestonesController < Groups::ApplicationController def index respond_to do |format| format.html do + @milestone_states = GlobalMilestone.states_count(@projects) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 2d8064c9878..8b6c83d4fed 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,5 +1,3 @@ -require 'project_wiki' - class Projects::WikisController < Projects::ApplicationController before_action :authorize_read_wiki! before_action :authorize_create_wiki!, only: [:edit, :create, :history] diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index 603f2dd7e5d..f1d8532a6d6 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -2,16 +2,14 @@ module RelativePositioning extend ActiveSupport::Concern MIN_POSITION = 0 + START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2 MAX_POSITION = Gitlab::Database::MAX_INT_VALUE + IDEAL_DISTANCE = 500 included do after_save :save_positionable_neighbours end - def min_relative_position - self.class.in_projects(project.id).minimum(:relative_position) - end - def max_relative_position self.class.in_projects(project.id).maximum(:relative_position) end @@ -26,7 +24,7 @@ module RelativePositioning maximum(:relative_position) end - prev_pos || MIN_POSITION + prev_pos end def next_relative_position @@ -39,55 +37,95 @@ module RelativePositioning minimum(:relative_position) end - next_pos || MAX_POSITION + next_pos end def move_between(before, after) return move_after(before) unless after return move_before(after) unless before + # If there is no place to insert an issue we need to create one by moving the before issue closer + # to its predecessor. This process will recursively move all the predecessors until we have a place + if (after.relative_position - before.relative_position) < 2 + before.move_before + @positionable_neighbours = [before] + end + + self.relative_position = position_between(before.relative_position, after.relative_position) + end + + def move_after(before = self) pos_before = before.relative_position + pos_after = before.next_relative_position + + if before.shift_after? + issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after) + issue_to_move.move_after + @positionable_neighbours = [issue_to_move] + + pos_after = issue_to_move.relative_position + end + + self.relative_position = position_between(pos_before, pos_after) + end + + def move_before(after = self) pos_after = after.relative_position + pos_before = after.prev_relative_position - if pos_after && (pos_before == pos_after) - self.relative_position = pos_before - before.move_before(self) - after.move_after(self) + if after.shift_before? + issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before) + issue_to_move.move_before + @positionable_neighbours = [issue_to_move] - @positionable_neighbours = [before, after] - else - self.relative_position = position_between(pos_before, pos_after) + pos_before = issue_to_move.relative_position end + + self.relative_position = position_between(pos_before, pos_after) end - def move_before(after) - self.relative_position = position_between(after.prev_relative_position, after.relative_position) + def move_to_end + self.relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION) end - def move_after(before) - self.relative_position = position_between(before.relative_position, before.next_relative_position) + # Indicates if there is an issue that should be shifted to free the place + def shift_after? + next_pos = next_relative_position + next_pos && (next_pos - relative_position) == 1 end - def move_to_end - self.relative_position = position_between(max_relative_position, MAX_POSITION) + # Indicates if there is an issue that should be shifted to free the place + def shift_before? + prev_pos = prev_relative_position + prev_pos && (relative_position - prev_pos) == 1 end private # This method takes two integer values (positions) and - # calculates some random position between them. The range is huge as - # the maximum integer value is 2147483647. Ideally, the calculated value would be - # exactly between those terminating values, but this will introduce possibility of a race condition - # so two or more issues can get the same value, we want to avoid that and we also want to avoid - # using a lock here. If we have two issues with distance more than one thousand, we are OK. - # Given the huge range of possible values that integer can fit we shoud never face a problem. + # calculates the position between them. The range is huge as + # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time + # when we have enough space. If distance is less then IDEAL_DISTANCE we are calculating an average number def position_between(pos_before, pos_after) pos_before ||= MIN_POSITION pos_after ||= MAX_POSITION pos_before, pos_after = [pos_before, pos_after].sort - rand(pos_before.next..pos_after.pred) + halfway = (pos_after + pos_before) / 2 + distance_to_halfway = pos_after - halfway + + if distance_to_halfway < IDEAL_DISTANCE + halfway + else + if pos_before == MIN_POSITION + pos_after - IDEAL_DISTANCE + elsif pos_after == MAX_POSITION + pos_before + IDEAL_DISTANCE + else + halfway + end + end end def save_positionable_neighbours diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index b991d78e27f..0afbca2cb32 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -28,6 +28,28 @@ class GlobalMilestone new(title, child_milestones) end + def self.states_count(projects) + relation = MilestonesFinder.new.execute(projects, state: 'all') + milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count + + opened = count_by_state(milestones_by_state_and_title, 'active') + closed = count_by_state(milestones_by_state_and_title, 'closed') + all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count + + { + opened: opened, + closed: closed, + all: all + } + end + + def self.count_by_state(milestones_by_state_and_title, state) + milestones_by_state_and_title.count do |(milestone_state, _), _| + milestone_state == state + end + end + private_class_method :count_by_state + def initialize(title, milestones) @title = title @name = title diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 917bfbd47e9..505b475f55b 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -1,11 +1,11 @@ -- page_title "Milestones" -- header_title "Milestones", dashboard_milestones_path +- page_title 'Milestones' +- header_title 'Milestones', dashboard_milestones_path .top-area - = render 'shared/milestones_filter' + = render 'shared/milestones_filter', counts: @milestone_states .nav-controls - = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true + = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New Milestone', include_groups: true .milestones %ul.content-list @@ -15,4 +15,4 @@ - else - @milestones.each do |milestone| = render 'milestone', milestone: milestone - = paginate @milestones, theme: "gitlab" + = paginate @milestones, theme: 'gitlab' diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 644895c56a1..6893168f039 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -2,7 +2,7 @@ = render "groups/head_issues" .top-area - = render 'shared/milestones_filter' + = render 'shared/milestones_filter', counts: @milestone_states .nav-controls - if can?(current_user, :admin_milestones, @group) diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 87f9b503989..1fb2c6271ad 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -410,7 +410,7 @@ :javascript $('#js-project-dropdown').glDropdown({ data: function (term, callback) { - Api.projects(term, "last_activity_at", function (data) { + Api.projects(term, { order_by: 'last_activity_at' }, function (data) { callback(data); }); }, diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 62135d3ae32..c09c7b87e24 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -9,7 +9,7 @@ - case type - when 'match' = diff_match_line line.old_pos, line.new_pos, text: line.text - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.old_line.diff-line-num %td.new_line.diff-line-num %td.line_content.match= line.text diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index e7758c8bdfa..b7346f27ddb 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -12,7 +12,7 @@ - case left.type - when 'match' = diff_match_line left.old_pos, nil, text: left.text, view: :parallel - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.old_line.diff-line-num %td.line_content.match= left.text - else @@ -31,7 +31,7 @@ - case right.type - when 'match' = diff_match_line nil, right.new_pos, text: left.text, view: :parallel - - when 'nonewline' + - when 'old-nonewline', 'new-nonewline' %td.new_line.diff-line-num %td.line_content.match= right.text - else diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index ad2bfbec915..918f5d161bb 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,14 +1,14 @@ - @no_container = true -- page_title "Milestones" -= render "projects/issues/head" +- page_title 'Milestones' += render 'projects/issues/head' %div{ class: container_class } .top-area - = render 'shared/milestones_filter' + = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) .nav-controls - if can?(current_user, :admin_milestone, @project) - = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "btn btn-new", title: "New Milestone" do + = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do New Milestone .milestones @@ -19,4 +19,4 @@ %li .nothing-here-block No milestones to show - = paginate @milestones, theme: "gitlab" + = paginate @milestones, theme: 'gitlab' diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 704893b4d5b..57a0eaa919e 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,19 +1,13 @@ -- if @project - - counts = milestone_counts(@project.milestones) - %ul.nav-links %li{ class: milestone_class_for_state(params[:state], 'opened', true) }> = link_to milestones_filter_path(state: 'opened') do Open - - if @project - %span.badge= counts[:opened] + %span.badge= counts[:opened] %li{ class: milestone_class_for_state(params[:state], 'closed') }> = link_to milestones_filter_path(state: 'closed') do Closed - - if @project - %span.badge= counts[:closed] + %span.badge= counts[:closed] %li{ class: milestone_class_for_state(params[:state], 'all') }> = link_to milestones_filter_path(state: 'all') do All - - if @project - %span.badge= counts[:all] + %span.badge= counts[:all] diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 4a27965754d..7e9fb7bb4d3 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -7,7 +7,7 @@ - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3'] -- cache_key.push(project.commit.status) if project.commit.try(:status) +- cache_key.push(project.commit&.sha, project.commit&.status) %li.project-row{ class: css_class } = cache(cache_key) do diff --git a/changelogs/unreleased/24421-personal-milestone-count-badges.yml b/changelogs/unreleased/24421-personal-milestone-count-badges.yml new file mode 100644 index 00000000000..8bbc1ed2dde --- /dev/null +++ b/changelogs/unreleased/24421-personal-milestone-count-badges.yml @@ -0,0 +1,4 @@ +--- +title: Add dashboard and group milestones count badges +merge_request: 9836 +author: Alex Braha Stoll diff --git a/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml new file mode 100644 index 00000000000..4db684c40b2 --- /dev/null +++ b/changelogs/unreleased/fix-gb-dashboard-commit-status-caching.yml @@ -0,0 +1,4 @@ +--- +title: Resolve project pipeline status caching problem on dashboard +merge_request: 9895 +author: diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index 3e1657b8382..5e0eefdb154 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -124,9 +124,9 @@ if Gitlab::Metrics.enabled? # These are manually require'd so the classes are registered properly with # ActiveSupport. - require 'gitlab/metrics/subscribers/action_view' - require 'gitlab/metrics/subscribers/active_record' - require 'gitlab/metrics/subscribers/rails_cache' + require_dependency 'gitlab/metrics/subscribers/action_view' + require_dependency 'gitlab/metrics/subscribers/active_record' + require_dependency 'gitlab/metrics/subscribers/rails_cache' Gitlab::Application.configure do |config| config.middleware.use(Gitlab::Metrics::RackMiddleware) diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index ab5a0561b8c..f7fa6d1c2de 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -20,15 +20,12 @@ OmniAuth.config.before_request_phase do |env| end if Gitlab.config.omniauth.enabled - Gitlab.config.omniauth.providers.each do |provider| - if provider['name'] == 'kerberos' - require 'omniauth-kerberos' - end - end + provider_names = Gitlab.config.omniauth.providers.map(&:name) + require 'omniauth-kerberos' if provider_names.include?('kerberos') end module OmniAuth module Strategies - autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket') + autoload :Bitbucket, Rails.root.join('lib', 'omni_auth', 'strategies', 'bitbucket') end end diff --git a/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb new file mode 100644 index 00000000000..b61dd7cfc61 --- /dev/null +++ b/db/post_migrate/20170309171644_reset_relative_position_for_issue.rb @@ -0,0 +1,17 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ResetRelativePositionForIssue < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + update_column_in_batches(:issues, :relative_position, nil) do |table, query| + query.where(table[:relative_position].not_eq(nil)) + end + end + + def down + end +end diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 00787323b6b..f025a7e3496 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -170,13 +170,17 @@ services: ``` When the job is run, `tutum/wordpress` will be started and you will have -access to it from your build container under the hostname `tutum__wordpress`. +access to it from your build container under the hostnames `tutum-wordpress` +(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`. -The alias hostname for the service is made from the image name following these +*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.* + +The alias hostnames for the service are made from the image name following these rules: 1. Everything after `:` is stripped -2. Slash (`/`) is replaced with double underscores (`__`) +2. Slash (`/`) is replaced with double underscores (`__`) - primary alias +3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer ## Configuring services diff --git a/doc/development/changelog.md b/doc/development/changelog.md index c71858c6a24..ff9a4fc4fec 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -1,7 +1,7 @@ -# Generate a changelog entry +# Changelog entries -This guide contains instructions for generating a changelog entry data file, as -well as information and history about our changelog process. +This guide contains instructions for when and how to generate a changelog entry +file, as well as information and history about our changelog process. ## Overview @@ -19,19 +19,51 @@ author: Ozzy Osbourne The `merge_request` value is a reference to a merge request that adds this entry, and the `author` key is used to give attribution to community -contributors. Both are optional. +contributors. **Both are optional**. Community contributors and core team members are encouraged to add their name to -the `author` field. GitLab team members should not. - -If you're working on the GitLab EE repository, the entry will be added to -`changelogs/unreleased-ee/` instead. +the `author` field. GitLab team members **should not**. [changelog.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md [unreleased]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/changelogs/ [YAML]: https://en.wikipedia.org/wiki/YAML -## Instructions +## What warrants a changelog entry? + +- Any user-facing change **should** have a changelog entry. Example: "GitLab now + uses system fonts for all text." +- A fix for a regression introduced and then fixed in the same release (i.e., + fixing a bug introduced during a monthly release candidate) **should not** + have a changelog entry. +- Any developer-facing change (e.g., refactoring, technical debt remediation, + test suite changes) **should not** have a changelog entry. Example: "Reduce + database records created during Cycle Analytics model spec." +- _Any_ contribution from a community member, no matter how small, **may** have + a changelog entry regardless of these guidelines if the contributor wants one. + Example: "Fixed a typo on the search results page. (Jane Smith)" + +## Writing good changelog entries + +A good changelog entry should be descriptive and concise. It should explain the +change to a reader who has _zero context_ about the change. If you have trouble +making it both concise and descriptive, err on the side of descriptive. + +- **Bad:** Go to a project order. +- **Good:** Show a user's starred projects at the top of the "Go to project" + dropdown. +- **Bad:** Copy [some text] to clipboard. +- **Good:** Update the "Copy to clipboard" tooltip to indicate what's being + copied. +- **Bad:** Fixes and Improves CSS and HTML problems in mini pipeline graph and + builds dropdown. +- **Good:** Fix tooltips and hover states in mini pipeline graph and builds + dropdown. + +Use your best judgement and try to put yourself in the mindset of someone +reading the compiled changelog. Does this entry add value? Does it offer context +about _where_ and _why_ the change was made? + +## How to generate a changelog entry A `bin/changelog` script is available to generate the changelog entry file automatically. @@ -55,19 +87,28 @@ title: Hey DZ, I added a feature to GitLab! merge_request: author: ``` +If you're working on the GitLab EE repository, the entry will be added to +`changelogs/unreleased-ee/` instead. + +#### Arguments -### Arguments +| Argument | Shorthand | Purpose | +| ----------------- | --------- | --------------------------------------------- | +| [`--amend`] | | Amend the previous commit | +| [`--force`] | `-f` | Overwrite an existing entry | +| [`--merge-request`] | `-m` | Set merge request ID | +| [`--dry-run`] | `-n` | Don't actually write anything, just print | +| [`--git-username`] | `-u` | Use Git user.name configuration as the author | +| [`--help`] | `-h` | Print help message | -| Argument | Shorthand | Purpose | -| ----------------- | --------- | --------------------------------------------- | -| `--amend` | | Amend the previous commit | -| `--force` | `-f` | Overwrite an existing entry | -| `--merge-request` | `-m` | Merge Request ID | -| `--dry-run` | `-n` | Don't actually write anything, just print | -| `--git-username` | `-u` | Use Git user.name configuration as the author | -| `--help` | `-h` | Print help message | +[`--amend`]: #-amend +[`--force`]: #-force-or-f +[`--merge-request`]: #-merge-request-or-m +[`--dry-run`]: #-dry-run-or-n +[`--git-username`]: #-git-username-or-u +[`--help`]: #-help -#### `--amend` +##### `--amend` You can pass the **`--amend`** argument to automatically stage the generated file and amend it to the previous commit. @@ -88,7 +129,7 @@ merge_request: author: ``` -#### `--force` or `-f` +##### `--force` or `-f` Use **`--force`** or **`-f`** to overwrite an existing changelog entry if it already exists. @@ -105,7 +146,7 @@ merge_request: 1983 author: ``` -#### `--merge-request` or `-m` +##### `--merge-request` or `-m` Use the **`--merge-request`** or **`-m`** argument to provide the `merge_request` value: @@ -119,7 +160,7 @@ merge_request: 1983 author: ``` -#### `--dry-run` or `-n` +##### `--dry-run` or `-n` Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or committing anything: @@ -135,7 +176,7 @@ author: $ ls changelogs/unreleased/ ``` -#### `--git-username` or `-u` +##### `--git-username` or `-u` Use the **`--git-username`** or **`-u`** argument to automatically fill in the `author` value with your configured Git `user.name` value: @@ -152,7 +193,7 @@ merge_request: author: Jane Doe ``` -## History and Reasoning +### History and Reasoning Our `CHANGELOG` file was previously updated manually by each contributor that felt their change warranted an entry. When two merge requests added their own diff --git a/doc/development/frontend.md b/doc/development/frontend.md index d646de7c54a..e7add17fe2d 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -66,6 +66,8 @@ Let's look into each of them: This is the index file of your new feature. This is where the root Vue instance of the new feature should be. +The Store and the Service should be imported and initialized in this file and provided as a prop to the main component. + Don't forget to follow [these steps.][page_specific_javascript] **A folder for Components** @@ -86,7 +88,7 @@ You can read more about components in Vue.js site, [Component System][component- **A folder for the Store** -The Store is a simple object that allows us to manage the state in a single +The Store is a class that allows us to manage the state in a single source of truth. The concept we are trying to follow is better explained by Vue documentation @@ -289,7 +291,7 @@ When exactly one object is needed for a given task, prefer to define it as a `class` rather than as an object literal. Prefer also to explicitly restrict instantiation, unless flexibility is important (e.g. for testing). -``` +```javascript // bad gl.MyThing = { @@ -332,6 +334,33 @@ gl.MyThing = MyThing; ``` +### Manipulating the DOM in a JS Class + +When writing a class that needs to manipulate the DOM guarantee a container option is provided. +This is useful when we need that class to be instantiated more than once in the same page. + +Bad: +```javascript +class Foo { + constructor() { + document.querySelector('.bar'); + } +} +new Foo(); +``` + +Good: +```javascript +class Foo { + constructor(opts) { + document.querySelector(`${opts.container} .bar`); + } +} + +new Foo({ container: '.my-element' }); +``` +You can find an example of the above in this [class][container-class-example]; + ## Supported browsers For our currently-supported browsers, see our [requirements][requirements]. @@ -457,3 +486,4 @@ Scenario: Developer can approve merge request [eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc [team-page]: https://about.gitlab.com/team [vue-section]: https://docs.gitlab.com/ce/development/frontend.html#how-to-build-a-new-feature-with-vue-js +[container-class-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js diff --git a/doc/integration/github.md b/doc/integration/github.md index cea85f073cc..4b0d33334bd 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -19,7 +19,7 @@ GitHub will generate an application ID and secret key for you to use. - Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}' + - Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'. Please make sure the port is included if your Gitlab instance is not configured on default port. 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). diff --git a/features/support/capybara.rb b/features/support/capybara.rb index a5fcbb65131..c0c489d2775 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -1,5 +1,6 @@ require 'spinach/capybara' require 'capybara/poltergeist' +require 'capybara-screenshot/spinach' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10 @@ -20,12 +21,8 @@ end Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = false -unless ENV['CI'] || ENV['CI_SERVER'] - require 'capybara-screenshot/spinach' - - # Keep only the screenshots generated from the last failing test suite - Capybara::Screenshot.prune_strategy = :keep_last_run -end +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run Spinach.hooks.before_run do TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER'] diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 80a146b4a5a..114656958e3 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -38,11 +38,11 @@ module Gitlab end def added? - type == 'new' + type == 'new' || type == 'new-nonewline' end def removed? - type == 'old' + type == 'old' || type == 'old-nonewline' end def rich_text @@ -52,7 +52,7 @@ module Gitlab end def meta? - type == 'match' || type == 'nonewline' + type == 'match' end def as_json(opts = nil) diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index 8f844224a7a..742f989c50b 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -11,6 +11,7 @@ module Gitlab line_old = 1 line_new = 1 type = nil + context = nil # By returning an Enumerator we make it possible to search for a single line (with #find) # without having to instantiate all the others that come after it. @@ -31,7 +32,8 @@ module Gitlab line_obj_index += 1 next elsif line[0] == '\\' - type = 'nonewline' + type = "#{context}-nonewline" + yielder << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new) line_obj_index += 1 else @@ -43,8 +45,10 @@ module Gitlab case line[0] when "+" line_new += 1 + context = :new when "-" line_old += 1 + context = :old when "\\" # rubocop:disable Lint/EmptyWhen # No increment else diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 6c275a8d5de..5ab84266b7d 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -1,7 +1,7 @@ module Gitlab module GonHelper def add_gon_variables - gon.api_version = 'v3' # v4 Is not officially released yet, therefore can't be considered as "frozen" + gon.api_version = 'v4' gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s gon.max_file_size = current_application_settings.max_attachment_size gon.asset_host = ActionController::Base.asset_host diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index b84c81f1a6c..2d5e47a6f3b 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -1,5 +1,3 @@ -require 'gitlab/o_auth/user' - # LDAP extension for User model # # * Find or create user from omniauth.auth data diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omni_auth/strategies/bitbucket.rb index 5a7d67c2390..5a7d67c2390 100644 --- a/lib/omniauth/strategies/bitbucket.rb +++ b/lib/omni_auth/strategies/bitbucket.rb diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 69d9587771f..3a1d4e2440f 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -26,7 +26,7 @@ require('~/project'); var fakeAjaxResponse = function fakeAjaxResponse(req) { var d; expect(req.url).toBe('/api/v3/projects.json?simple=true'); - expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20 }); + expect(req.data).toEqual({ search: '', order_by: 'last_activity_at', per_page: 20, membership: true }); d = $.Deferred(); d.resolve(this.projects_data); return d.promise(); diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index b983d73f8be..e76128ecd87 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -91,6 +91,54 @@ eos end end + describe '\ No newline at end of file' do + it "parses nonewline in one file correctly" do + first_nonewline_diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + +ipsum + lorem + -ipsum + \\ No newline at end of file + END + lines = parser.parse(first_nonewline_diff.lines).to_a + + expect(lines[0].type).to eq('new') + expect(lines[0].text).to eq('+ipsum') + expect(lines[2].type).to eq('old') + expect(lines[3].type).to eq('old-nonewline') + expect(lines[1].old_pos).to eq(1) + expect(lines[1].new_pos).to eq(2) + end + + it "parses nonewline in two files correctly" do + both_nonewline_diff = <<~END + --- a/test + +++ b/test + @@ -1,2 +1,2 @@ + -lorem + -ipsum + \\ No newline at end of file + +ipsum + +lorem + \\ No newline at end of file + END + lines = parser.parse(both_nonewline_diff.lines).to_a + + expect(lines[0].type).to eq('old') + expect(lines[1].type).to eq('old') + expect(lines[2].type).to eq('old-nonewline') + expect(lines[5].type).to eq('new-nonewline') + expect(lines[3].text).to eq('+ipsum') + expect(lines[3].old_pos).to eq(3) + expect(lines[3].new_pos).to eq(1) + expect(lines[4].text).to eq('+lorem') + expect(lines[4].old_pos).to eq(3) + expect(lines[4].new_pos).to eq(2) + end + end + context 'when lines is empty' do it { expect(parser.parse([])).to eq([]) } it { expect(parser.parse(nil)).to eq([]) } diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb index 69906382545..255b584a85e 100644 --- a/spec/models/concerns/relative_positioning_spec.rb +++ b/spec/models/concerns/relative_positioning_spec.rb @@ -12,12 +12,6 @@ describe Issue, 'RelativePositioning' do end end - describe '#min_relative_position' do - it 'returns maximum position' do - expect(issue.min_relative_position).to eq issue.relative_position - end - end - describe '#max_relative_position' do it 'returns maximum position' do expect(issue.max_relative_position).to eq issue1.relative_position @@ -29,8 +23,8 @@ describe Issue, 'RelativePositioning' do expect(issue1.prev_relative_position).to eq issue.relative_position end - it 'returns minimum position if there is no issue above' do - expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION + it 'returns nil if there is no issue above' do + expect(issue.prev_relative_position).to eq nil end end @@ -39,8 +33,8 @@ describe Issue, 'RelativePositioning' do expect(issue.next_relative_position).to eq issue1.relative_position end - it 'returns next position if there is no issue below' do - expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION + it 'returns nil if there is no issue below' do + expect(issue1.next_relative_position).to eq nil end end @@ -72,6 +66,34 @@ describe Issue, 'RelativePositioning' do end end + describe '#shift_after?' do + it 'returns true' do + issue.update(relative_position: issue1.relative_position - 1) + + expect(issue.shift_after?).to be_truthy + end + + it 'returns false' do + issue.update(relative_position: issue1.relative_position - 2) + + expect(issue.shift_after?).to be_falsey + end + end + + describe '#shift_before?' do + it 'returns true' do + issue.update(relative_position: issue1.relative_position + 1) + + expect(issue.shift_before?).to be_truthy + end + + it 'returns false' do + issue.update(relative_position: issue1.relative_position + 2) + + expect(issue.shift_before?).to be_falsey + end + end + describe '#move_between' do it 'positions issue between two other' do new_issue.move_between(issue, issue1) @@ -100,5 +122,83 @@ describe Issue, 'RelativePositioning' do expect(new_issue.relative_position).to be > issue.relative_position expect(issue.relative_position).to be < issue1.relative_position end + + it 'positions issues between other two if distance is 1' do + issue1.update relative_position: issue.relative_position + 1 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be > issue.relative_position + expect(issue.relative_position).to be < issue1.relative_position + end + + it 'positions issue in the middle of other two if distance is big enough' do + issue.update relative_position: 6000 + issue1.update relative_position: 10000 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to eq(8000) + end + + it 'positions issue closer to the middle if we are at the very top' do + issue1.update relative_position: 6000 + + new_issue.move_between(nil, issue1) + + expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE) + end + + it 'positions issue closer to the middle if we are at the very bottom' do + issue.update relative_position: 6000 + issue1.update relative_position: nil + + new_issue.move_between(issue, nil) + + expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE) + end + + it 'positions issue in the middle of other two if distance is not big enough' do + issue.update relative_position: 100 + issue1.update relative_position: 400 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to eq(250) + end + + it 'positions issue in the middle of other two is there is no place' do + issue.update relative_position: 100 + issue1.update relative_position: 101 + + new_issue.move_between(issue, issue1) + + expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position) + end + + it 'uses rebalancing if there is no place' do + issue.update relative_position: 100 + issue1.update relative_position: 101 + issue2 = create(:issue, relative_position: 102, project: project) + new_issue.update relative_position: 103 + + new_issue.move_between(issue1, issue2) + new_issue.save! + + expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) + expect(issue.reload.relative_position).not_to eq(100) + end + + it 'positions issue right if we pass none-sequential parameters' do + issue.update relative_position: 99 + issue1.update relative_position: 101 + issue2 = create(:issue, relative_position: 102, project: project) + new_issue.update relative_position: 103 + + new_issue.move_between(issue, issue2) + new_issue.save! + + expect(new_issue.relative_position).to be(100) + end end end diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index cacbab8bcb1..55b87d1c48a 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -92,6 +92,41 @@ describe GlobalMilestone, models: true do end end + describe '.states_count' do + context 'when the projects have milestones' do + before do + create(:closed_milestone, title: 'Active Group Milestone', project: project3) + create(:active_milestone, title: 'Active Group Milestone', project: project1) + create(:active_milestone, title: 'Active Group Milestone', project: project2) + create(:closed_milestone, title: 'Closed Group Milestone', project: project1) + create(:closed_milestone, title: 'Closed Group Milestone', project: project2) + create(:closed_milestone, title: 'Closed Group Milestone', project: project3) + end + + it 'returns the quantity of global milestones in each possible state' do + expected_count = { opened: 1, closed: 2, all: 2 } + + count = GlobalMilestone.states_count(Project.all) + + expect(count).to eq(expected_count) + end + end + + context 'when the projects do not have milestones' do + before do + project1 + end + + it 'returns 0 as the quantity of global milestones in each state' do + expected_count = { opened: 0, closed: 0, all: 0 } + + count = GlobalMilestone.states_count(Project.all) + + expect(count).to eq(expected_count) + end + end + end + describe '#initialize' do let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } @@ -127,4 +162,32 @@ describe GlobalMilestone, models: true do expect(global_milestone.safe_title).to eq('git-test') end end + + describe '#state' do + context 'when at least one milestone is active' do + it 'returns active' do + title = 'Active Group Milestone' + milestones = [ + create(:active_milestone, title: title), + create(:closed_milestone, title: title) + ] + global_milestone = GlobalMilestone.new(title, milestones) + + expect(global_milestone.state).to eq('active') + end + end + + context 'when all milestones are closed' do + it 'returns closed' do + title = 'Closed Group Milestone' + milestones = [ + create(:closed_milestone, title: title), + create(:closed_milestone, title: title) + ] + global_milestone = GlobalMilestone.new(title, milestones) + + expect(global_milestone.state).to eq('closed') + end + end + end end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 62740ec29fd..aa14709bc9c 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,6 +1,7 @@ require 'capybara/rails' require 'capybara/rspec' require 'capybara/poltergeist' +require 'capybara-screenshot/rspec' # Give CI some extra time timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10 @@ -21,12 +22,8 @@ end Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = true -unless ENV['CI'] || ENV['CI_SERVER'] - require 'capybara-screenshot/rspec' - - # Keep only the screenshots generated from the last failing test suite - Capybara::Screenshot.prune_strategy = :keep_last_run -end +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run RSpec.configure do |config| config.before(:suite) do |