summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/visual_review_toolbar/index.js2
-rw-r--r--app/assets/javascripts/visual_review_toolbar/styles/toolbar.css149
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/controllers/admin/projects_controller.rb11
-rw-r--r--app/helpers/storage_helper.rb3
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/project_statistics.rb16
-rw-r--r--app/services/projects/update_statistics_service.rb2
-rw-r--r--app/views/admin/projects/_projects.html.haml4
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/project_cache_worker.rb4
-rw-r--r--changelogs/unreleased/61024-update-resolved-icon.yml5
-rw-r--r--changelogs/unreleased/add-wiki-size-to-statistics.yml5
-rw-r--r--config/routes/admin.rb2
-rw-r--r--config/webpack.config.review_toolbar.js58
-rw-r--r--db/migrate/20190527194830_add_wiki_size_to_statistics.rb9
-rw-r--r--db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb32
-rw-r--r--db/schema.rb3
-rw-r--r--doc/api/boards.md179
-rw-r--r--doc/api/discussions.md216
-rw-r--r--doc/api/epic_issues.md1
-rw-r--r--doc/api/epic_links.md1
-rw-r--r--doc/api/groups.md1
-rw-r--r--doc/api/notes.md130
-rw-r--r--doc/api/projects.md5
-rw-r--r--doc/api/resource_label_events.md86
-rw-r--r--doc/api/scim.md2
-rw-r--r--doc/api/services.md69
-rw-r--r--doc/api/vulnerabilities.md2
-rw-r--r--doc/ci/variables/README.md5
-rw-r--r--doc/development/geo.md78
-rw-r--r--doc/user/admin_area/index.md24
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/gitlab/background_migration/calculate_wiki_sizes.rb18
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/tasks/gitlab/assets.rake6
-rw-r--r--locale/gitlab.pot2
-rw-r--r--package.json5
-rw-r--r--qa/docs/best_practices.md6
-rw-r--r--qa/docs/writing_tests_from_scratch.md418
-rw-r--r--qa/qa/page/project/branches/show.rb8
-rw-r--r--qa/qa/resource/base.rb64
-rw-r--r--qa/qa/resource/file.rb31
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb26
-rw-r--r--spec/features/admin/admin_sees_project_statistics_spec.rb2
-rw-r--r--spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap2
-rw-r--r--spec/helpers/storage_helper_spec.rb3
-rw-r--r--spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb62
-rw-r--r--spec/models/namespace_spec.rb6
-rw-r--r--spec/models/project_statistics_spec.rb68
-rw-r--r--spec/requests/api/groups_spec.rb3
-rw-r--r--spec/services/projects/update_statistics_service_spec.rb12
-rw-r--r--spec/workers/project_cache_worker_spec.rb5
-rw-r--r--vendor/assets/javascripts/visual_review_toolbar.js (renamed from public/visual-review-toolbar.js)355
-rw-r--r--yarn.lock12
59 files changed, 1659 insertions, 575 deletions
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index c7cfc0f0f3b..307e56708e0 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -57,7 +57,7 @@ export default {
class="line-resolve-btn is-disabled"
type="button"
>
- <icon name="check-circle" />
+ <icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" />
</span>
<span class="line-resolve-text">
{{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }}
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 5a4ff15d198..78f397ccc12 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -135,7 +135,7 @@ export default {
@click="onResolve"
>
<template v-if="!isResolving">
- <icon name="check-circle" />
+ <icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" />
</template>
<gl-loading-icon v-else inline />
</button>
diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js
new file mode 100644
index 00000000000..91d0382feac
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/index.js
@@ -0,0 +1,2 @@
+import './styles/toolbar.css';
+import 'vendor/visual_review_toolbar';
diff --git a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
new file mode 100644
index 00000000000..342b3599a44
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
@@ -0,0 +1,149 @@
+/*
+ As a standalone script, the toolbar has its own css
+ */
+
+#gitlab-collapse > * {
+ pointer-events: none;
+}
+
+#gitlab-form-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%
+}
+
+#gitlab-review-container {
+ max-width: 22rem;
+ max-height: 22rem;
+ overflow: scroll;
+ position: fixed;
+ bottom: 1rem;
+ right: 1rem;
+ display: flex;
+ flex-direction: row-reverse;
+ padding: 1rem;
+ background-color: #fff;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
+ 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+ font-size: .8rem;
+ font-weight: 400;
+ color: #2e2e2e;
+}
+
+.gitlab-open-wrapper {
+ max-width: 22rem;
+ max-height: 22rem;
+}
+
+.gitlab-closed-wrapper {
+ max-width: 3.4rem;
+ max-height: 3.4rem;
+}
+
+.gitlab-button {
+ cursor: pointer;
+ transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear;
+}
+
+.gitlab-button-secondary {
+ background: none #fff;
+ margin: 0 .5rem;
+ border: 1px solid #e3e3e3;
+}
+
+.gitlab-button-secondary:hover {
+ background-color: #f0f0f0;
+ border-color: #e3e3e3;
+ color: #2e2e2e;
+}
+
+.gitlab-button-secondary:active {
+ color: #2e2e2e;
+ background-color: #e1e1e1;
+ border-color: #dadada;
+}
+
+.gitlab-button-success:hover {
+ color: #fff;
+ background-color: #137e3f;
+ border-color: #127339;
+}
+
+.gitlab-button-success:active {
+ background-color: #168f48;
+ border-color: #12753a;
+ color: #fff;
+}
+
+.gitlab-button-success {
+ background-color: #1aaa55;
+ border: 1px solid #168f48;
+ color: #fff;
+}
+
+.gitlab-button-wide {
+ width: 100%;
+}
+
+.gitlab-button-wrapper {
+ margin-top: 1rem;
+ display: flex;
+ align-items: baseline;
+ justify-content: flex-end;
+}
+
+.gitlab-collapse {
+ width: 2.4rem;
+ height: 2.2rem;
+ margin-left: 1rem;
+ padding: .5rem;
+}
+
+.gitlab-collapse-closed {
+ align-self: center;
+}
+
+.gitlab-checkbox-label {
+ padding: 0 .2rem;
+}
+
+.gitlab-checkbox-wrapper {
+ display: flex;
+ align-items: baseline;
+}
+
+.gitlab-label {
+ font-weight: 600;
+ display: inline-block;
+ width: 100%;
+}
+
+.gitlab-link {
+ color: #1b69b6;
+ text-decoration: none;
+ background-color: transparent;
+ background-image: none;
+}
+
+.gitlab-message {
+ padding: .25rem 0;
+ margin: 0;
+ line-height: 1.2rem;
+}
+
+.gitlab-metadata-note {
+ font-size: .7rem;
+ line-height: 1rem;
+ color: #666;
+ margin-bottom: 0;
+}
+
+.gitlab-input {
+ width: 100%;
+ border: 1px solid #dfdfdf;
+ border-radius: 4px;
+ padding: .1rem .2rem;
+ min-height: 2rem;
+ max-width: 17rem;
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 50c87e55f56..170432a9e62 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -822,6 +822,7 @@ $note-form-margin-left: 72px;
.line-resolve-btn {
margin-right: 5px;
+ color: $gray-darkest;
svg {
vertical-align: middle;
@@ -836,7 +837,6 @@ $note-form-margin-left: 72px;
background-color: transparent;
border: 0;
outline: 0;
- color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled {
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index aeff0c96b64..70db15916b9 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,7 +3,7 @@
class Admin::ProjectsController < Admin::ApplicationController
include MembersPresentation
- before_action :project, only: [:show, :transfer, :repository_check]
+ before_action :project, only: [:show, :transfer, :repository_check, :destroy]
before_action :group, only: [:show, :transfer]
def index
@@ -35,6 +35,15 @@ class Admin::ProjectsController < Admin::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def destroy
+ ::Projects::DestroyService.new(@project, current_user, {}).async_execute
+ flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
+
+ redirect_to admin_projects_path, status: :found
+ rescue Projects::DestroyService::DestroyError => ex
+ redirect_to admin_projects_path, status: 302, alert: ex.message
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def transfer
namespace = Namespace.find_by(id: params[:new_namespace_id])
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index e80b3f2b54a..ecf37bae6b3 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -12,10 +12,11 @@ module StorageHelper
def storage_counters_details(statistics)
counters = {
counter_repositories: storage_counter(statistics.repository_size),
+ counter_wikis: storage_counter(statistics.wiki_size),
counter_build_artifacts: storage_counter(statistics.build_artifacts_size),
counter_lfs_objects: storage_counter(statistics.lfs_objects_size)
}
- _("%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters
+ _("%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7393ef4b05c..f7c31890198 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -76,6 +76,7 @@ class Namespace < ApplicationRecord
'namespaces.*',
'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
+ 'COALESCE(SUM(ps.wiki_size), 0) AS wiki_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
'COALESCE(SUM(ps.packages_size), 0) AS packages_size'
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 6fe8cb40d25..832c8417b5b 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -4,9 +4,16 @@ class ProjectStatistics < ApplicationRecord
belongs_to :project
belongs_to :namespace
+ default_value_for :wiki_size, 0
+
+ # older migrations fail due to non-existent attribute without this
+ def wiki_size
+ has_attribute?(:wiki_size) ? super : 0
+ end
+
before_save :update_storage_size
- COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze
+ COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count].freeze
INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze
def total_repository_size
@@ -27,11 +34,14 @@ class ProjectStatistics < ApplicationRecord
self.commit_count = project.repository.commit_count
end
- # Repository#size needs to be converted from MB to Byte.
def update_repository_size
self.repository_size = project.repository.size * 1.megabyte
end
+ def update_wiki_size
+ self.wiki_size = project.wiki.repository.size * 1.megabyte
+ end
+
def update_lfs_objects_size
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
@@ -42,7 +52,7 @@ class ProjectStatistics < ApplicationRecord
end
def update_storage_size
- self.storage_size = repository_size + lfs_objects_size + build_artifacts_size + packages_size
+ self.storage_size = repository_size + wiki_size + lfs_objects_size + build_artifacts_size + packages_size
end
# Since this incremental update method does not call update_storage_size above,
diff --git a/app/services/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb
index f32a779fab0..28677a398f3 100644
--- a/app/services/projects/update_statistics_service.rb
+++ b/app/services/projects/update_statistics_service.rb
@@ -3,7 +3,7 @@
module Projects
class UpdateStatisticsService < BaseService
def execute
- return unless project && project.repository.exists?
+ return unless project
Rails.logger.info("Updating statistics for project #{project.id}")
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index 9117f63f939..2f7ad35eb3e 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -7,7 +7,7 @@
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
%button.delete-project-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-project-modal',
- delete_project_url: project_path(project),
+ delete_project_url: admin_project_path(project),
project_name: project.name }, type: 'button' }
= s_('AdminProjects|Delete')
@@ -17,7 +17,7 @@
- if project.archived
%span.badge.badge-warning archived
.title
- = link_to(admin_namespace_project_path(project.namespace, project)) do
+ = link_to(admin_project_path(project)) do
.dash-project-avatar
.avatar-container.rect-avatar.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 9a9c0c9d803..3f1639ec2ed 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -74,6 +74,8 @@ class PostReceive
def process_wiki_changes(post_received)
post_received.project.touch(:last_activity_at, :last_repository_updated_at)
+ post_received.project.wiki.repository.expire_statistics_caches
+ ProjectCacheWorker.perform_async(post_received.project.id, [], [:wiki_size])
end
def log(message)
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index b2e0701008a..4e8ea903139 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -16,10 +16,12 @@ class ProjectCacheWorker
def perform(project_id, files = [], statistics = [])
project = Project.find_by(id: project_id)
- return unless project && project.repository.exists?
+ return unless project
update_statistics(project, statistics)
+ return unless project.repository.exists?
+
project.repository.refresh_method_caches(files.map(&:to_sym))
project.cleanup
diff --git a/changelogs/unreleased/61024-update-resolved-icon.yml b/changelogs/unreleased/61024-update-resolved-icon.yml
new file mode 100644
index 00000000000..4a4de9eb13a
--- /dev/null
+++ b/changelogs/unreleased/61024-update-resolved-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Add check circle filled icon for resolved comments
+merge_request: 28663
+author:
+type: changed
diff --git a/changelogs/unreleased/add-wiki-size-to-statistics.yml b/changelogs/unreleased/add-wiki-size-to-statistics.yml
new file mode 100644
index 00000000000..85b6d7a1774
--- /dev/null
+++ b/changelogs/unreleased/add-wiki-size-to-statistics.yml
@@ -0,0 +1,5 @@
+---
+title: Add wiki size to project statistics
+merge_request: 25321
+author: Peter Marko
+type: added
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index bc19219a0b8..ae79beb1dba 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -83,7 +83,7 @@ namespace :admin do
resources(:projects,
path: '/',
constraints: { id: Gitlab::PathRegex.project_route_regex },
- only: [:show]) do
+ only: [:show, :destroy]) do
member do
put :transfer
diff --git a/config/webpack.config.review_toolbar.js b/config/webpack.config.review_toolbar.js
new file mode 100644
index 00000000000..baaba7ed387
--- /dev/null
+++ b/config/webpack.config.review_toolbar.js
@@ -0,0 +1,58 @@
+const path = require('path');
+const CompressionPlugin = require('compression-webpack-plugin');
+
+const ROOT_PATH = path.resolve(__dirname, '..');
+const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache');
+const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS;
+const IS_PRODUCTION = process.env.NODE_ENV === 'production';
+
+const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map';
+
+const alias = {
+ vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
+ spec: path.join(ROOT_PATH, 'spec/javascripts'),
+};
+
+module.exports = {
+ mode: IS_PRODUCTION ? 'production' : 'development',
+
+ context: path.join(ROOT_PATH, 'app/assets/javascripts'),
+
+ name: 'visual_review_toolbar',
+
+ entry: './visual_review_toolbar',
+
+ output: {
+ path: path.join(ROOT_PATH, 'public/assets/webpack'),
+ filename: 'visual_review_toolbar.js',
+ library: 'VisualReviewToolbar',
+ libraryTarget: 'var',
+ },
+
+ resolve: {
+ alias,
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ loader: 'babel-loader',
+ options: {
+ cacheDirectory: path.join(CACHE_PATH, 'babel-loader'),
+ },
+ },
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
+
+ plugins: [
+ // compression can require a lot of compute time and is disabled in CI
+ new CompressionPlugin(),
+ ].filter(Boolean),
+
+ devtool: NO_SOURCEMAPS ? false : devtool,
+};
diff --git a/db/migrate/20190527194830_add_wiki_size_to_statistics.rb b/db/migrate/20190527194830_add_wiki_size_to_statistics.rb
new file mode 100644
index 00000000000..d4f16cdec18
--- /dev/null
+++ b/db/migrate/20190527194830_add_wiki_size_to_statistics.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddWikiSizeToStatistics < ActiveRecord::Migration[5.0]
+ DOWNTIME = false
+
+ def change
+ add_column :project_statistics, :wiki_size, :bigint
+ end
+end
diff --git a/db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb b/db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
new file mode 100644
index 00000000000..04cf5906b61
--- /dev/null
+++ b/db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class ScheduleCalculateWikiSizes < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'CalculateWikiSizes'
+ BATCH_SIZE = 100000
+ BATCH_TIME = 5.minutes
+
+ class ProjectStatistics < ActiveRecord::Base
+ self.table_name = 'project_statistics'
+
+ scope :without_wiki_size, -> { where(wiki_size: nil) }
+
+ include ::EachBatch
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(
+ ::ScheduleCalculateWikiSizes::ProjectStatistics.without_wiki_size,
+ MIGRATION,
+ BATCH_TIME,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index bb59af540fe..89140048ad3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20190524062810) do
+ActiveRecord::Schema.define(version: 20190527194900) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1745,6 +1745,7 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.bigint "lfs_objects_size", default: 0, null: false
t.bigint "build_artifacts_size", default: 0, null: false
t.bigint "packages_size"
+ t.bigint "wiki_size"
t.index ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree
t.index ["project_id"], name: "index_project_statistics_on_project_id", unique: true, using: :btree
end
diff --git a/doc/api/boards.md b/doc/api/boards.md
index 28c73db6b98..a96206f5df3 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -141,6 +141,173 @@ Example response:
}
```
+## Create a board **[STARTER]**
+
+Creates a board.
+
+```
+POST /projects/:id/boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the new board |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards?name=newboard
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site"
+ },
+ "name": "newboard",
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+```
+
+## Update a board **[STARTER]**
+
+> [Introduced][ee-5954] in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1.
+
+Updates a board.
+
+```
+PUT /projects/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `name` | string | no | The new name of the board |
+| `assignee_id` | integer | no | The assignee the board should be scoped to |
+| `milestone_id` | integer | no | The milestone the board should be scoped to |
+| `labels` | string | no | Comma-separated list of label names which the board should be scoped to |
+| `weight` | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
+
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1?name=new_name&milestone_id=43&assignee_id=1&labels=Doing&weight=4
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "created_at": "2018-07-03T05:48:49.982Z",
+ "default_branch": null,
+ "tag_list": [],
+ "ssh_url_to_repo": "ssh://user@example.com/diaspora/diaspora-project-site.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": null,
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "last_activity_at": "2018-07-03T05:48:49.982Z"
+ },
+ "lists": [],
+ "name": "new_name",
+ "group": null,
+ "milestone": {
+ "id": 43,
+ "iid": 1,
+ "project_id": 15,
+ "title": "Milestone 1",
+ "description": "Milestone 1 desc",
+ "state": "active",
+ "created_at": "2018-07-03T06:36:42.618Z",
+ "updated_at": "2018-07-03T06:36:42.618Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "http://example.com/root/board1/milestones/1"
+ },
+ "assignee": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://example.com/root"
+ },
+ "labels": [{
+ "id": 10,
+ "name": "Doing",
+ "color": "#5CB85C",
+ "description": null
+ }],
+ "weight": 4
+ }
+```
+
+## Delete a board **[STARTER]**
+
+Deletes a board.
+
+```
+DELETE /projects/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1
+```
+
## List board lists
Get a list of the board's lists.
@@ -237,7 +404,15 @@ POST /projects/:id/boards/:board_id/lists
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
-| `label_id` | integer | yes | The ID of a label |
+| `label_id` | integer | no | The ID of a label |
+| `assignee_id` **[PREMIUM]** | integer | no | The ID of a user |
+| `milestone_id` **[PREMIUM]** | integer | no | The ID of a milestone |
+
+NOTE: **Note**:
+Label, assignee and milestone arguments are mutually exclusive,
+that is, only one of them are accepted in a request.
+Check the [Issue Board docs](../user/project/issue_board.md#summary-of-features-per-tier)
+for more information regarding the required license for each list type.
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5
@@ -307,3 +482,5 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
```bash
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
```
+
+[ee-5954]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5954
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index 07a6201b10b..9defef4fd53 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -1,6 +1,12 @@
# Discussions API
-Discussions are set of related notes on snippets, issues, merge requests or commits.
+Discussions are a set of related notes on:
+
+- Snippets
+- Issues
+- Epics **[ULTIMATE]**
+- Merge requests
+- Commits
This includes system notes, which are notes about changes to the object (for example, when a milestone changes, there will be a corresponding system note). Label notes are not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
@@ -424,6 +430,214 @@ Parameters:
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/636
```
+## Epics **[ULTIMATE]**
+
+### List group epic discussions
+
+Gets a list of all discussions for a single epic.
+
+```
+GET /groups/:id/epics/:epic_id/discussions
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+
+```json
+[
+ {
+ "id": "6a9c1750b37d513a43987b574953fceb50b03ce7",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1126,
+ "type": "DiscussionNote",
+ "body": "discussion text",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-03T21:54:39.668Z",
+ "updated_at": "2018-03-03T21:54:39.668Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Epic",
+ "noteable_id": null,
+ "resolvable": false
+ },
+ {
+ "id": 1129,
+ "type": "DiscussionNote",
+ "body": "reply to the discussion",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T13:38:02.127Z",
+ "updated_at": "2018-03-04T13:38:02.127Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Epic",
+ "noteable_id": null,
+ "resolvable": false
+ }
+ ]
+ },
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": true,
+ "notes": [
+ {
+ "id": 1128,
+ "type": null,
+ "body": "a single comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Epic",
+ "noteable_id": null,
+ "resolvable": false
+ }
+ ]
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions
+```
+
+### Get single epic discussion
+
+Returns a single discussion for a specific group epic
+
+```
+GET /groups/:id/epics/:epic_id/discussions/:discussion_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7
+```
+
+### Create new epic discussion
+
+Creates a new discussion to a single group epic. This is similar to creating
+a note but but another comments (replies) can be added to it later.
+
+```
+POST /groups/:id/epics/:epic_id/discussions
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions?body=comment
+```
+
+### Add note to existing epic discussion
+
+Adds a new note to the discussion. This can also
+[create a discussion from a single comment](../user/discussions/#start-a-discussion-by-replying-to-a-standard-comment).
+
+```
+POST /groups/:id/epics/:epic_id/discussions/:discussion_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
+```
+
+### Modify existing epic discussion note
+
+Modify existing discussion note of an epic.
+
+```
+PUT /groups/:id/epics/:epic_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment
+```
+
+### Delete an epic discussion note
+
+Deletes an existing discussion note of an epic.
+
+```
+DELETE /groups/:id/epics/:epic_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/636
+```
+
## Merge requests
### List project merge request discussions
diff --git a/doc/api/epic_issues.md b/doc/api/epic_issues.md
index 438a3361dcc..ec59ea7068e 100644
--- a/doc/api/epic_issues.md
+++ b/doc/api/epic_issues.md
@@ -7,6 +7,7 @@ If a user is not a member of a group and the group is private, a `GET` request o
Epics are available only in Ultimate. If epics feature is not available a `403` status code will be returned.
## List issues for an epic
+
Gets all issues that are assigned to an epic and the authenticated user has access to.
```
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
index d6e43ae7074..9ad90a6d0f1 100644
--- a/doc/api/epic_links.md
+++ b/doc/api/epic_links.md
@@ -12,6 +12,7 @@ If a user is not a member of a group and the group is private, a `GET` request o
Epics are available only in the [Ultimate/Gold tier](https://about.gitlab.com/pricing/). If the epics feature is not available, a `403` status code will be returned.
## List epics related to a given epic
+
Gets all child epics of an epic.
```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 907b443d355..20789a1d4a4 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -68,6 +68,7 @@ GET /groups?statistics=true
"statistics": {
"storage_size" : 212,
"repository_size" : 33,
+ "wiki_size" : 100,
"lfs_objects_size" : 123,
"job_artifacts_size" : 57
diff --git a/doc/api/notes.md b/doc/api/notes.md
index dfde80c6441..c09129c22d4 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -1,6 +1,11 @@
# Notes API
-Notes are comments on snippets, issues or merge requests.
+Notes are comments on:
+
+- Snippets
+- Issues
+- Merge requests
+- Epics **[ULTIMATE]**
This includes system notes, which are notes about changes to the object (for example, when a milestone changes, there will be a corresponding system note). Label notes are not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
@@ -390,3 +395,126 @@ Parameters:
```bash
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/7/notes/1602
```
+
+## Epics **[ULTIMATE]**
+
+### List all epic notes
+
+Gets a list of all notes for a single epic. Epic notes are comments users can post to an epic.
+
+```
+GET /groups/:id/epics/:epic_id/notes
+GET /groups/:id/epics/:epic_id/notes?sort=asc&order_by=updated_at
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of a group epic |
+| `sort` | string | no | Return epic notes sorted in `asc` or `desc` order. Default is `desc` |
+| `order_by` | string | no | Return epic notes ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/notes
+```
+
+### Get single epic note
+
+Returns a single note for a given epic.
+
+```
+GET /groups/:id/epics/:epic_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `note_id` | integer | yes | The ID of a note |
+
+```json
+{
+ "id": 52,
+ "title": "Epic",
+ "file_name": "epic.rb",
+ "author": {
+ "id": 1,
+ "username": "pipin",
+ "email": "admin@example.com",
+ "name": "Pip",
+ "state": "active",
+ "created_at": "2013-09-30T13:46:01Z"
+ },
+ "expires_at": null,
+ "updated_at": "2013-10-02T07:34:20Z",
+ "created_at": "2013-10-02T07:34:20Z"
+}
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/notes/1
+```
+
+### Create new epic note
+
+Creates a new note for a single epic. Epic notes are comments users can post to an epic.
+If you create a note where the body only contains an Award Emoji, you'll receive this object back.
+
+```
+POST /groups/:id/epics/:epic_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `body` | string | yes | The content of a note |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
+```
+
+### Modify existing epic note
+
+Modify existing note of an epic.
+
+```
+PUT /groups/:id/epics/:epic_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `note_id` | integer | yes | The ID of a note |
+| `body` | string | yes | The content of a note |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
+```
+
+### Delete an epic note
+
+Deletes an existing note of an epic.
+
+```
+DELETE /groups/:id/epics/:epic_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `note_id` | integer | yes | The ID of a note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/52/notes/1659
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 951961e45ff..75669d85803 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -153,6 +153,7 @@ When the user is authenticated and `simple` is not set this returns something li
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -234,6 +235,7 @@ When the user is authenticated and `simple` is not set this returns something li
"commit_count": 12,
"storage_size": 2066080,
"repository_size": 2066080,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -342,6 +344,7 @@ GET /users/:user_id/projects
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -423,6 +426,7 @@ GET /users/:user_id/projects
"commit_count": 12,
"storage_size": 2066080,
"repository_size": 2066080,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -548,6 +552,7 @@ GET /projects/:id
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
diff --git a/doc/api/resource_label_events.md b/doc/api/resource_label_events.md
index e1f9ffa9472..f0a7ac4e41d 100644
--- a/doc/api/resource_label_events.md
+++ b/doc/api/resource_label_events.md
@@ -88,6 +88,92 @@ Parameters:
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/resource_label_events/1
```
+## Epics **[ULTIMATE]**
+
+### List group epic label events
+
+Gets a list of all label events for a single epic.
+
+```
+GET /groups/:id/epics/:epic_id/resource_label_events
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+
+```json
+[
+ {
+ "id": 106,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-19T11:43:01.746Z",
+ "resource_type": "Epic",
+ "resource_id": 33,
+ "label": {
+ "id": 73,
+ "name": "a1",
+ "color": "#34495E",
+ "description": ""
+ },
+ "action": "add"
+ },
+ {
+ "id": 107,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-19T11:43:01.746Z",
+ "resource_type": "Epic",
+ "resource_id": 33,
+ "label": {
+ "id": 37,
+ "name": "glabel2",
+ "color": "#A8D695",
+ "description": ""
+ },
+ "action": "add"
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/11/resource_label_events
+```
+
+### Get single epic label event
+
+Returns a single label event for a specific group epic
+
+```
+GET /groups/:id/epics/:epic_id/resource_label_events/:resource_label_event_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `resource_label_event_id` | integer | yes | The ID of a label event |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/11/resource_label_events/107
+```
+
## Merge requests
### List project merge request label events
diff --git a/doc/api/scim.md b/doc/api/scim.md
index 4595c6f2ed3..3870ea788e7 100644
--- a/doc/api/scim.md
+++ b/doc/api/scim.md
@@ -1,4 +1,4 @@
-# SCIM API
+# SCIM API **[SILVER ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
diff --git a/doc/api/services.md b/doc/api/services.md
index 01df2a50198..898cfad7254 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -1101,6 +1101,75 @@ Get JetBrains TeamCity CI service settings for a project.
GET /projects/:id/services/teamcity
```
+## Jenkins CI **[STARTER]**
+
+A continuous integration and build server
+
+### Create/Edit Jenkins CI service
+
+Set Jenkins CI service for a project.
+
+```
+PUT /projects/:id/services/jenkins
+```
+
+Parameters:
+
+- `jenkins_url` (**required**) - Jenkins URL like http://jenkins.example.com
+- `project_name` (**required**) - The URL-friendly project name. Example: my_project_name
+- `username` (optional) - A user with access to the Jenkins server, if applicable
+- `password` (optional) - The password of the user
+
+### Delete Jenkins CI service
+
+Delete Jenkins CI service for a project.
+
+```
+DELETE /projects/:id/services/jenkins
+```
+
+### Get Jenkins CI service settings
+
+Get Jenkins CI service settings for a project.
+
+```
+GET /projects/:id/services/jenkins
+```
+
+## Jenkins CI (Deprecated) Service
+
+A continuous integration and build server
+
+### Create/Edit Jenkins CI (Deprecated) service
+
+Set Jenkins CI (Deprecated) service for a project.
+
+```
+PUT /projects/:id/services/jenkins-deprecated
+```
+
+Parameters:
+
+- `project_url` (**required**) - Jenkins project URL like http://jenkins.example.com/job/my-project/
+- `multiproject_enabled` (optional) - Multi-project mode is configured in Jenkins GitLab Hook plugin
+- `pass_unstable` (optional) - Unstable builds will be treated as passing
+
+### Delete Jenkins CI (Deprecated) service
+
+Delete Jenkins CI (Deprecated) service for a project.
+
+```
+DELETE /projects/:id/services/jenkins-deprecated
+```
+
+### Get Jenkins CI (Deprecated) service settings
+
+Get Jenkins CI (Deprecated) service settings for a project.
+
+```
+GET /projects/:id/services/jenkins-deprecated
+```
+
[jira-doc]: ../user/project/integrations/jira.md
[old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira
diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md
index 87f77613ad3..390d0966244 100644
--- a/doc/api/vulnerabilities.md
+++ b/doc/api/vulnerabilities.md
@@ -1,4 +1,4 @@
-# Vulnerabilities API
+# Vulnerabilities API **[ULTIMATE]**
Every API call to vulnerabilities must be authenticated.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index d33275cae4f..2157a6dc097 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -91,10 +91,9 @@ This means that the value of the variable will be hidden in job logs,
though it must match certain requirements to do so:
- The value must be in a single line.
-- The value must not have escape characters.
-- The value must not use variables.
-- The value must not have any whitespace.
+- The value must contain only letters, numbers, or underscores.
- The value must be at least 8 characters long.
+- The value must not use variables.
If the value does not meet the requirements above, then the CI variable will fail to save.
In order to save, either alter the value to meet the masking requirements
diff --git a/doc/development/geo.md b/doc/development/geo.md
index 87ec34ec5c4..6e59fab34c7 100644
--- a/doc/development/geo.md
+++ b/doc/development/geo.md
@@ -10,6 +10,7 @@ the diagram below and are described in more detail within this document.
## Replication layer
Geo handles replication for different components:
+
- [Database](#database-replication): includes the entire application, except cache and jobs.
- [Git repositories](#repository-replication): includes both projects and wikis.
- [Uploaded blobs](#uploads-replication): includes anything from images attached on issues
@@ -209,20 +210,38 @@ bundle exec rake geo:db:migrate
### Foreign Data Wrapper
-The use of [FDW](#fdw) was introduced in GitLab 10.1.
+> Introduced in GitLab 10.1.
+
+Foreign Data Wrapper ([FDW](#fdw)) is used by the [Geo Log Cursor](#geo-log-cursor) and improves
+the performance of many synchronization operations.
-This is useful for the [Geo Log Cursor](#geo-log-cursor) and improves
-the performance of some synchronization operations.
+FDW is a PostgreSQL extension ([`postgres_fdw`](https://www.postgresql.org/docs/current/postgres-fdw.html)) that is enabled within
+the Geo Tracking Database (on a **secondary** node), which allows it
+to connect to the readonly database replica and perform queries and filter
+data from both instances.
While FDW is available in older versions of PostgreSQL, we needed to
raise the minimum required version to 9.6 as this includes many
performance improvements to the FDW implementation.
+This persistent connection is configured as an FDW server
+named `gitlab_secondary`. This configuration exists within the database's user
+context only. To access the `gitlab_secondary`, GitLab needs to use the
+same database user that had previously been configured.
+
+The Geo Tracking Database accesses the readonly database replica via FDW as a regular user,
+limited by its own restrictions. The credentials are configured as a
+`USER MAPPING` associated with the `SERVER` mapped previously
+(`gitlab_secondary`).
+
+FDW configuration and credentials definition are managed automatically by the
+Omnibus GitLab `gitlab-ctl reconfigure` command.
+
#### Refeshing the Foreign Tables
-Whenever the database schema changes on the **primary** node, the
-**secondary** node will need to refresh its foreign tables by running
-the following:
+Whenever a new Geo node is configured or the database schema changes on the
+**primary** node, you must refresh the foreign tables on the **secondary** node
+by running the following:
```sh
bundle exec rake geo:db:refresh_foreign_tables
@@ -243,6 +262,53 @@ STATEMENT: SELECT a.attname, format_type(a.atttypid, a.atttypmod)
ORDER BY a.attnum
```
+#### Accessing data from a Foreign Table
+
+At the SQL level, all you have to do is `SELECT` data from `gitlab_secondary.*`.
+
+Here's an example of how to access all projects from the Geo Tracking Database's FDW:
+
+```sql
+SELECT * FROM gitlab_secondary.projects;
+```
+
+As a more real-world example, this is how you filter for unarchived projects
+on the Tracking Database:
+
+```sql
+SELECT project_registry.*
+ FROM project_registry
+ JOIN gitlab_secondary.projects
+ ON (project_registry.project_id = gitlab_secondary.projects.id
+ AND gitlab_secondary.projects.archived IS FALSE)
+```
+
+At the ActiveRecord level, we have additional Models that represent the
+foreign tables. They must be mapped in a slightly different way, and they are read-only.
+
+Check the existing FDW models in `ee/app/models/geo/fdw` for reference.
+
+From a developer's perspective, it's no different than creating a model that
+represents a Database View.
+
+With the examples above, you can access the projects with:
+
+```ruby
+Geo::Fdw::Project.all
+```
+
+and to access the `ProjectRegistry` filtering by unarchived projects:
+
+```ruby
+# We have to use Arel here:
+project_registry_table = Geo::ProjectRegistry.arel_table
+fdw_project_table = Geo::Fdw::Project.arel_table
+
+project_registry_table.join(fdw_project_table)
+ .on(project_registry_table[:project_id].eq(fdw_project_table[:id]))
+ .where((fdw_project_table[:archived]).eq(true)) # if you append `.to_sql` you can check generated query
+```
+
## Finders
Geo uses [Finders](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/app/finders),
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 4ed1287abbc..db5da26ca0a 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -114,3 +114,27 @@ To change the sort order, click the sort dropdown and select the desired order.
To search for users, enter your criteria in the search field. The user search is case
insensitive, and applies partial matching to name and username. To search for an email address,
you must provide the complete email address.
+
+## Administer Jobs
+
+You can administer all jobs in the GitLab instance from the Admin Area's Jobs page.
+
+To access the Jobs page, go to **Admin Area > Overview > Jobs**.
+
+All jobs are listed, in reverse order of their job ID.
+
+Click the **All** tab to list all jobs. Click the **Pending**, **Running**, or **Finished** tab to list only jobs of that status.
+
+For each job, the following details are listed:
+
+| Field | Description |
+|--------- | ----------- |
+| Status | Job status, either **passed**, **skipped**, or **failed**. |
+| Job | Includes links to the job, branch, and the commit that started the job. |
+| Pipeline | Includes a link to the specific pipeline. |
+| Project | Name of the project, and organization, to which the job belongs. |
+| Runner | Name of the CI runner assigned to execute the job. |
+| Stage | Stage that the job is declared in a `.gitlab-ci.yml` file. |
+| Name | Name of the job specified in a `.gitlab-ci.yml` file. |
+| Timing | Duration of the job, and how long ago the job completed. |
+| Coverage | Percentage of tests coverage. |
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 3d7cf2c0bb1..4e99fec02dd 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -303,6 +303,7 @@ module API
expose :commit_count
expose :storage_size
expose :repository_size
+ expose :wiki_size
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
end
@@ -355,6 +356,7 @@ module API
with_options format_with: -> (value) { value.to_i } do
expose :storage_size
expose :repository_size
+ expose :wiki_size
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
end
diff --git a/lib/gitlab/background_migration/calculate_wiki_sizes.rb b/lib/gitlab/background_migration/calculate_wiki_sizes.rb
new file mode 100644
index 00000000000..886c41a2b9d
--- /dev/null
+++ b/lib/gitlab/background_migration/calculate_wiki_sizes.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class CalculateWikiSizes
+ def perform(start_id, stop_id)
+ ::ProjectStatistics.where(wiki_size: nil)
+ .where(id: start_id..stop_id)
+ .includes(project: [:route, :group, namespace: [:owner]]).find_each do |statistics|
+ statistics.refresh!(only: [:wiki_size])
+ rescue => e
+ Rails.logger.error "Failed to update wiki statistics. id: #{statistics.id} message: #{e.message}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index aa2c1ac9cef..a07b1246bee 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -58,7 +58,6 @@ module Gitlab
uploads
users
v2
- visual-review-toolbar.js
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 7a42e4e92a0..a07ae3a418a 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -10,9 +10,15 @@ namespace :gitlab do
rake:assets:precompile
webpack:compile
gitlab:assets:fix_urls
+ gitlab:assets:compile_vrt
].each(&Gitlab::TaskHelpers.method(:invoke_and_time_task))
end
+ desc 'GitLab | Assets | Compile visual review toolbar'
+ task :compile_vrt do
+ system 'yarn', 'webpack-vrt'
+ end
+
desc 'GitLab | Assets | Clean up old compiled frontend assets'
task clean: ['rake:assets:clean']
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index af53da84bea..81a0ad46b77 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -107,7 +107,7 @@ msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
-msgid "%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS"
+msgid "%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS"
msgstr ""
msgid "%{count} more"
diff --git a/package.json b/package.json
index a4ec2dce0df..7c992f61c00 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,8 @@
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"test": "node scripts/frontend/test",
"webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
- "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js"
+ "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js",
+ "webpack-vrt": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.review_toolbar.js"
},
"dependencies": {
"@babel/core": "^7.4.4",
@@ -37,7 +38,7 @@
"@babel/preset-env": "^7.4.4",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.63.0",
- "@gitlab/ui": "^3.10.0",
+ "@gitlab/ui": "^3.10.1",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-upload-client": "^10.0.0",
diff --git a/qa/docs/best_practices.md b/qa/docs/best_practices.md
index d6e5350b0c8..62a90563bf0 100644
--- a/qa/docs/best_practices.md
+++ b/qa/docs/best_practices.md
@@ -4,7 +4,7 @@ The majority of the end-to-end tests require some state to be built in the appli
A good example is a user being logged in as a pre-condition for testing the feature.
-But if the login feature is already covered with end-to-end tests through the GUI, there is no reason to perform such an expensive task to test the functionality of creating a project, or importing a repo, even if this features depend on a user being logged in. Let's see an example to make things clear.
+But if the login feature is already covered with end-to-end tests through the GUI, there is no reason to perform such an expensive task to test the functionality of creating a project, or importing a repo, even if these features depend on a user being logged in. Let's see an example to make things clear.
Let's say that, on average, the process to perform a successful login through the GUI takes 2 seconds.
@@ -33,6 +33,6 @@ Finally, interacting with the application only by its GUI generates a higher rat
**The takeaways here are:**
- Building state through the GUI is time consuming and it's not sustainable as the test suite grows.
-- When depending only on the GUI to create the application's state and tests fail due to front-end issues, we can't rely on the test failures rate, and we generates a higher rate of test flakiness.
+- When depending only on the GUI to create the application's state and tests fail due to front-end issues, we can't rely on the test failures rate, and we generate a higher rate of test flakiness.
-Now that we are aware of all of it, [let's go create some tests](writing_tests_from_scratch.md).
+Now that we are aware of all of it, [let's go create some tests](writing_tests_from_scratch.md#this-document-covers-the-following-items).
diff --git a/qa/docs/writing_tests_from_scratch.md b/qa/docs/writing_tests_from_scratch.md
index 65e7a78a8b5..06b60a1ef45 100644
--- a/qa/docs/writing_tests_from_scratch.md
+++ b/qa/docs/writing_tests_from_scratch.md
@@ -12,22 +12,21 @@ If you don't exactly understand what we mean by **not everything needs to happen
## This document covers the following items:
-0. Identifying if end-to-end tests are really needed
-1. Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests
-2. Creating the skeleton of the test file (`*_spec.rb`)
-3. The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases logic
-4. Extracting duplicated code into methods
-5. Tests' pre-conditions (`before :all` and `before`) using resources and [Page Objects](./qa/page/README.md)
-6. Optimizing the test suite
-7. Using and implementing resources
-8. Moving elements definitions and its methods to [Page Objects](./qa/page/README.md)
- - Adding testability to the application
+- [0.](#0-are-end-to-end-tests-needed) Identifying if end-to-end tests are really needed
+- [1.](#1-identifying-the-devops-stage) Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests
+- [2.](#2-test-skeleton) Creating the skeleton of the test file (`*_spec.rb`)
+- [3.](#3-test-cases-mvc) The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases' logic
+- [4.](#4-extracting-duplicated-code) Extracting duplicated code into methods
+- [5.](#5-tests-pre-conditions-using-resources-and-page-objects) Tests' pre-conditions (`before :context` and `before`) using resources and [Page Objects](./qa/qa/page/README.md)
+- [6.](#6-optimization) Optimizing the test suite
+- [7.](#7-resources) Using and implementing resources
+- [8.](#8-page-objects) Moving element definitions and methods to [Page Objects](./qa/qa/page/README.md)
### 0. Are end-to-end tests needed?
-At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend to check the code coverage of a specific feature before writing end-to-end tests.
+At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend you check the code coverage of a specific feature before writing end-to-end tests, for both [CE](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby/#_AllFiles) and [EE](https://gitlab-org.gitlab.io/gitlab-ee/coverage-ruby/#_AllFiles) projects.
-Sometimes you may notice that there is already a good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
+Sometimes you may notice that there is already good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
If after this analysis you still think that end-to-end tests are needed, keep reading.
@@ -35,7 +34,7 @@ If after this analysis you still think that end-to-end tests are needed, keep re
The GitLab QA end-to-end tests are organized by the different [stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/specs/features/browser_ui), and so, if you are creating tests for issue creation, for instance, you would locate the spec files under the `qa/qa/specs/features/browser_ui/2_plan/` directory since issue creation is part of the Plan stage.
- In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge request is a feature from the Create stage.
+ In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge requests are a feature from the Create stage.
> There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`.
@@ -60,7 +59,7 @@ Specs have an outer `context` that indicates the DevOps stage. The next level is
```ruby
module QA
context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
+ describe 'Editing scoped labels on issues' do
end
end
end
@@ -68,12 +67,12 @@ end
#### The `it` blocks
-Every test suite is composed by at least one `it` block, and a good way to start writing end-to-end tests is by typing test cases descriptions as `it` blocks. Take a look at the following example:
+Every test suite is composed of at least one `it` block, and a good way to start writing end-to-end tests is by writing test cases descriptions as `it` blocks. These might help you to think of different test scenarios. Take a look at the following example:
```ruby
module QA
context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
+ describe 'Editing scoped labels on issues' do
it 'replaces an existing label if it has the same key' do
end
@@ -88,96 +87,104 @@ end
For the [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of our test cases, let's say that we already have the application in the state needed for the tests, and then let's focus on the logic of the test cases only.
-To evolve the test cases drafted on step 2, let's imagine that the user is already logged in a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `foo::bar`), there are other scoped labels (for the same scope and for a different scope, e.g. `foo::baz` and `bar::bah`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another.
+To evolve the test cases drafted on step 2, let's imagine that the user is already logged into a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `animal::fox`), there are other scoped labels (for the same scope and for a different scope (e.g. `animal::dolphin` and `plant::orchid`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another.
-> Note: there are different approaches to create an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind.
+> Note: there are different approaches to creating an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind.
Let's now focus on the first test case.
```ruby
-it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
- # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
+it 'replaces an existing label if it has the same key' do
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects (which we cover on section 8).
page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['foo::baz', :enter]
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['animal::dolphin', :enter]
page.find('#content-body').click
page.refresh
- scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
+ labels_block = page.find('.qa-labels-block')
- expect(scoped_label).to have_content('foo::baz')
- expect(scoped_label).not_to have_content('foo::bar')
- expect(page).to have_content('added foo::baz label and removed foo::bar')
+ expect(labels_block).to have_content('animal::dolphin')
+ expect(labels_block).not_to have_content('animal::fox')
+ expect(page).to have_content('added animal::dolphin label and removed animal::fox')
end
```
> Notice that the test itself is simple. The most challenging part is the creation of the application state, which will be covered later.
-> The exemplified test cases' MVC is not enough for the change to be submitted in an MR, but they help on building up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects](./qa/page/README.md) before they can be merged.
+> The exemplified test case's MVC is not enough for the change to be merged, but it helps to build up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects](./qa/qa/page/README.md) before they can be merged. This way we better separate the responsibilities, where the Page Objects encapsulate elements and methods that allow us to interact with pages, while the spec files describe the test cases in more business-related language.
Below are the steps that the test covers:
-1. The test finds the 'Edit' link for the labels and clicks on it
-2. Then it fills in the 'Assign labels' input field with the value 'foo::baz' and press enter
-3. Then it clicks in the content body to apply the label and refreshes the page
-4. Finally the expectation that the previous scoped label was removed and that the new one was added happens
+1. The test finds the 'Edit' link for the labels and clicks on it.
+2. Then it fills in the 'Assign labels' input field with the value 'animal::dolphin' and press enters.
+3. Then it clicks in the content body to apply the label and refreshes the page.
+4. Finally, the expectations check that the previous scoped label was removed and that the new one was added.
-Let's now see how the second test case would look like.
+Let's now see how the second test case would look.
```ruby
it 'keeps both scoped labels when adding a label with a different key' do
- # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects (which we cover on section 8).
page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['bar::bah', :enter]
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['plant::orchid', :enter]
page.find('#content-body').click
page.refresh
- scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
+ labels_block = page.find('.qa-labels-block')
- expect(scoped_labels.first).to have_content('bar::bah')
- expect(scoped_labels.last).to have_content('foo::ba')
- expect(page).to have_content('added bar::bah')
- expect(page).to have_content('added foo::ba')
+ expect(labels_block).to have_content('animal::fox')
+ expect(labels_block).to have_content('plant::orchid')
+ expect(page).to have_content('added animal::fox')
+ expect(page).to have_content('added plant::orchid')
end
```
-> Note that elements are always located using CSS selectors, and a good practice is to add test specific attribute:value for elements (this is called adding testability to the application and we will talk more about it later.)
+> Note that elements are always located using CSS selectors, and a good practice is to add test-specific selectors (this is called adding testability to the application and we will talk more about it later.) For example, the `labels_block` element uses the selector `.qa-labels-block`, which was added specifically for testing purposes.
Below are the steps that the test covers:
-1. The test finds the 'Edit' link for the labels and clicks on it
-2. Then it fills in the 'Assign labels' input field with the value 'bar::bah' and press enter
-3. Then it clicks in the content body to apply the label and refreshes the page
-4. Finally the expectation that the both scoped labels are present happens
+1. The test finds the 'Edit' link for the labels and clicks on it.
+2. Then it fills in the 'Assign labels' input field with the value 'plant::orchid' and press enters.
+3. Then it clicks in the content body to apply the label and refreshes the page.
+4. Finally, the expectations check that both scoped labels are present.
-> Similar to the previous test, this one is also very straight forward, but there is some code duplication. Let's address it.
+> Similar to the previous test, this one is also very straightforward, but there is some code duplication. Let's address it.
### 4. Extracting duplicated code
If we refactor the tests created on step 3 we could come up with something like this:
```ruby
-it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
- select_label_and_refresh 'foo::baz'
+before do
+ ...
- expect(page).to have_content('added foo::baz')
- expect(page).to have_content('and removed foo::bar')
+ @initial_label = 'animal::fox'
+ @new_label_same_scope = 'animal::dolphin'
+ @new_label_different_scope = 'plant::orchid'
- scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
+ ...
+end
+
+it 'replaces an existing label if it has the same key' do
+ select_label_and_refresh @new_label_same_scope
- expect(scoped_label).to have_content('foo::baz')
- expect(scoped_label).not_to have_content('foo::bar')
+ labels_block = page.find('.qa-labels-block')
+
+ expect(labels_block).to have_content(@new_label_same_scope)
+ expect(labels_block).not_to have_content(@initial_label)
+ expect(page).to have_content("added #{@new_label_same_scope}")
+ expect(page).to have_content("and removed #{@initial_label}")
end
it 'keeps both scoped label when adding a label with a different key' do
- select_label_and_refresh 'bar::bah'
-
- expect(page).to have_content('added bar::bah')
- expect(page).to have_content('added foo::ba')
+ select_label_and_refresh @new_label_different_scope
- scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
+ labels_block = page.find('.qa-labels-block')
- expect(scoped_labels.first).to have_content('bar::bah')
- expect(scoped_labels.last).to have_content('foo::ba')
+ expect(labels_blocks).to have_content(@new_label_different_scope)
+ expect(labels_blocks).to have_content(@initial_label)
+ expect(page).to have_content("added #{@new_label_different_scope}")
+ expect(page).to have_content("added #{@initial_label}")
end
def select_label_and_refresh(label)
@@ -188,72 +195,67 @@ def select_label_and_refresh(label)
end
```
-By creating a reusable `select_label_and_refresh` method, we remove the code duplication, and later we can move this method to a Page Object class that will be created for easier maintenance purposes.
+First, we remove the duplication of strings by defining the global variables `@initial_label`, `@new_label_same_scope` and `@new_label_different_scope` in the `before` block, and by using them in the expectations.
-> Notice that the reusable method is created in the bottom of the file. The reason for that is that reading the code should be similar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, while low level, or more specific information, is at the bottom.
+Then, by creating a reusable `select_label_and_refresh` method, we remove the code duplication of this action, and later we can move this method to a Page Object class that will be created for easier maintenance purposes.
-### 5. Tests' pre-conditions using resources and Page Objects
+> Notice that the reusable method is created at the bottom of the file. The reason for that is that reading the code should be similar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, while low level, or more specific information, is at the bottom (this helps readability).
-In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :all` and `before` blocks, together with resources and Page Objects.
+### 5. Tests' pre-conditions using resources and Page Objects
-#### `before :all`
+In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :context` and `before` blocks, together with resources and Page Objects.
-A pre-condition for the entire test suite is defined in the `before :all` block.
+#### `before :context`
-For our test suite example, some things that could happen before the entire test suite starts are:
+A pre-condition for the entire test suite is defined in the `before :context` block.
-- The user logging in;
-- A premium license already being set up;
-- A project being created with an issue and labels already setup.
+> For our test suite, due to the need of the tests being completely independent of each other, we won't use the `before :context` block. The `before :context` block would make the tests dependent on each other because the first test changes the label of the issue, and the second one depends on the `'animal::fox'` label being set.
-> In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions.
+> **Tip:** In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions.
#### `before`
-A pre-condition for each test case is defined in the `before` block.
+As the pre-conditions for our test suite, the things that needs to happen before each test starts are:
-For our test cases samples, what we need is that for every test the issue page is opened, and there is only one scoped label applied to it.
+- The user logging in;
+- A premium license already being set;
+- A project being created with an issue and labels already set;
+- The issue page being opened with only one scoped label applied to the it.
+
+> When running end-to-end tests as part of the GitLab's continuous integration process [a license is already set as an environment variable](https://gitlab.com/gitlab-org/gitlab-ee/blob/1a60d926740db10e3b5724713285780a4f470531/qa/qa/ee/strategy.rb#L20). For running tests locally you can set up such license by following the document [what tests can be run?](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-remote-grid-environment-variables), based on the [supported GitLab environment variables](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-gitlab-environment-variables).
#### Implementation
-In the following code we will focus on the test suite and the test cases' pre-conditions only:
+In the following code we will focus only on the test suite's pre-conditions:
```ruby
module QA
context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
- before :all do
- project = Resource::Project.fabricate_via_api! do |resource|
- resource.name = 'scoped-labels-project'
- end
+ describe 'Editing scoped labels on issues' do
+ before do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
- @foo_bar_scoped_label = 'foo::bar'
+ @initial_label = 'animal::fox'
+ @new_label_same_scope = 'animal::dolphin'
+ @new_label_different_scope = 'plant::orchid'
- @issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.project = project
+ issue = Resource::Issue.fabricate_via_api! do |issue|
issue.title = 'Issue to test the scoped labels'
- issue.labels = @foo_bar_scoped_label
+ issue.labels = @initial_label
end
- @labels = ['foo::baz', 'bar::bah']
- @labels.each do |label|
+ [@new_label_same_scope, @new_label_different_scope].each do |label|
Resource::Label.fabricate_via_api! do |l|
- l.project = project.id
+ l.project = issue.project.id
l.title = label
end
end
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ issue.visit!
end
- before do
- Page::Project::Issue::Show.perform do |issue_page|
- @issue.visit!
- end
- end
-
- it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
+ it 'replaces an existing label if it has the same key' do
...
end
@@ -269,9 +271,11 @@ module QA
end
```
-In the `before :all` block we create all the application state needed for the tests to run. We do that by fabricating resources via APIs (`project`, `@issue`, and `@labels`), by using the `Runtime::Browser.visit` method to go to the login page, and by performing a `sign_in_using_credentials` from the `Login` Page Object.
+In the `before` block we create all the application state needed for the tests to run. We do that by using the `Runtime::Browser.visit` method to go to the login page, by performing a `sign_in_using_credentials` from the `Login` Page Object, by fabricating resources via APIs (`issue`, and `Resource::Label`), and by using the `issue.visit!` to visit the issue page.
-> When creating the resources, notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `name` for the `project` resource; `project`, `title`, and `labels` for the `issue` resource; and `project`, and `title` for `label` resources.
+> A project is created in the background by creating the `issue` resource.
+
+> When [creating the resources](./qa/qa/resource/README.md), notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `title`, and `labels` for the `issue` resource; and `project` and `title` for the `label` resource.
> What's important to understand here is that by creating the application state mostly using the public APIs we save a lot of time in the test suite setup stage.
@@ -281,58 +285,33 @@ In the `before :all` block we create all the application state needed for the te
As already mentioned in the [best practices](./BEST_PRACTICES.md) document, end-to-end tests are very costly in terms of execution time, and it's our responsibility as software engineers to ensure that we optimize them as much as possible.
-> Differently than unit tests, that exercise every little piece of the application in isolation, usually having only one assertion per test, and being very fast to run, end-to-end tests can have more actions and assertions in a single test to help on speeding up the test's feedback since they are much slower when comparing to unit tests.
+> Note that end-to-end tests are slow to run and so they can have several actions and assertions in a single test, which helps us get feedback from the tests sooner. In comparison, unit tests are much faster to run and can exercise every little piece of the application in isolation, and so they usually have only one assertion per test.
Some improvements that we could make in our test suite to optimize its time to run are:
-1. Having a single test case (an `it` block) that exercise both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases.
-2. Moving all the pre-conditions to the `before` block since there will be only one `it` block.
-3. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method.
+1. Having a single test case (an `it` block) that exercises both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases.
+2. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method.
Let's look at a suggestion that addresses the above points, one by one:
```ruby
module QA
context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
+ describe 'Editing scoped labels on issues' do
before do
- project = Resource::Project.fabricate_via_api! do |resource|
- resource.name = 'scoped-labels-project'
- end
-
- @foo_bar_scoped_label = 'foo::bar'
-
- @issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.project = project
- issue.title = 'Issue to test the scoped labels'
- issue.labels = @foo_bar_scoped_label
- end
-
- @labels = ['foo::baz', 'bar::bah']
- @labels.each do |label|
- Resource::Label.fabricate_via_api! do |l|
- l.project = project.id
- l.title = label
- end
- end
-
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
- Page::Project::Issue::Show.perform do |issue_page|
- @issue.visit!
- end
+ ...
end
- it 'correctly applies the scoped labels depending if they are from the same or a different scope' do
- select_labels_and_refresh @labels
+ it 'correctly applies scoped labels depending on if they are from the same or a different scope' do
+ select_labels_and_refresh [@new_label_same_scope, @new_label_different_scope]
- scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
+ labels_block = page.all('.qa-labels-block')
- expect(page).to have_content("added #{@foo_bar_scoped_label}")
- expect(page).to have_content("added #{@labels[1]} #{@labels[0]} labels and removed #{@foo_bar_scoped_label}")
- expect(scoped_labels.count).to eq(2)
- expect(scoped_labels.first).to have_content(@labels[1])
- expect(scoped_labels.last).to have_content(@labels[0])
+ expect(labels_block).to have_content(@new_label_same_scope)
+ expect(labels_block).to have_content(@new_label_different_scope)
+ expect(labels_block).not_to have_content(@initial_label)
+ expect(page).to have_content("added #{@initial_label}")
+ expect(page).to have_content("added #{@new_label_same_scope} #{@new_label_different_scope} labels and removed #{@initial_label}")
end
def select_labels_and_refresh(labels)
@@ -348,20 +327,19 @@ module QA
end
```
-As you can see, now all the pre-conditions from the `before :all` block were moved to the `before` block, addressing point 2.
-
To address point 1, we changed the test implementation from two `it` blocks into a single one that exercises both scenarios. Now the new test description is: `'correctly applies the scoped labels depending if they are from the same or a different scope'`. It's a long description, but it describes well what the test does.
> Notice that the implementation of the new and unique `it` block had to change a little bit. Below we describe in details what it does.
-1. At the same time, it selects two scoped labels, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope.
-2. It runs the assertions that the labels where correctly added and removed; that only two labels are applied; and that those are the correct ones, and that they are shown in the right order.
-
-Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.)
+1. It selects two scoped labels simultaneously, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope.
+2. It asserts that the correct labels are visible in the `labels_block`, and that the labels were correctly added and removed;
+3. Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.)
### 7. Resources
-You can think of resources as anything that can be created on GitLab CE or EE, either through the GUI, the API, or the CLI.
+**Note:** When writing this document, some code that is now merged to master was not implemented yet, but we left them here for the readers to understand the whole process of end-to-end test creation.
+
+You can think of [resources](qa/qa/resource/README.md) as anything that can be created on GitLab CE or EE, either through the GUI, the API, or the CLI.
With that in mind, resources can be a project, an epic, an issue, a label, a commit, etc.
@@ -369,21 +347,11 @@ As you saw in the tests' pre-conditions and the optimization sections, we're alr
> We could be using the `fabricate!` method instead, which would use the `fabricate_via_api!` method if it exists, and fallback to GUI fabrication otherwise, but we recommend being explicit to make it clear what the test does. Also, we always recommend fabricating resources via API since this makes tests faster and more reliable.
-For our test suite example, the [project resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/project.rb#L55) already had a `fabricate_via_api!` method available, while other resources don't have it, so we will have to create them, like for the issue and label resources. Also, we will have to make a small change in the project resource to expose its `id` attribute so that we can refer to it when fabricating the issue.
+For our test suite example, the resources that we need to create don't have the necessary code for the `fabricate_via_api!` method to correctly work (e.g., the issue and label resources), so we will have to create them.
#### Implementation
-Following we describe the changes needed in every of the before-mentioned resource files.
-
-**Project resource**
-
-Let's start with the smallest change.
-
-In the [project resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/project.rb), let's expose its `id` attribute.
-
-Add the following `attribute :id` right below the [`attribute :description`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/project.rb#L11).
-
-> This line is needed to allow for issues and labels to be automatically added to a project when fabricating them via API.
+In the following we describe the changes needed in each of the resource files mentioned above.
**Issue resource**
@@ -467,16 +435,148 @@ By defining the `api_post_body` method, we we allow for the [`ApiFabricator.api_
### 8. Page Objects
-> Page Objects are auto-loaded in the `qa/qa.rb` file and available in all the test files (`*_spec.rb`).
+Page Objects are used in end-to-end tests for maintenance reasons, where a page's elements and methods are defined to be reused in any test.
+
+> Page Objects are auto-loaded in the `qa/qa.rb` file and available in all the test files (`*_spec.rb`).
+
+Take a look at [this document for a detailed explanation about Page Objects](./qa/page/README.md).
+
+Now, let's go back to our example.
+
+As you may have noticed, we are defining elements with CSS selectors and the `select_labels_and_refresh` method directly in the test file, and this is an anti-pattern since we need to better separate the responsibilities.
+
+To address this issue, we will move the implementation to Page Objects, and the test suite will only focus on the business rules that we are testing.
+
+#### Updates in the test file
+
+As in a test-driven development approach, let's start changing the test file even before the Page Object implementation is in place.
+
+Replace the code of the `it` block in the test file by the following:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ before do
+ ...
+ end
+
+ it 'correctly applies scoped labels depending on if they are from the same or a different scope' do
+ Page::Project::Issue::Show.perform do |issue_page|
+ issue_page.select_labels_and_refresh [@new_label_same_scope, @new_label_different_scope]
+
+ expect(page).to have_content("added #{@initial_label}")
+ expect(page).to have_content("added #{@new_label_same_scope} #{@new_label_different_scope} labels and removed #{@initial_label}")
+ expect(issue_page.text_of_labels_block).to have_content(@new_label_same_scope)
+ expect(issue_page.text_of_labels_block).to have_content(@new_label_different_scope)
+ expect(issue_page.text_of_labels_block).not_to have_content(@initial_label)
+ end
+ end
+ end
+ end
+end
+```
+
+Notice that `select_labels_and_refresh` is now a method from the issue Page Object (which is not yet implemented), and that we verify the labels' text by using `text_of_labels_block`, instead of via the `labels_block` element. The `text_of_labels_block` method will also be implemented in the issue Page Object.
+
+Let's now update the Issue Page Object.
+
+#### Updates in the Issue Page Object
+
+> Page Objects are located in the `qa/qa/page/` directory, and its sub-directories.
+
+The file we will have to change is the [Issue Page Object](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/page/project/issue/show.rb).
-Page Objects are used in end-to-end tests for maintenance reasons, where page's elements and methods are defined to be reused in any test.
+First, add the following code right below the definition of an already implemented view:
-Take a look at [this document that specifically details the usage of Page Objects](./qa/page/README.md).
+```ruby
+view 'app/views/shared/issuable/_sidebar.html.haml' do
+ element :labels_block
+ element :edit_link_labels
+ element :dropdown_menu_labels
+end
+
+view 'app/helpers/dropdowns_helper.rb' do
+ element :dropdown_input_field
+end
+```
+
+Similarly to what we did before, let's first change the Page Object even without the elements being defined in the view (`_sidebar.html.haml`) and the `dropdowns_helper.rb` files, and later we will update them by adding the appropriate CSS selectors.
+
+Now, let's implement the methods `select_labels_and_refresh` and `text_of_labels_block`.
+
+Somewhere between the definition of the views and the private methods, add the following snippet of code:
+
+```ruby
+def select_labels_and_refresh(labels)
+ click_element(:edit_link_labels)
+ labels.each do |label|
+ within_element(:dropdown_menu_labels, text: label) do
+ send_keys_to_element(:dropdown_input_field, [label, :enter])
+ end
+ end
+ click_body
+ labels.each do |label|
+ has_element?(:labels_block, text: label)
+ end
+ refresh
+end
+
+def text_of_labels_block
+ find_element(:labels_block)
+end
+```
+
+##### Details of `select_labels_and_refresh`
+
+Notice that we have not only moved the `select_labels_and_refresh` method, but we have also changed its implementation to:
+1. Click the `:edit_link_labels` element previously defined, instead of using `find('.block.labels .edit-link').click`
+2. Use `within_element(:dropdown_menu_labels, text: label)`, and inside of it, we call `send_keys_to_element(:dropdown_input_field, [label, :enter])`, which is a method that we will implement in the `QA::Page::Base` class to replace `find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]`
+3. Use `click_body` after iterating on each label, instead of using `find('#content-body').click`
+4. Iterate on every label again, and then we use `has_element?(:labels_block, text: label)` after clicking the page body (which applies the labels), and before refreshing the page, to avoid test flakiness due to refreshing too fast.
+
+##### Details of `text_of_labels_block`
+
+The `text_of_labels_block` method is a simple method that returns the `:labels_block` element (`find_element(:labels_block)`).
+
+#### Updates in the view (*.html.haml) and `dropdowns_helper.rb` files
+
+Now let's change the view and the `dropdowns_helper` files to add the selectors that relate to the Page Object.
+
+In the [app/views/shared/issuable/_sidebar.html.haml](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/app/views/shared/issuable/_sidebar.html.haml) file, on [line 105 ](https://gitlab.com/gitlab-org/gitlab-ee/blob/84043fa72ca7f83ae9cde48ad670e6d5d16501a3/app/views/shared/issuable/_sidebar.html.haml#L105), add an extra class `qa-edit-link-labels`.
+
+The code should look like this: `= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right'`.
+
+In the same file, on [line 121](https://gitlab.com/gitlab-org/gitlab-ee/blob/84043fa72ca7f83ae9cde48ad670e6d5d16501a3/app/views/shared/issuable/_sidebar.html.haml#L121), add an extra class `.qa-dropdown-menu-labels`.
+
+The code should look like this: `.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.qa-dropdown-menu-labels.dropdown-menu-selectable`.
+
+In the [`dropdowns_helper.rb`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/app/helpers/dropdowns_helper.rb) file, on [line 94](https://gitlab.com/gitlab-org/gitlab-ee/blob/99e51a374f2c20bee0989cac802e4b5621f72714/app/helpers/dropdowns_helper.rb#L94), add an extra class `qa-dropdown-input-field`.
+
+The code should look like this: `filter_output = search_field_tag search_id, nil, class: "dropdown-input-field qa-dropdown-input-field", placeholder: placeholder, autocomplete: 'off'`.
+
+> Classes starting with `qa-` are used for testing purposes only, and by defining such classes in the elements we add **testability** in the application.
+
+> When defining a class like `qa-labels-block`, it is transformed into `:labels_block` for usage in the Page Objects. So, `qa-edit-link-labels` is tranformed into `:edit_link_labels`, `qa-dropdown-menu-labels` is transformed into `:dropdown_menu_labels`, and `qa-dropdown-input-field` is transformed into `:dropdown_input_field`. Also, we use a [sanity test](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/page#how-did-we-solve-fragile-tests-problem) to check that defined elements have their respective `qa-` selectors in the specified views.
+
+> We did not define the `qa-labels-block` class in the `app/views/shared/issuable/_sidebar.html.haml` file because it was already there to be used.
+
+#### Updates in the `QA::Page::Base` class
+
+The last thing that we have to do is to update `QA::Page::Base` class to add the `send_keys_to_element` method on it.
+
+Add the following snippet of code somewhere where class methods are defined:
+
+```ruby
+def send_keys_to_element(name, keys)
+ find_element(name).send_keys(keys)
+end
+```
-Now, let's go back to our examples.
+This method receives an element (`name`) and the `keys` that it will send to that element, and the keys are an array that can receive strings, or "special" keys, like `:enter`.
-...
+As you might remember, in the Issue Page Object we call this method like this: `send_keys_to_element(:dropdown_input_field, [label, :enter])`.
-#### Adding testability
+___
-TBD.
+With that, you should be able to start writing end-to-end tests yourself. *Congratulations!*
diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb
index 762eacdab15..480fc7d78cb 100644
--- a/qa/qa/page/project/branches/show.rb
+++ b/qa/qa/page/project/branches/show.rb
@@ -28,9 +28,11 @@ module QA
finished_loading?
end
- def has_no_branch?(branch_name)
- within_element(:all_branches) do
- has_no_element?(:branch_name, text: branch_name, wait: Support::Waiter::DEFAULT_MAX_WAIT_TIME)
+ def has_no_branch?(branch_name, reload: false)
+ wait(reload: reload) do
+ within_element(:all_branches) do
+ has_no_element?(:branch_name, text: branch_name)
+ end
end
end
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index 523d92c7ef3..283fc6cdbcb 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -15,6 +15,38 @@ module QA
def_delegators :evaluator, :attribute
+ def self.fabricate!(*args, &prepare_block)
+ fabricate_via_api!(*args, &prepare_block)
+ rescue NotImplementedError
+ fabricate_via_browser_ui!(*args, &prepare_block)
+ end
+
+ def self.fabricate_via_browser_ui!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
+
+ current_url
+ end
+ end
+
+ def self.fabricate_via_api!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ raise NotImplementedError unless resource.api_support?
+
+ resource.eager_load_api_client!
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
+ end
+ end
+
def fabricate!(*_args)
raise NotImplementedError
end
@@ -55,38 +87,6 @@ module QA
QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
end
- def self.fabricate!(*args, &prepare_block)
- fabricate_via_api!(*args, &prepare_block)
- rescue NotImplementedError
- fabricate_via_browser_ui!(*args, &prepare_block)
- end
-
- def self.fabricate_via_browser_ui!(*args, &prepare_block)
- options = args.extract_options!
- resource = options.fetch(:resource) { new }
- parents = options.fetch(:parents) { [] }
-
- do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
-
- current_url
- end
- end
-
- def self.fabricate_via_api!(*args, &prepare_block)
- options = args.extract_options!
- resource = options.fetch(:resource) { new }
- parents = options.fetch(:parents) { [] }
-
- raise NotImplementedError unless resource.api_support?
-
- resource.eager_load_api_client!
-
- do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
- end
- end
-
def self.do_fabricate!(resource:, prepare_block:, parents: [])
prepare_block.call(resource) if prepare_block
diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb
index 57e82ac19ad..ca74654bf90 100644
--- a/qa/qa/resource/file.rb
+++ b/qa/qa/resource/file.rb
@@ -3,9 +3,12 @@
module QA
module Resource
class File < Base
- attr_accessor :name,
+ attr_accessor :author_email,
+ :author_name,
+ :branch,
:content,
- :commit_message
+ :commit_message,
+ :name
attribute :project do
Project.fabricate! do |resource|
@@ -31,6 +34,30 @@ module QA
page.commit_changes
end
end
+
+ def resource_web_url(resource)
+ super
+ rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
+ end
+
+ def api_get_path
+ "/projects/#{CGI.escape(project.path_with_namespace)}/repository/files/#{CGI.escape(@name)}"
+ end
+
+ def api_post_path
+ api_get_path
+ end
+
+ def api_post_body
+ {
+ branch: @branch || "master",
+ author_email: @author_email || Runtime::User.default_email,
+ author_name: @author_name || Runtime::User.username,
+ content: content,
+ commit_message: commit_message
+ }
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index cf6c24fa873..37a784248d4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -84,7 +84,7 @@ module QA
page.refresh
Page::Project::Branches::Show.perform do |branches_view|
- expect(branches_view).to have_no_branch(second_branch)
+ expect(branches_view).to have_no_branch(second_branch, reload: true)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
index 46346d1b984..d345fbfe995 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
@@ -12,7 +12,7 @@ module QA
file_content = 'QA Test - File content'
commit_message_for_create = 'QA Test - Create new file'
- Resource::File.fabricate! do |file|
+ Resource::File.fabricate_via_browser_ui! do |file|
file.name = file_name
file.content = file_content
file.commit_message = commit_message_for_create
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
index b7400cdca97..680c5e21fa4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
@@ -3,13 +3,16 @@
module QA
context 'Create' do
# failure reported: https://gitlab.com/gitlab-org/quality/nightly/issues/42
- # also failing in staging until the fix is picked into the next release:
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24533
describe 'Commit data', :quarantine do
before(:context) do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
+ # Get the user's details to confirm they're included in the email patch
+ @user = Resource::User.fabricate_via_api! do |user|
+ user.username = Runtime::User.username
+ end
+
project_push = Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
@@ -21,12 +24,13 @@ module QA
# add second file to repo to enable diff from initial commit
@commit_message = 'Add second file'
- Page::Project::Show.perform(&:create_new_file!)
- Page::File::Form.perform do |f|
- f.add_name('second')
- f.add_content('second file content')
- f.add_commit_message(@commit_message)
- f.commit_changes
+ Resource::File.fabricate_via_api! do |file|
+ file.project = @project
+ file.name = 'second'
+ file.content = 'second file content'
+ file.commit_message = @commit_message
+ file.author_name = @user.name
+ file.author_email = @user.public_email
end
end
@@ -42,15 +46,11 @@ module QA
end
it 'user views raw email patch' do
- user = Resource::User.fabricate_via_api! do |user|
- user.username = Runtime::User.username
- end
-
view_commit
Page::Project::Commit::Show.perform(&:select_email_patches)
- expect(page).to have_content("From: #{user.name} <#{user.public_email}>")
+ expect(page).to have_content("From: #{@user.name} <#{@user.public_email}>")
expect(page).to have_content('Subject: [PATCH] Add second file')
expect(page).to have_content('diff --git a/second b/second')
end
diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb
index 95d1fc5b57a..b5323a1c76d 100644
--- a/spec/features/admin/admin_sees_project_statistics_spec.rb
+++ b/spec/features/admin/admin_sees_project_statistics_spec.rb
@@ -15,7 +15,7 @@ describe "Admin > Admin sees project statistics" do
let(:project) { create(:project, :repository) }
it "shows project statistics" do
- expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes build artifacts, 0 Bytes LFS)")
+ expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes wikis, 0 Bytes build artifacts, 0 Bytes LFS)")
end
end
diff --git a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
index 5f9f13d591d..a2a7d0ee91e 100644
--- a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
+++ b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
@@ -3,6 +3,7 @@
exports[`MR Popover loaded state matches the snapshot 1`] = `
<glpopover-stub
boundary="viewport"
+ cssclasses=""
placement="top"
show=""
target=""
@@ -61,6 +62,7 @@ exports[`MR Popover loaded state matches the snapshot 1`] = `
exports[`MR Popover shows skeleton-loader while apollo is loading 1`] = `
<glpopover-stub
boundary="viewport"
+ cssclasses=""
placement="top"
show=""
target=""
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 50c74a7c2f9..62c00964524 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -26,11 +26,12 @@ describe StorageHelper do
namespace: namespace,
statistics: build(:project_statistics,
repository_size: 10.kilobytes,
+ wiki_size: 10.bytes,
lfs_objects_size: 20.gigabytes,
build_artifacts_size: 30.megabytes))
end
- let(:message) { '10 KB repositories, 30 MB build artifacts, 20 GB LFS' }
+ let(:message) { '10 KB repositories, 10 Bytes wikis, 30 MB build artifacts, 20 GB LFS' }
it 'works on ProjectStatistics' do
expect(helper.storage_counters_details(project.statistics)).to eq(message)
diff --git a/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
new file mode 100644
index 00000000000..d494ce68c5b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190527194900_schedule_calculate_wiki_sizes.rb')
+
+describe ScheduleCalculateWikiSizes, :migration, :sidekiq do
+ let(:migration_class) { Gitlab::BackgroundMigration::CalculateWikiSizes }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:project_statistics) { table(:project_statistics) }
+
+ context 'when missing wiki sizes exist' do
+ before do
+ namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration')
+ projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1)
+ projects.create!(id: 2, name: 'wiki-project-2', path: 'wiki-project-2', namespace_id: 1)
+ projects.create!(id: 3, name: 'wiki-project-3', path: 'wiki-project-3', namespace_id: 1)
+ project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000)
+ project_statistics.create!(id: 2, project_id: 2, namespace_id: 1, wiki_size: nil)
+ project_statistics.create!(id: 3, project_id: 3, namespace_id: 1, wiki_size: nil)
+ end
+
+ it 'schedules a background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(5.minutes, 2, 3)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 1
+ end
+ end
+ end
+
+ it 'calculates missing wiki sizes' do
+ expect(project_statistics.find_by(id: 2).wiki_size).to be_nil
+ expect(project_statistics.find_by(id: 3).wiki_size).to be_nil
+
+ migrate!
+
+ expect(project_statistics.find_by(id: 2).wiki_size).not_to be_nil
+ expect(project_statistics.find_by(id: 3).wiki_size).not_to be_nil
+ end
+ end
+
+ context 'when missing wiki sizes do not exist' do
+ before do
+ namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration')
+ projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1)
+ project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000)
+ end
+
+ it 'does not schedule a background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index bfde367c47f..d80183af33e 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -147,6 +147,7 @@ describe Namespace do
namespace: namespace,
statistics: build(:project_statistics,
repository_size: 101,
+ wiki_size: 505,
lfs_objects_size: 202,
build_artifacts_size: 303,
packages_size: 404))
@@ -157,6 +158,7 @@ describe Namespace do
namespace: namespace,
statistics: build(:project_statistics,
repository_size: 10,
+ wiki_size: 50,
lfs_objects_size: 20,
build_artifacts_size: 30,
packages_size: 40))
@@ -167,8 +169,9 @@ describe Namespace do
project2
statistics = described_class.with_statistics.find(namespace.id)
- expect(statistics.storage_size).to eq 1110
+ expect(statistics.storage_size).to eq 1665
expect(statistics.repository_size).to eq 111
+ expect(statistics.wiki_size).to eq 555
expect(statistics.lfs_objects_size).to eq 222
expect(statistics.build_artifacts_size).to eq 333
expect(statistics.packages_size).to eq 444
@@ -179,6 +182,7 @@ describe Namespace do
expect(statistics.storage_size).to eq 0
expect(statistics.repository_size).to eq 0
+ expect(statistics.wiki_size).to eq 0
expect(statistics.lfs_objects_size).to eq 0
expect(statistics.build_artifacts_size).to eq 0
expect(statistics.packages_size).to eq 0
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 738398a06f9..f985c114d4b 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -16,16 +16,18 @@ describe ProjectStatistics do
statistics.update!(
commit_count: 8.exabytes - 1,
repository_size: 2.exabytes,
+ wiki_size: 1.exabytes,
lfs_objects_size: 2.exabytes,
- build_artifacts_size: 4.exabytes - 1
+ build_artifacts_size: 3.exabytes - 1
)
statistics.reload
expect(statistics.commit_count).to eq(8.exabytes - 1)
expect(statistics.repository_size).to eq(2.exabytes)
+ expect(statistics.wiki_size).to eq(1.exabytes)
expect(statistics.lfs_objects_size).to eq(2.exabytes)
- expect(statistics.build_artifacts_size).to eq(4.exabytes - 1)
+ expect(statistics.build_artifacts_size).to eq(3.exabytes - 1)
expect(statistics.storage_size).to eq(8.exabytes - 1)
end
end
@@ -33,6 +35,7 @@ describe ProjectStatistics do
describe '#total_repository_size' do
it "sums repository and LFS object size" do
statistics.repository_size = 2
+ statistics.wiki_size = 6
statistics.lfs_objects_size = 3
statistics.build_artifacts_size = 4
@@ -40,10 +43,17 @@ describe ProjectStatistics do
end
end
+ describe '#wiki_size' do
+ it "is initialized with not null value" do
+ expect(statistics.wiki_size).to eq 0
+ end
+ end
+
describe '#refresh!' do
before do
allow(statistics).to receive(:update_commit_count)
allow(statistics).to receive(:update_repository_size)
+ allow(statistics).to receive(:update_wiki_size)
allow(statistics).to receive(:update_lfs_objects_size)
allow(statistics).to receive(:update_storage_size)
end
@@ -56,6 +66,7 @@ describe ProjectStatistics do
it "sums all counters" do
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
expect(statistics).to have_received(:update_lfs_objects_size)
end
end
@@ -69,6 +80,45 @@ describe ProjectStatistics do
expect(statistics).to have_received(:update_lfs_objects_size)
expect(statistics).not_to have_received(:update_commit_count)
expect(statistics).not_to have_received(:update_repository_size)
+ expect(statistics).not_to have_received(:update_wiki_size)
+ end
+ end
+
+ context 'without repositories' do
+ it 'does not crash' do
+ expect(project.repository.exists?).to be_falsey
+ expect(project.wiki.repository.exists?).to be_falsey
+
+ statistics.refresh!
+
+ expect(statistics).to have_received(:update_commit_count)
+ expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
+ expect(statistics.repository_size).to eq(0)
+ expect(statistics.commit_count).to eq(0)
+ expect(statistics.wiki_size).to eq(0)
+ end
+ end
+
+ context 'with deleted repositories' do
+ let(:project) { create(:project, :repository, :wiki_repo) }
+
+ before do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.rm_rf(project.repository.path)
+ FileUtils.rm_rf(project.wiki.repository.path)
+ end
+ end
+
+ it 'does not crash' do
+ statistics.refresh!
+
+ expect(statistics).to have_received(:update_commit_count)
+ expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
+ expect(statistics.repository_size).to eq(0)
+ expect(statistics.commit_count).to eq(0)
+ expect(statistics.wiki_size).to eq(0)
end
end
end
@@ -95,6 +145,17 @@ describe ProjectStatistics do
end
end
+ describe '#update_wiki_size' do
+ before do
+ allow(project.wiki.repository).to receive(:size).and_return(34)
+ statistics.update_wiki_size
+ end
+
+ it "stores the size of the wiki" do
+ expect(statistics.wiki_size).to eq 34.megabytes
+ end
+ end
+
describe '#update_lfs_objects_size' do
let!(:lfs_object1) { create(:lfs_object, size: 23.megabytes) }
let!(:lfs_object2) { create(:lfs_object, size: 34.megabytes) }
@@ -114,12 +175,13 @@ describe ProjectStatistics do
it "sums all storage counters" do
statistics.update!(
repository_size: 2,
+ wiki_size: 4,
lfs_objects_size: 3
)
statistics.reload
- expect(statistics.storage_size).to eq 5
+ expect(statistics.storage_size).to eq 9
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 7176bc23e34..c41408fba65 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -90,8 +90,9 @@ describe API::Groups do
it "includes statistics if requested" do
attributes = {
- storage_size: 702,
+ storage_size: 1158,
repository_size: 123,
+ wiki_size: 456,
lfs_objects_size: 234,
build_artifacts_size: 345
}.stringify_keys
diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
index 5000ea58e5f..8534853fbc7 100644
--- a/spec/services/projects/update_statistics_service_spec.rb
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -17,19 +17,9 @@ describe Projects::UpdateStatisticsService do
end
end
- context 'with an existing project without a repository' do
+ context 'with an existing project' do
let(:project) { create(:project) }
- it 'does nothing' do
- expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!)
-
- service.execute
- end
- end
-
- context 'with an existing project with a repository' do
- let(:project) { create(:project, :repository) }
-
it 'refreshes the project statistics' do
expect_any_instance_of(ProjectStatistics).to receive(:refresh!)
.with(only: statistics.map(&:to_sym))
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 3c40269adc7..51afb076da1 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -25,10 +25,11 @@ describe ProjectCacheWorker do
end
context 'with an existing project without a repository' do
- it 'does nothing' do
+ it 'updates statistics but does not refresh the method cashes' do
allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
- expect(worker).not_to receive(:update_statistics)
+ expect(worker).to receive(:update_statistics)
+ expect_any_instance_of(Repository).not_to receive(:refresh_method_caches)
worker.perform(project.id)
end
diff --git a/public/visual-review-toolbar.js b/vendor/assets/javascripts/visual_review_toolbar.js
index 6a0fdb29cc2..12a3a4c9672 100644
--- a/public/visual-review-toolbar.js
+++ b/vendor/assets/javascripts/visual_review_toolbar.js
@@ -2,260 +2,11 @@
/////////////////// STYLES ////////////////////
///////////////////////////////////////////////
+// this style must be applied inline
const buttonClearStyles = `
-webkit-appearance: none;
`;
-const buttonBaseStyles = `
- cursor: pointer;
- transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear;
-`;
-
-const buttonSuccessActiveStyles = `
- background-color: #168f48;
- border-color: #12753a;
- color: #fff;
-`;
-
-const buttonSuccessHoverStyles = `
- color: #fff;
- background-color: #137e3f;
- border-color: #127339;
-`;
-
-const buttonSuccessStyles = `
- ${buttonBaseStyles}
- background-color: #1aaa55;
- border: 1px solid #168f48;
- color: #fff;
-`;
-
-const buttonSecondaryStyles = `
- ${buttonBaseStyles}
- background: none #fff;
- margin: 0 .5rem;
- border: 1px solid #e3e3e3;
-`;
-
-const buttonSecondaryActiveStyles = `
- color: #2e2e2e;
- background-color: #e1e1e1;
- border-color: #dadada;
-`;
-
-const buttonSecondaryHoverStyles = `
- background-color: #f0f0f0;
- border-color: #e3e3e3;
- color: #2e2e2e;
-`;
-
-const buttonWideStyles = `
- width: 100%;
-`;
-
-const buttonWrapperStyles = `
- margin-top: 1rem;
- display: flex;
- align-items: baseline;
- justify-content: flex-end;
-`;
-
-const collapseStyles = `
- ${buttonBaseStyles}
- width: 2.4rem;
- height: 2.2rem;
- margin-left: 1rem;
- padding: .5rem;
-`;
-
-const collapseClosedStyles = `
- ${collapseStyles}
- align-self: center;
-`;
-
-const collapseOpenStyles = `
- ${collapseStyles}
-`;
-
-const checkboxLabelStyles = `
- padding: 0 .2rem;
-`;
-
-const checkboxWrapperStyles = `
- display: flex;
- align-items: baseline;
-`;
-
-const formStyles = `
- display: flex;
- flex-direction: column;
- width: 100%
-`;
-
-const labelStyles = `
- font-weight: 600;
- display: inline-block;
- width: 100%;
-`;
-
-const linkStyles = `
- color: #1b69b6;
- text-decoration: none;
- background-color: transparent;
- background-image: none;
-`;
-
-const messageStyles = `
- padding: .25rem 0;
- margin: 0;
- line-height: 1.2rem;
-`;
-
-const metadataNoteStyles = `
- font-size: .7rem;
- line-height: 1rem;
- color: #666;
- margin-bottom: 0;
-`;
-
-const inputStyles = `
- width: 100%;
- border: 1px solid #dfdfdf;
- border-radius: 4px;
- padding: .1rem .2rem;
- min-height: 2rem;
- max-width: 17rem;
-`;
-
-const svgInnerStyles = `
- pointer-events: none;
-`;
-
-const wrapperClosedStyles = `
- max-width: 3.4rem;
- max-height: 3.4rem;
-`;
-
-const wrapperOpenStyles = `
- max-width: 22rem;
- max-height: 22rem;
-`;
-
-const wrapperStyles = `
- max-width: 22rem;
- max-height: 22rem;
- overflow: scroll;
- position: fixed;
- bottom: 1rem;
- right: 1rem;
- display: flex;
- flex-direction: row-reverse;
- padding: 1rem;
- background-color: #fff;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
- 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
- 'Noto Color Emoji';
- font-size: .8rem;
- font-weight: 400;
- color: #2e2e2e;
-`;
-
-const gitlabStyles = `
- #gitlab-collapse > * {
- ${svgInnerStyles}
- }
-
- #gitlab-form-wrapper {
- ${formStyles}
- }
-
- #gitlab-review-container {
- ${wrapperStyles}
- }
-
- .gitlab-open-wrapper {
- ${wrapperOpenStyles}
- }
-
- .gitlab-closed-wrapper {
- ${wrapperClosedStyles}
- }
-
- .gitlab-button-secondary {
- ${buttonSecondaryStyles}
- }
-
- .gitlab-button-secondary:hover {
- ${buttonSecondaryHoverStyles}
- }
-
- .gitlab-button-secondary:active {
- ${buttonSecondaryActiveStyles}
- }
-
- .gitlab-button-success:hover {
- ${buttonSuccessHoverStyles}
- }
-
- .gitlab-button-success:active {
- ${buttonSuccessActiveStyles}
- }
-
- .gitlab-button-success {
- ${buttonSuccessStyles}
- }
-
- .gitlab-button-wide {
- ${buttonWideStyles}
- }
-
- .gitlab-button-wrapper {
- ${buttonWrapperStyles}
- }
-
- .gitlab-collapse-closed {
- ${collapseClosedStyles}
- }
-
- .gitlab-collapse-open {
- ${collapseOpenStyles}
- }
-
- .gitlab-checkbox-label {
- ${checkboxLabelStyles}
- }
-
- .gitlab-checkbox-wrapper {
- ${checkboxWrapperStyles}
- }
-
- .gitlab-label {
- ${labelStyles}
- }
-
- .gitlab-link {
- ${linkStyles}
- }
-
- .gitlab-message {
- ${messageStyles}
- }
-
- .gitlab-metadata-note {
- ${metadataNoteStyles}
- }
-
- .gitlab-input {
- ${inputStyles}
- }
-`;
-
-function addStylesheet() {
- const styleEl = document.createElement('style');
- styleEl.insertAdjacentHTML('beforeend', gitlabStyles);
- document.head.appendChild(styleEl);
-}
-
///////////////////////////////////////////////
/////////////////// STATE ////////////////////
///////////////////////////////////////////////
@@ -275,25 +26,24 @@ const comment = `
<p class='gitlab-metadata-note'>Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
</div>
<div class='gitlab-button-wrapper''>
- <button class='gitlab-button-secondary' style='${buttonClearStyles}' type='button' id='gitlab-logout-button'> Logout </button>
- <button class='gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-comment-button'> Send feedback </button>
+ <button class='gitlab-button gitlab-button-secondary' style='${buttonClearStyles}' type='button' id='gitlab-logout-button'> Logout </button>
+ <button class='gitlab-button gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-comment-button'> Send feedback </button>
</div>
`;
const commentIcon = `
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg>
-`
+`;
const compressIcon = `
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg>
`;
const collapseButton = `
- <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-collapse-open gitlab-button-secondary'>${compressIcon}</button>
+ <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-button gitlab-button-secondary gitlab-collapse gitlab-collapse-open'>${compressIcon}</button>
`;
-
-const form = (content) => `
+const form = content => `
<div id='gitlab-form-wrapper'>
${content}
</div>
@@ -310,7 +60,7 @@ const login = `
<label for="remember_token" class='gitlab-checkbox-label'>Remember me</label>
</div>
<div class='gitlab-button-wrapper'>
- <button class='gitlab-button-wide gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-login'> Submit </button>
+ <button class='gitlab-button-wide gitlab-button gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-login'> Submit </button>
</div>
`;
@@ -319,25 +69,25 @@ const login = `
///////////////////////////////////////////////
// from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator
-function getBrowserId (sUsrAg) {
- var aKeys = ["MSIE", "Edge", "Firefox", "Safari", "Chrome", "Opera"],
- nIdx = aKeys.length - 1;
+function getBrowserId(sUsrAg) {
+ var aKeys = ['MSIE', 'Edge', 'Firefox', 'Safari', 'Chrome', 'Opera'],
+ nIdx = aKeys.length - 1;
for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx--);
return aKeys[nIdx];
}
-function addCommentForm () {
+function addCommentForm() {
const formWrapper = document.getElementById('gitlab-form-wrapper');
formWrapper.innerHTML = comment;
}
-function addLoginForm () {
+function addLoginForm() {
const formWrapper = document.getElementById('gitlab-form-wrapper');
formWrapper.innerHTML = login;
}
-function authorizeUser () {
+function authorizeUser() {
// Clear any old errors
clearNote('gitlab-token');
@@ -357,13 +107,12 @@ function authorizeUser () {
return;
}
-function authSuccess (token) {
+function authSuccess(token) {
data.token = token;
addCommentForm();
}
-
-function clearNote (inputId) {
+function clearNote(inputId) {
const note = document.getElementById('gitlab-validation-note');
note.innerText = '';
note.style.color = '';
@@ -374,7 +123,7 @@ function clearNote (inputId) {
}
}
-function confirmAndClear (mergeRequestId) {
+function confirmAndClear(mergeRequestId) {
const commentButton = document.getElementById('gitlab-comment-button');
const note = document.getElementById('gitlab-validation-note');
@@ -384,7 +133,7 @@ function confirmAndClear (mergeRequestId) {
setTimeout(resetCommentButton, 1000);
}
-function getInitialState () {
+function getInitialState() {
const { localStorage } = window;
try {
@@ -396,22 +145,21 @@ function getInitialState () {
}
return login;
-
} catch (err) {
return login;
}
}
-function getProjectDetails () {
- const { innerWidth,
- innerHeight,
- location: { href },
- navigator: {
- platform, userAgent
- } } = window;
+function getProjectDetails() {
+ const {
+ innerWidth,
+ innerHeight,
+ location: { href },
+ navigator: { platform, userAgent },
+ } = window;
const browser = getBrowserId(userAgent);
- const scriptEl = document.getElementById('review-app-toolbar-script')
+ const scriptEl = document.getElementById('review-app-toolbar-script');
const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset;
return {
@@ -427,7 +175,7 @@ function getProjectDetails () {
};
}
-function logoutUser () {
+function logoutUser() {
const { localStorage } = window;
// All the browsers we support have localStorage, so let's silently fail
@@ -441,7 +189,7 @@ function logoutUser () {
addLoginForm();
}
-function postComment ({
+function postComment({
href,
platform,
browser,
@@ -478,32 +226,34 @@ function postComment ({
const url = `
${mrUrl}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/discussions`;
-
const body = `${commentText} ${detailText}`;
fetch(url, {
- method: 'POST',
- headers: {
+ method: 'POST',
+ headers: {
'PRIVATE-TOKEN': data.token,
'Content-Type': 'application/json',
},
body: JSON.stringify({ body }),
})
- .then((response) => {
- if (response.ok) {
- confirmAndClear(mergeRequestId);
- return;
- }
-
- throw new Error(`${response.status}: ${response.statusText}`)
- })
- .catch((err) => {
- postError(`The feedback was not sent successfully. Please try again. Error: ${err.message}`, 'gitlab-comment');
- resetCommentBox();
- });
+ .then(response => {
+ if (response.ok) {
+ confirmAndClear(mergeRequestId);
+ return;
+ }
+
+ throw new Error(`${response.status}: ${response.statusText}`);
+ })
+ .catch(err => {
+ postError(
+ `The feedback was not sent successfully. Please try again. Error: ${err.message}`,
+ 'gitlab-comment',
+ );
+ resetCommentBox();
+ });
}
-function postError (message, inputId) {
+function postError(message, inputId) {
const note = document.getElementById('gitlab-validation-note');
const field = document.getElementById(inputId);
field.style.borderColor = '#db3b21';
@@ -543,8 +293,7 @@ function setInProgressState() {
commentBox.style.pointerEvents = 'none';
}
-function storeToken (token) {
-
+function storeToken(token) {
const { localStorage } = window;
// All the browsers we support have localStorage, so let's silently fail
@@ -556,7 +305,7 @@ function storeToken (token) {
}
}
-function toggleForm () {
+function toggleForm() {
const container = document.getElementById('gitlab-review-container');
const collapseButton = document.getElementById('gitlab-collapse');
const form = document.getElementById('gitlab-form-wrapper');
@@ -578,7 +327,7 @@ function toggleForm () {
display: 'none',
backgroundColor: 'rgba(255, 255, 255, 0)',
},
- }
+ };
const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
@@ -593,9 +342,9 @@ function toggleForm () {
///////////////// INJECTION //////////////////
///////////////////////////////////////////////
-function noop() {};
+function noop() {}
-const eventLookup = ({target: { id }}) => {
+const eventLookup = ({ target: { id } }) => {
switch (id) {
case 'gitlab-collapse':
return toggleForm;
@@ -620,9 +369,9 @@ window.addEventListener('load', () => {
container.insertAdjacentHTML('beforeend', form(content));
document.body.insertBefore(container, document.body.firstChild);
- addStylesheet();
- document.getElementById('gitlab-review-container').addEventListener('click', (event) => {
+ document.getElementById('gitlab-review-container').addEventListener('click', event => {
eventLookup(event)();
});
+
});
diff --git a/yarn.lock b/yarn.lock
index 7c119e2c9dd..45729eda890 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -698,10 +698,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.63.0.tgz#9dd544026d203e4ce6efed72b05db68f710c4d49"
integrity sha512-YztrReFTg31B7v5wtUC5j15KHNcMebtW+kACytEU42XomMaIwk4USIbygqWlq0VRHA2VHJrHApfJHIjxiCCQcA==
-"@gitlab/ui@^3.10.0":
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.10.0.tgz#0f16772b7fe8052dabd37aba2ae436255b9e0f0a"
- integrity sha512-po6fh2T8esa2Nach73AYLdoTg8N0YrRa5GkJk5GoxVrHYoAUD8T1Rn3pXXXKSsQdQcYjIZJ6uvY8sL+qg+Yjww==
+"@gitlab/ui@^3.10.1":
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.10.1.tgz#473e1383b05847d7226257228a8fd8d2905a9415"
+ integrity sha512-LReB+EIBvZlfeEX2s/azuz6NvegrwwzficvBxK5yPCztjDeegcEsTDlt0F32mX6DcvNFacuuAyWN8QtgpaWkhA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"
@@ -712,7 +712,7 @@
js-beautify "^1.8.8"
lodash "^4.17.11"
url-search-params-polyfill "^5.0.0"
- vue "^2.5.21"
+ vue "^2.6.10"
vue-loader "^15.4.2"
"@gitlab/vue-toasted@^1.2.1":
@@ -11353,7 +11353,7 @@ vue-virtual-scroll-list@^1.3.1:
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76"
integrity sha512-PMTxiK9/P1LtgoWWw4n1QnmDDkYqIdWWCNdt1L4JD9g6rwDgnsGsSV10bAnd5n7DQLHGWHjRex+zAbjXWT8t0g==
-vue@^2.5.21, vue@^2.6.10:
+vue@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==