summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile.lock2
-rw-r--r--README.md2
-rw-r--r--app/assets/javascripts/api.js11
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js5
-rw-r--r--app/assets/javascripts/diffs/components/app.vue2
-rw-r--r--app/assets/javascripts/diffs/components/changed_files_dropdown.vue100
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue8
-rw-r--r--app/assets/javascripts/dispatcher.js6
-rw-r--r--app/assets/javascripts/due_date_select.js41
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js3
-rw-r--r--app/assets/javascripts/gl_form.js7
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue4
-rw-r--r--app/assets/javascripts/ide/services/index.js33
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js31
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js44
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js44
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js39
-rw-r--r--app/assets/javascripts/merge_request_tabs.js172
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue2
-rw-r--r--app/assets/javascripts/pages/search/show/search.js2
-rw-r--r--app/assets/javascripts/pages/snippets/form.js1
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue37
-rw-r--r--app/assets/javascripts/project_select.js5
-rw-r--r--app/assets/javascripts/shared/milestones/form.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue1
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss5
-rw-r--r--app/assets/stylesheets/framework/blocks.scss5
-rw-r--r--app/assets/stylesheets/framework/common.scss5
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss17
-rw-r--r--app/assets/stylesheets/framework/files.scss30
-rw-r--r--app/assets/stylesheets/framework/forms.scss12
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss5
-rw-r--r--app/assets/stylesheets/pages/commits.scss14
-rw-r--r--app/assets/stylesheets/pages/diff.scss8
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/milestone.scss36
-rw-r--r--app/assets/stylesheets/pages/settings.scss13
-rw-r--r--app/controllers/projects/lfs_api_controller.rb7
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/notes_helper.rb1
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/merge_request.rb20
-rw-r--r--app/models/repository.rb10
-rw-r--r--app/models/user.rb2
-rw-r--r--app/views/admin/dashboard/index.html.haml8
-rw-r--r--app/views/admin/identities/_form.html.haml4
-rw-r--r--app/views/admin/identities/_identity.html.haml6
-rw-r--r--app/views/admin/identities/edit.html.haml4
-rw-r--r--app/views/admin/identities/index.html.haml10
-rw-r--r--app/views/admin/identities/new.html.haml4
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml2
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml37
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml48
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml26
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml8
-rw-r--r--app/views/projects/settings/integrations/show.html.haml4
-rw-r--r--app/views/projects/settings/members/show.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml4
-rw-r--r--app/views/shared/_milestone_expired.html.haml9
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml16
-rw-r--r--app/views/shared/boards/components/_board.html.haml6
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml3
-rw-r--r--app/views/shared/empty_states/_issues.html.haml2
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml2
-rw-r--r--app/views/shared/issuable/form/_title.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml105
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml8
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/project_cache_worker.rb33
-rw-r--r--app/workers/prune_web_hook_logs_worker.rb26
-rw-r--r--changelogs/unreleased/39543-milestone-page-list-redesign.yml5
-rw-r--r--changelogs/unreleased/43270-import-with-milestones-failing.yml5
-rw-r--r--changelogs/unreleased/44725-expire_correct_methods_after_change_head.yml5
-rw-r--r--changelogs/unreleased/44726-cancel_lease_upon_completion_in_project_cache_worker.yml5
-rw-r--r--changelogs/unreleased/47462-issues-disabled-group-page.yml6
-rw-r--r--changelogs/unreleased/47516-pipe-scroll.yml5
-rw-r--r--changelogs/unreleased/47661-merge-request-box-disappearing-on-chrome.yml5
-rw-r--r--changelogs/unreleased/47769-fix_ambiguous_due_date_for_issue_scopes.yml5
-rw-r--r--changelogs/unreleased/47865-changelog-for-style-updates.yml5
-rw-r--r--changelogs/unreleased/48603-merge-request-refactor-title-and-copy-to-clipboard-button-are-behind-the-action-buttons.yml5
-rw-r--r--changelogs/unreleased/48653-mr-target-branch-missing.yml5
-rw-r--r--changelogs/unreleased/add-title-placeholder-for-new-issues.yml5
-rw-r--r--changelogs/unreleased/backstage-gb-stages-position-migration-clean-up.yml5
-rw-r--r--changelogs/unreleased/bw-fix-ee-dashboard.yml5
-rw-r--r--changelogs/unreleased/cr-add-locked-state-to-MR.yml5
-rw-r--r--changelogs/unreleased/dm-user-without-projects-performance.yml5
-rw-r--r--changelogs/unreleased/fix-last-commit-author-link-is-blue.yml5
-rw-r--r--changelogs/unreleased/fix-tooltip-flicker.yml5
-rw-r--r--changelogs/unreleased/jprovazn-extra-line.yml5
-rw-r--r--changelogs/unreleased/prune-web-hook-logs.yml5
-rw-r--r--changelogs/unreleased/transfer_project_api_endpoint.yml5
-rw-r--r--changelogs/unreleased/update-bcrypt-to-support-libxcrypt.yml5
-rw-r--r--changelogs/unreleased/update-external-link-icon-in-merge-request-widget.yml5
-rw-r--r--config/aws.yml.example22
-rw-r--r--config/initializers/1_settings.rb4
-rw-r--r--config/initializers/carrierwave.rb31
-rw-r--r--db/post_migrate/20180604123514_cleanup_stages_position_migration.rb43
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/index.md2
-rw-r--r--doc/administration/job_artifacts.md5
-rw-r--r--doc/administration/job_traces.md155
-rw-r--r--doc/administration/uploads.md1
-rw-r--r--doc/api/groups.md2
-rw-r--r--doc/api/merge_requests.md12
-rw-r--r--doc/api/projects.md10
-rw-r--r--doc/ci/docker/using_docker_build.md23
-rw-r--r--doc/ci/examples/code_climate.md2
-rw-r--r--doc/ci/examples/container_scanning.md2
-rw-r--r--doc/ci/examples/dast.md2
-rw-r--r--doc/ci/triggers/README.md2
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/ee_features.md2
-rw-r--r--doc/development/i18n/externalization.md2
-rw-r--r--doc/development/ux_guide/copy.md2
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md2
-rw-r--r--doc/install/requirements.md8
-rw-r--r--doc/raketasks/backup_restore.md25
-rw-r--r--doc/topics/autodevops/index.md4
-rw-r--r--doc/update/11.0-to-11.1.md361
-rw-r--r--doc/user/index.md2
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/clusters/index.md2
-rw-r--r--doc/user/project/integrations/bamboo.md3
-rw-r--r--doc/user/project/integrations/webhooks.md16
-rw-r--r--doc/user/project/issue_board.md2
-rw-r--r--doc/user/project/issues/index.md6
-rw-r--r--doc/user/project/issues/issues_functionalities.md2
-rw-r--r--doc/user/project/merge_requests/index.md2
-rw-r--r--doc/user/project/repository/index.md6
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--doc/user/project/settings/index.md2
-rw-r--r--doc/workflow/lfs/lfs_administration.md3
-rw-r--r--lib/api/groups.rb4
-rw-r--r--lib/api/merge_requests.rb4
-rw-r--r--lib/api/projects.rb17
-rw-r--r--lib/banzai/filter/emoji_filter.rb4
-rw-r--r--lib/gitlab/diff/file.rb2
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/group_project_object_builder.rb90
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb29
-rw-r--r--lib/gitlab/import_export/relation_factory.rb74
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb33
-rw-r--r--lib/gitlab/repository_cache_adapter.rb10
-rw-r--r--lib/tasks/gitlab/import_export.rake21
-rw-r--r--locale/gitlab.pot230
-rw-r--r--package.json1
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb2
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/features/groups/empty_states_spec.rb30
-rw-r--r--spec/features/groups/issues_spec.rb22
-rw-r--r--spec/features/groups/merge_requests_spec.rb17
-rw-r--r--spec/features/groups/milestone_spec.rb13
-rw-r--r--spec/features/ics/dashboard_issues_spec.rb37
-rw-r--r--spec/features/milestones/user_deletes_milestone_spec.rb1
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin343091 -> 343136 bytes
-rw-r--r--spec/features/projects/issues/user_creates_issue_spec.rb3
-rw-r--r--spec/features/projects/milestones/new_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb4
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb4
-rw-r--r--spec/features/tags/master_updates_tag_spec.rb4
-rw-r--r--spec/finders/merge_requests_finder_spec.rb14
-rw-r--r--spec/fixtures/exported-project.gzbin2560 -> 0 bytes
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb2
-rw-r--r--spec/javascripts/api_spec.js2
-rw-r--r--spec/javascripts/helpers/init_vue_mr_page_helper.js12
-rw-r--r--spec/javascripts/ide/components/error_message_spec.js12
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js18
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js276
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js267
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js2
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js103
-rw-r--r--spec/javascripts/merge_request_spec.js23
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js1
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js30
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb15
-rw-r--r--spec/lib/gitlab/import_export/group_project_object_builder_spec.rb52
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project.light.json8
-rw-r--r--spec/lib/gitlab/import_export/project.milestone-iid.json80
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb82
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb35
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb12
-rw-r--r--spec/lib/mattermost/session_spec.rb20
-rw-r--r--spec/migrations/cleanup_stages_position_migration_spec.rb67
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb4
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb11
-rw-r--r--spec/models/merge_request_spec.rb16
-rw-r--r--spec/models/repository_spec.rb36
-rw-r--r--spec/requests/api/merge_requests_spec.rb15
-rw-r--r--spec/requests/api/projects_spec.rb32
-rw-r--r--spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb17
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb10
-rw-r--r--spec/support/helpers/exclusive_lease_helpers.rb36
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/api/merge_requests_list.rb18
-rw-r--r--spec/support/shared_examples/uploaders/object_storage_shared_examples.rb8
-rw-r--r--spec/workers/project_cache_worker_spec.rb86
-rw-r--r--spec/workers/project_migrate_hashed_storage_worker_spec.rb60
-rw-r--r--spec/workers/propagate_service_template_worker_spec.rb38
-rw-r--r--spec/workers/prune_web_hook_logs_worker_spec.rb22
-rw-r--r--spec/workers/repository_remove_remote_worker_spec.rb57
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb38
-rw-r--r--vendor/assets/javascripts/date.format.js132
-rw-r--r--vendor/project_templates/express.tar.gzbin4866 -> 4894 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25151 -> 25182 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin49430 -> 49476 bytes
-rw-r--r--yarn.lock4
224 files changed, 3072 insertions, 1519 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 79e3888fa64..1cd336c95d0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -76,7 +76,7 @@ GEM
babosa (1.0.2)
base32 (0.3.2)
batch-loader (1.2.1)
- bcrypt (3.1.11)
+ bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
better_errors (2.1.1)
diff --git a/README.md b/README.md
index 8bd667b3dac..295e1d6c6cc 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ We're hiring developers, support people, and production engineers all the time,
There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license.
-- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/products/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/products/).
+- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/pricing/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
## Website
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index d62fae99c6b..0ca0e8f35dd 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -150,14 +150,15 @@ const Api = {
},
// Return group projects list. Filtered by query
- groupProjects(groupId, query, callback) {
+ groupProjects(groupId, query, options, callback) {
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+ const defaults = {
+ search: query,
+ per_page: 20,
+ };
return axios
.get(url, {
- params: {
- search: query,
- per_page: 20,
- },
+ params: Object.assign({}, defaults, options),
})
.then(({ data }) => callback(data));
},
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js
index 70132dbfa6f..9eaa0cd227d 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js
@@ -1,8 +1,7 @@
-/* global dateFormat */
-
import Vue from 'vue';
+import dateFormat from 'dateformat';
-Vue.filter('due-date', (value) => {
+Vue.filter('due-date', value => {
const date = new Date(value);
return dateFormat(date, 'mmm d, yyyy', true);
});
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index e0aecda54f6..eb0985e5603 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -145,7 +145,7 @@ export default {
</script>
<template>
- <div v-if="shouldShow">
+ <div v-show="shouldShow">
<div
v-if="isLoading"
class="loading"
diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
index f224b9dd246..b38d217fbe3 100644
--- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue
@@ -66,59 +66,61 @@ export default {
@click="clearSearch"
></i>
</div>
- <ul>
- <li
- v-for="diffFile in filteredDiffFiles"
- :key="diffFile.name"
- >
- <a
- :href="`#${diffFile.fileHash}`"
- :title="diffFile.newPath"
- class="diff-changed-file"
+ <div class="dropdown-content">
+ <ul>
+ <li
+ v-for="diffFile in filteredDiffFiles"
+ :key="diffFile.name"
>
- <icon
- :name="fileChangedIcon(diffFile)"
- :size="16"
- :class="fileChangedClass(diffFile)"
- class="diff-file-changed-icon append-right-8"
- />
- <span class="diff-changed-file-content append-right-8">
- <strong
- v-if="diffFile.blob && diffFile.blob.name"
- class="diff-changed-file-name"
- >
- {{ diffFile.blob.name }}
- </strong>
- <strong
- v-else
- class="diff-changed-blank-file-name"
- >
- {{ s__('Diffs|No file name available') }}
- </strong>
- <span class="diff-changed-file-path prepend-top-5">
- {{ truncatedDiffPath(diffFile.blob.path) }}
+ <a
+ :href="`#${diffFile.fileHash}`"
+ :title="diffFile.newPath"
+ class="diff-changed-file"
+ >
+ <icon
+ :name="fileChangedIcon(diffFile)"
+ :size="16"
+ :class="fileChangedClass(diffFile)"
+ class="diff-file-changed-icon append-right-8"
+ />
+ <span class="diff-changed-file-content append-right-8">
+ <strong
+ v-if="diffFile.blob && diffFile.blob.name"
+ class="diff-changed-file-name"
+ >
+ {{ diffFile.blob.name }}
+ </strong>
+ <strong
+ v-else
+ class="diff-changed-blank-file-name"
+ >
+ {{ s__('Diffs|No file name available') }}
+ </strong>
+ <span class="diff-changed-file-path prepend-top-5">
+ {{ truncatedDiffPath(diffFile.blob.path) }}
+ </span>
</span>
- </span>
- <span class="diff-changed-stats">
- <span class="cgreen">
- +{{ diffFile.addedLines }}
+ <span class="diff-changed-stats">
+ <span class="cgreen">
+ +{{ diffFile.addedLines }}
+ </span>
+ <span class="cred">
+ -{{ diffFile.removedLines }}
+ </span>
</span>
- <span class="cred">
- -{{ diffFile.removedLines }}
- </span>
- </span>
- </a>
- </li>
+ </a>
+ </li>
- <li
- v-show="filteredDiffFiles.length === 0"
- class="dropdown-menu-empty-item"
- >
- <a>
- {{ __('No files found') }}
- </a>
- </li>
- </ul>
+ <li
+ v-show="filteredDiffFiles.length === 0"
+ class="dropdown-menu-empty-item"
+ >
+ <a>
+ {{ __('No files found') }}
+ </a>
+ </li>
+ </ul>
+ </div>
</div>
</span>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 6bad389f778..fba1d1af7cd 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -112,7 +112,11 @@ export default {
},
methods: {
handleToggle(e, checkTarget) {
- if (!checkTarget || e.target === this.$refs.header) {
+ if (
+ !checkTarget ||
+ e.target === this.$refs.header ||
+ (e.target.classList && e.target.classList.contains('diff-toggle-caret'))
+ ) {
this.$emit('toggleFile');
}
},
@@ -201,7 +205,7 @@ export default {
<div
v-if="!diffFile.submodule && addMergeRequestButtons"
- class="file-actions d-none d-md-block"
+ class="file-actions d-none d-sm-block"
>
<template
v-if="diffFile.blob && diffFile.blob.readableText"
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index b755458aa4b..a5af37e80b6 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,12 +1,12 @@
/* eslint-disable consistent-return, no-new */
import $ from 'jquery';
-import Flash from './flash';
import GfmAutoComplete from './gfm_auto_complete';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete';
+import performanceBar from './performance_bar';
function initSearch() {
// Only when search form is present
@@ -72,9 +72,7 @@ function initGFMInput() {
function initPerformanceBar() {
if (document.querySelector('#js-peek')) {
- import('./performance_bar')
- .then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap
- .catch(() => Flash('Error loading performance bar module'));
+ performanceBar({ container: '#js-peek' });
}
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 4164149dd06..17ea3bdb179 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -1,7 +1,6 @@
-/* global dateFormat */
-
import $ from 'jquery';
import Pikaday from 'pikaday';
+import dateFormat from 'dateformat';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
@@ -55,7 +54,7 @@ class DueDateSelect {
format: 'yyyy-mm-dd',
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
- onSelect: (dateText) => {
+ onSelect: dateText => {
$dueDateInput.val(calendar.toString(dateText));
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
@@ -73,7 +72,7 @@ class DueDateSelect {
}
initRemoveDueDate() {
- this.$block.on('click', '.js-remove-due-date', (e) => {
+ this.$block.on('click', '.js-remove-due-date', e => {
const calendar = this.$datePicker.data('pikaday');
e.preventDefault();
@@ -124,7 +123,8 @@ class DueDateSelect {
this.$loading.fadeOut();
};
- gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
+ gl.issueBoards.BoardsStore.detail.issue
+ .update(this.$dropdown.attr('data-issue-update'))
.then(fadeOutLoader)
.catch(fadeOutLoader);
}
@@ -147,17 +147,18 @@ class DueDateSelect {
$('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length);
- return axios.put(this.issueUpdateURL, this.datePayload)
- .then(() => {
- const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
- if (isDropdown) {
- this.$dropdown.trigger('loaded.gl.dropdown');
- this.$dropdown.dropdown('toggle');
- }
- this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
+ return axios.put(this.issueUpdateURL, this.datePayload).then(() => {
+ const tooltipText = hasDueDate
+ ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})`
+ : __('Due date');
+ if (isDropdown) {
+ this.$dropdown.trigger('loaded.gl.dropdown');
+ this.$dropdown.dropdown('toggle');
+ }
+ this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
- return this.$loading.fadeOut();
- });
+ return this.$loading.fadeOut();
+ });
}
}
@@ -187,15 +188,19 @@ export default class DueDateSelectors {
$datePicker.data('pikaday', calendar);
});
- $('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
+ $('.js-clear-due-date,.js-clear-start-date').on('click', e => {
e.preventDefault();
- const calendar = $(e.target).siblings('.datepicker').data('pikaday');
+ const calendar = $(e.target)
+ .siblings('.datepicker')
+ .data('pikaday');
calendar.setDate(null);
});
}
// eslint-disable-next-line class-methods-use-this
initIssuableSelect() {
- const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
+ const $loading = $('.js-issuable-update .due_date')
+ .find('.block-loading')
+ .hide();
$('.js-due-date-select').each((i, dropdown) => {
const $dropdown = $(dropdown);
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 09186a865e4..73b2cd0b2c7 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -12,7 +12,7 @@ export const defaultAutocompleteConfig = {
members: true,
issues: true,
mergeRequests: true,
- epics: false,
+ epics: true,
milestones: true,
labels: true,
};
@@ -493,6 +493,7 @@ GfmAutoComplete.atTypeMap = {
'@': 'members',
'#': 'issues',
'!': 'mergeRequests',
+ '&': 'epics',
'~': 'labels',
'%': 'milestones',
'/': 'commands',
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index f802971a3ca..c74de7ac34d 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -9,6 +9,13 @@ export default class GLForm {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM);
+ // Disable autocomplete for keywords which do not have dataSources available
+ const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
+ Object.keys(this.enableGFM).forEach(item => {
+ if (item !== 'emojis') {
+ this.enableGFM[item] = !!dataSources[item];
+ }
+ });
// Before we start, we should clean up any previous data for this form
this.destroy();
// Setup the form
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index e7408264c80..acbc98b7a7b 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -24,8 +24,8 @@ export default {
this.isLoading = true;
- this.$store
- .dispatch(this.message.action, this.message.actionPayload)
+ this.message
+ .action(this.message.actionPayload)
.then(() => {
this.isLoading = false;
})
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 5e642067141..3e939f0c1a3 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,16 +1,11 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
-Vue.use(VueResource);
-
export default {
- getTreeData(endpoint) {
- return Vue.http.get(endpoint, { params: { format: 'json' } });
- },
getFileData(endpoint) {
- return Vue.http.get(endpoint, { params: { format: 'json', viewer: 'none' } });
+ return axios.get(endpoint, {
+ params: { format: 'json', viewer: 'none' },
+ });
},
getRawFileData(file) {
if (file.tempFile) {
@@ -21,7 +16,11 @@ export default {
return Promise.resolve(file.raw);
}
- return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
+ return axios
+ .get(file.rawPath, {
+ params: { format: 'json' },
+ })
+ .then(({ data }) => data);
},
getBaseRawFileData(file, sha) {
if (file.tempFile) {
@@ -32,11 +31,11 @@ export default {
return Promise.resolve(file.baseRaw);
}
- return Vue.http
+ return axios
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
params: { format: 'json' },
})
- .then(res => res.text());
+ .then(({ data }) => data);
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
@@ -53,21 +52,9 @@ export default {
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
- createBranch(projectId, payload) {
- const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
-
- return Vue.http.post(url, payload);
- },
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
- getTreeLastCommit(endpoint) {
- return Vue.http.get(endpoint, {
- params: {
- format: 'json',
- },
- });
- },
getFiles(projectUrl, branchId) {
const url = `${projectUrl}/files/${branchId}`;
return axios.get(url, { params: { format: 'json' } });
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 29995a29d1a..6c0887e11ee 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,5 +1,5 @@
-import { normalizeHeaders } from '~/lib/utils/common_utils';
-import flash from '~/flash';
+import { __ } from '../../../locale';
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
@@ -66,13 +66,10 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
.getFileData(
`${gon.relative_url_root ? gon.relative_url_root : ''}${file.url.replace('/-/', '/')}`,
)
- .then(res => {
- const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
- setPageTitle(pageTitle);
+ .then(({ data, headers }) => {
+ const normalizedHeaders = normalizeHeaders(headers);
+ setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
- return res.json();
- })
- .then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, path);
if (makeFileActive) dispatch('setFileActive', path);
@@ -80,7 +77,13 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
- flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the file.'),
+ action: payload =>
+ dispatch('getFileData', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { path, makeFileActive },
+ });
});
};
@@ -88,7 +91,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
};
-export const getRawFileData = ({ state, commit }, { path, baseSha }) => {
+export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
const file = state.entries[path];
return new Promise((resolve, reject) => {
service
@@ -113,7 +116,13 @@ export const getRawFileData = ({ state, commit }, { path, baseSha }) => {
}
})
.catch(() => {
- flash('Error loading file content. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the file content.'),
+ action: payload =>
+ dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { path, baseSha },
+ });
reject();
});
});
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index edb20ff96fc..4aa151abcb7 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -1,17 +1,16 @@
-import flash from '~/flash';
+import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
export const getMergeRequestData = (
- { commit, state },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service
.getProjectMergeRequestData(projectId, mergeRequestId)
- .then(res => res.data)
- .then(data => {
+ .then(({ data }) => {
commit(types.SET_MERGE_REQUEST, {
projectPath: projectId,
mergeRequestId,
@@ -21,7 +20,15 @@ export const getMergeRequestData = (
resolve(data);
})
.catch(() => {
- flash('Error loading merge request data. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the merge request.'),
+ action: payload =>
+ dispatch('getMergeRequestData', payload).then(() =>
+ dispatch('setErrorMessage', null),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, mergeRequestId, force },
+ });
reject(new Error(`Merge Request not loaded ${projectId}`));
});
} else {
@@ -30,15 +37,14 @@ export const getMergeRequestData = (
});
export const getMergeRequestChanges = (
- { commit, state },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
service
.getProjectMergeRequestChanges(projectId, mergeRequestId)
- .then(res => res.data)
- .then(data => {
+ .then(({ data }) => {
commit(types.SET_MERGE_REQUEST_CHANGES, {
projectPath: projectId,
mergeRequestId,
@@ -47,7 +53,15 @@ export const getMergeRequestChanges = (
resolve(data);
})
.catch(() => {
- flash('Error loading merge request changes. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the merge request changes.'),
+ action: payload =>
+ dispatch('getMergeRequestChanges', payload).then(() =>
+ dispatch('setErrorMessage', null),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, mergeRequestId, force },
+ });
reject(new Error(`Merge Request Changes not loaded ${projectId}`));
});
} else {
@@ -56,7 +70,7 @@ export const getMergeRequestChanges = (
});
export const getMergeRequestVersions = (
- { commit, state },
+ { commit, dispatch, state },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
@@ -73,7 +87,15 @@ export const getMergeRequestVersions = (
resolve(data);
})
.catch(() => {
- flash('Error loading merge request versions. Please try again.');
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading the merge request version data.'),
+ action: payload =>
+ dispatch('getMergeRequestVersions', payload).then(() =>
+ dispatch('setErrorMessage', null),
+ ),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, mergeRequestId, force },
+ });
reject(new Error(`Merge Request Versions not loaded ${projectId}`));
});
} else {
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index ab5cd8e4742..501e25d452b 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -104,7 +104,7 @@ export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch)
.catch(() => {
dispatch('setErrorMessage', {
text: __('An error occured creating the new branch.'),
- action: 'createNewBranchFromDefault',
+ action: payload => dispatch('createNewBranchFromDefault', payload),
actionText: __('Please try again'),
actionPayload: branch,
});
@@ -119,7 +119,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
},
false,
),
- action: 'createNewBranchFromDefault',
+ action: payload => dispatch('createNewBranchFromDefault', payload),
actionText: __('Create branch'),
actionPayload: branchId,
});
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index dcdd900fc7e..ffaaaabff17 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -1,9 +1,6 @@
-import { normalizeHeaders } from '~/lib/utils/common_utils';
-import flash from '~/flash';
import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
-import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit }, path) => {
@@ -37,32 +34,6 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('showTreeEntry', row.path);
};
-export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => {
- if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
-
- service
- .getTreeLastCommit(tree.lastCommitPath)
- .then(res => {
- const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
-
- commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
-
- return res.json();
- })
- .then(data => {
- data.forEach(lastCommit => {
- const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
-
- if (entry) {
- commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
- }
- });
-
- dispatch('getLastCommitData', tree);
- })
- .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
-};
-
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => {
if (
@@ -106,14 +77,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
if (e.response.status === 404) {
dispatch('showBranchNotFoundError', branchId);
} else {
- flash(
- __('Error loading tree data. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- );
+ dispatch('setErrorMessage', {
+ text: __('An error occured whilst loading all the files.'),
+ action: payload =>
+ dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, branchId },
+ });
}
reject(e);
});
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 7cca32dc6fa..1f66fa811ea 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,11 +1,10 @@
import $ from 'jquery';
import timeago from 'timeago.js';
-import dateFormat from 'vendor/date.format';
+import dateFormat from 'dateformat';
import { pluralize } from './text_utility';
import { languageCode, s__ } from '../../locale';
window.timeago = timeago;
-window.dateFormat = dateFormat;
/**
* Returns i18n month names array.
@@ -143,7 +142,8 @@ export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
if (setTimeago) {
// Recreate with custom template
$(el).tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
+ template:
+ '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>',
});
}
@@ -275,10 +275,8 @@ export const totalDaysInMonth = date => {
*
* @param {Array} quarter
*/
-export const totalDaysInQuarter = quarter => quarter.reduce(
- (acc, month) => acc + totalDaysInMonth(month),
- 0,
-);
+export const totalDaysInQuarter = quarter =>
+ quarter.reduce((acc, month) => acc + totalDaysInMonth(month), 0);
/**
* Returns list of Dates referring to Sundays of the month
@@ -333,14 +331,8 @@ export const getTimeframeWindowFrom = (startDate, length) => {
// Iterate and set date for the size of length
// and push date reference to timeframe list
const timeframe = new Array(length)
- .fill()
- .map(
- (val, i) => new Date(
- startDate.getFullYear(),
- startDate.getMonth() + i,
- 1,
- ),
- );
+ .fill()
+ .map((val, i) => new Date(startDate.getFullYear(), startDate.getMonth() + i, 1));
// Change date of last timeframe item to last date of the month
timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1]));
@@ -362,14 +354,15 @@ export const getTimeframeWindowFrom = (startDate, length) => {
* @param {Date} date
* @param {Array} quarter
*/
-export const dayInQuarter = (date, quarter) => quarter.reduce((acc, month) => {
- if (date.getMonth() > month.getMonth()) {
- return acc + totalDaysInMonth(month);
- } else if (date.getMonth() === month.getMonth()) {
- return acc + date.getDate();
- }
- return acc + 0;
-}, 0);
+export const dayInQuarter = (date, quarter) =>
+ quarter.reduce((acc, month) => {
+ if (date.getMonth() > month.getMonth()) {
+ return acc + totalDaysInMonth(month);
+ } else if (date.getMonth() === month.getMonth()) {
+ return acc + date.getDate();
+ }
+ return acc + 0;
+ }, 0);
window.gl = window.gl || {};
window.gl.utils = {
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 329d4303132..53d7504de35 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -16,6 +16,7 @@ import Diff from './diff';
import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
import Notes from './notes';
+import { polyfillSticky } from './lib/utils/sticky';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -68,12 +69,23 @@ let { location } = window;
export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
- const mergeRequestTabs = document.querySelector('.js-tabs-affix');
+ this.mergeRequestTabs = document.querySelector('.merge-request-tabs-container');
+ this.mergeRequestTabsAll =
+ this.mergeRequestTabs && this.mergeRequestTabs.querySelectorAll
+ ? this.mergeRequestTabs.querySelectorAll('.merge-request-tabs li')
+ : null;
+ this.mergeRequestTabPanes = document.querySelector('#diff-notes-app');
+ this.mergeRequestTabPanesAll =
+ this.mergeRequestTabPanes && this.mergeRequestTabPanes.querySelectorAll
+ ? this.mergeRequestTabPanes.querySelectorAll('.tab-pane')
+ : null;
const navbar = document.querySelector('.navbar-gitlab');
const peek = document.getElementById('js-peek');
const paddingTop = 16;
+
this.commitsTab = document.querySelector('.tab-content .commits.tab-pane');
+ this.currentTab = null;
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
@@ -83,15 +95,15 @@ export default class MergeRequestTabs {
this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
- this.showTab = this.showTab.bind(this);
+ this.clickTab = this.clickTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
if (peek) {
this.stickyTop += peek.offsetHeight;
}
- if (mergeRequestTabs) {
- this.stickyTop += mergeRequestTabs.offsetHeight;
+ if (this.mergeRequestTabs) {
+ this.stickyTop += this.mergeRequestTabs.offsetHeight;
}
if (stubLocation) {
@@ -99,25 +111,22 @@ export default class MergeRequestTabs {
}
this.bindEvents();
- this.activateTab(action);
+ if (
+ this.mergeRequestTabs &&
+ this.mergeRequestTabs.querySelector(`a[data-action='${action}']`) &&
+ this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click
+ )
+ this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click();
this.initAffix();
}
bindEvents() {
- $(document)
- .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .on('click', '.js-show-tab', this.showTab);
-
- $('.merge-request-tabs a[data-toggle="tab"]').on('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tabvue"]').on('click', this.clickTab);
}
// Used in tests
unbindEvents() {
- $(document)
- .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
- .off('click', '.js-show-tab', this.showTab);
-
- $('.merge-request-tabs a[data-toggle="tab"]').off('click', this.clickTab);
+ $('.merge-request-tabs a[data-toggle="tabvue"]').off('click', this.clickTab);
}
destroyPipelinesView() {
@@ -129,58 +138,87 @@ export default class MergeRequestTabs {
}
}
- showTab(e) {
- e.preventDefault();
- this.activateTab($(e.target).data('action'));
- }
-
clickTab(e) {
- if (e.currentTarget && isMetaClick(e)) {
- const targetLink = e.currentTarget.getAttribute('href');
+ if (e.currentTarget) {
e.stopImmediatePropagation();
e.preventDefault();
- window.open(targetLink, '_blank');
+
+ const { action } = e.currentTarget.dataset;
+
+ if (action) {
+ const href = e.currentTarget.getAttribute('href');
+ this.tabShown(action, href);
+ } else if (isMetaClick(e)) {
+ const targetLink = e.currentTarget.getAttribute('href');
+ window.open(targetLink, '_blank');
+ }
}
}
- tabShown(e) {
- const $target = $(e.target);
- const action = $target.data('action');
-
- if (action === 'commits') {
- this.loadCommits($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
- this.destroyPipelinesView();
- } else if (this.isDiffAction(action)) {
- if (!isInVueNoteablePage()) {
- this.loadDiff($target.attr('href'));
- }
- if (bp.getBreakpointSize() !== 'lg') {
- this.shrinkView();
+ tabShown(action, href) {
+ if (action !== this.currentTab && this.mergeRequestTabs) {
+ this.currentTab = action;
+
+ if (this.mergeRequestTabPanesAll) {
+ this.mergeRequestTabPanesAll.forEach(el => {
+ const tabPane = el;
+ tabPane.style.display = 'none';
+ });
}
- if (this.diffViewType() === 'parallel') {
- this.expandViewContainer();
+
+ if (this.mergeRequestTabsAll) {
+ this.mergeRequestTabsAll.forEach(el => {
+ el.classList.remove('active');
+ });
}
- this.destroyPipelinesView();
- this.commitsTab.classList.remove('active');
- } else if (action === 'pipelines') {
- this.resetViewContainer();
- this.mountPipelinesView();
- } else {
- if (bp.getBreakpointSize() !== 'xs') {
+
+ const tabPane = this.mergeRequestTabPanes.querySelector(`#${action}`);
+ if (tabPane) tabPane.style.display = 'block';
+ const tab = this.mergeRequestTabs.querySelector(`.${action}-tab`);
+ if (tab) tab.classList.add('active');
+
+ if (action === 'commits') {
+ this.loadCommits(href);
+ this.expandView();
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+ } else if (action === 'new') {
this.expandView();
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+ } else if (this.isDiffAction(action)) {
+ if (!isInVueNoteablePage()) {
+ this.loadDiff(href);
+ }
+ if (bp.getBreakpointSize() !== 'lg') {
+ this.shrinkView();
+ }
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
+ }
+ this.destroyPipelinesView();
+ this.commitsTab.classList.remove('active');
+ } else if (action === 'pipelines') {
+ this.resetViewContainer();
+ this.mountPipelinesView();
+ } else {
+ this.mergeRequestTabPanes.querySelector('#notes').style.display = 'block';
+ this.mergeRequestTabs.querySelector('.notes-tab').classList.add('active');
+
+ if (bp.getBreakpointSize() !== 'xs') {
+ this.expandView();
+ }
+ this.resetViewContainer();
+ this.destroyPipelinesView();
+
+ initDiscussionTab();
+ }
+ if (this.setUrl) {
+ this.setCurrentAction(action);
}
- this.resetViewContainer();
- this.destroyPipelinesView();
- initDiscussionTab();
- }
- if (this.setUrl) {
- this.setCurrentAction(action);
+ this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
}
-
- this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
}
scrollToElement(container) {
@@ -193,12 +231,6 @@ export default class MergeRequestTabs {
}
}
- // Activate a tab based on the current action
- activateTab(action) {
- // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
- $(`.merge-request-tabs a[data-action='${action}']`).tab('show');
- }
-
// Replaces the current Merge Request-specific action in the URL with a new one
//
// If the action is "notes", the URL is reset to the standard
@@ -426,7 +458,6 @@ export default class MergeRequestTabs {
initAffix() {
const $tabs = $('.js-tabs-affix');
- const $fixedNav = $('.navbar-gitlab');
// Screen space on small screens is usually very sparse
// So we dont affix the tabs on these
@@ -439,21 +470,6 @@ export default class MergeRequestTabs {
*/
if ($tabs.css('position') !== 'static') return;
- const $diffTabs = $('#diff-notes-app');
-
- $tabs
- .off('affix.bs.affix affix-top.bs.affix')
- .affix({
- offset: {
- top: () => $diffTabs.offset().top - $tabs.height() - $fixedNav.height(),
- },
- })
- .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() }))
- .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' }));
-
- // Fix bug when reloading the page already scrolling
- if ($tabs.hasClass('affix')) {
- $tabs.trigger('affix.bs.affix');
- }
+ polyfillSticky($tabs);
}
}
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 7853847fc37..a8995021699 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -175,7 +175,7 @@ export default {
<template>
<div
- v-if="shouldShow"
+ v-show="shouldShow"
id="notes"
>
<ul
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index 2e1fe78b3fa..e3e0ab91993 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -105,7 +105,7 @@ export default class Search {
getProjectsData(term) {
return new Promise((resolve) => {
if (this.groupId) {
- Api.groupProjects(this.groupId, term, resolve);
+ Api.groupProjects(this.groupId, term, {}, resolve);
} else {
Api.projects(term, {
order_by: 'id',
diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js
index 758bbafead3..f369c7ef9a6 100644
--- a/app/assets/javascripts/pages/snippets/form.js
+++ b/app/assets/javascripts/pages/snippets/form.js
@@ -8,6 +8,7 @@ export default () => {
members: false,
issues: false,
mergeRequests: false,
+ epics: false,
milestones: false,
labels: false,
});
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 50d042fef29..9892a039941 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import _ from 'underscore';
import { scaleLinear, scaleThreshold } from 'd3-scale';
import { select } from 'd3-selection';
+import dateFormat from 'dateformat';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
@@ -26,7 +27,7 @@ function getSystemDate(systemUtcOffsetSeconds) {
function formatTooltipText({ date, count }) {
const dateObject = new Date(date);
const dateDayName = getDayName(dateObject);
- const dateText = dateObject.format('mmm d, yyyy');
+ const dateText = dateFormat(dateObject, 'mmm d, yyyy');
let contribText = 'No contributions';
if (count > 0) {
@@ -84,7 +85,7 @@ export default class ActivityCalendar {
date.setDate(date.getDate() + i);
const day = date.getDay();
- const count = timestamps[date.format('yyyy-mm-dd')] || 0;
+ const count = timestamps[dateFormat(date, 'yyyy-mm-dd')] || 0;
// Create a new group array if this is the first day of the week
// or if is first object
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index e047d10ac93..c32dc83da8e 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -109,6 +109,7 @@ export default {
:key="i"
>
<job-component
+ :dropdown-length="job.size"
:job="item"
css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 886e62ab1a7..8af984ef91a 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -46,6 +46,11 @@ export default {
required: false,
default: '',
},
+ dropdownLength: {
+ type: Number,
+ required: false,
+ default: Infinity,
+ },
},
computed: {
status() {
@@ -70,6 +75,10 @@ export default {
return textBuilder.join(' ');
},
+ tooltipBoundary() {
+ return this.dropdownLength < 5 ? 'viewport' : null;
+ },
+
/**
* Verifies if the provided job has an action path
*
@@ -94,9 +103,9 @@ export default {
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
+ :data-boundary="tooltipBoundary"
data-container="body"
data-html="true"
- data-boundary="viewport"
class="js-pipeline-graph-job-link"
>
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index b9231c002fd..56fdb858088 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -186,32 +186,27 @@ export default {
</i>
</button>
- <ul
+ <div
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown"
>
-
- <li
+ <loading-icon v-if="isLoading"/>
+ <ul
+ v-else
class="js-builds-dropdown-list scrollable-menu"
>
-
- <loading-icon v-if="isLoading"/>
-
- <ul
- v-else
+ <li
+ v-for="job in dropdownContent"
+ :key="job.id"
>
- <li
- v-for="job in dropdownContent"
- :key="job.id"
- >
- <job-component
- :job="job"
- css-class-job-name="mini-pipeline-graph-dropdown-item"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
- </li>
- </ul>
- </li>
- </ul>
+ <job-component
+ :dropdown-length="dropdownContent.length"
+ :job="job"
+ css-class-job-name="mini-pipeline-graph-dropdown-item"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </li>
+ </ul>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 240dde56325..bce7556bd40 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -47,7 +47,10 @@ export default function projectSelect() {
projectsCallback = finalCallback;
}
if (_this.groupId) {
- return Api.groupProjects(_this.groupId, query.term, projectsCallback);
+ return Api.groupProjects(_this.groupId, query.term, {
+ with_issues_enabled: _this.withIssuesEnabled,
+ with_merge_requests_enabled: _this.withMergeRequestsEnabled,
+ }, projectsCallback);
} else {
return Api.projects(query.term, {
order_by: _this.orderBy,
diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js
index 060f374310c..8681a1776c6 100644
--- a/app/assets/javascripts/shared/milestones/form.js
+++ b/app/assets/javascripts/shared/milestones/form.js
@@ -8,10 +8,11 @@ export default (initGFM = true) => {
new DueDateSelectors(); // eslint-disable-line no-new
// eslint-disable-next-line no-new
new GLForm($('.milestone-form'), {
- emojis: initGFM,
+ emojis: true,
members: initGFM,
issues: initGFM,
mergeRequests: initGFM,
+ epics: initGFM,
milestones: initGFM,
labels: initGFM,
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index c44419d24e6..5e464f8a0e2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -1,4 +1,5 @@
<script>
+import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import tooltip from '../../vue_shared/directives/tooltip';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
@@ -14,6 +15,7 @@ export default {
LoadingButton,
MemoryUsage,
StatusIcon,
+ Icon,
},
directives: {
tooltip,
@@ -110,11 +112,10 @@ export default {
class="deploy-link js-deploy-url"
>
{{ deployment.external_url_formatted }}
- <i
- class="fa fa-external-link"
- aria-hidden="true"
- >
- </i>
+ <icon
+ :size="16"
+ name="external-link"
+ />
</a>
</template>
<span
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 1fdc3218671..53c4dc8c8f4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -32,7 +32,7 @@
};
</script>
<template>
- <div class="space-children flex-container-block append-right-10">
+ <div class="space-children d-flex append-right-10">
<div
v-if="isLoading"
class="mr-widget-icon"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
index 0d9a560c88e..97f4196b94d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue
@@ -82,7 +82,7 @@
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
- <h4 class="flex-container-block">
+ <h4 class="d-flex align-items-start">
<span class="append-right-10">
{{ s__("mrWidget|Set by") }}
<mr-widget-author :author="mr.setToMWPSBy" />
@@ -119,7 +119,7 @@
</p>
<p
v-else
- class="flex-container-block"
+ class="d-flex align-items-start"
>
<span class="append-right-10">
{{ s__("mrWidget|The source branch will not be removed") }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index fba67681777..298971a36b2 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -67,6 +67,7 @@
members: this.enableAutocomplete,
issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete,
+ epics: this.enableAutocomplete,
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete,
});
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index f610a1aea08..ded33e8b151 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -128,6 +128,11 @@ table {
border-spacing: 0;
}
+.tooltip {
+ // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders
+ pointer-events: none;
+}
+
.popover {
font-size: 14px;
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 1d4828be223..340fddd398b 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -350,11 +350,6 @@
}
}
-.flex-container-block {
- display: -webkit-flex;
- display: flex;
-}
-
.flex-right {
margin-left: auto;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 326499125fc..218e37602dd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -262,12 +262,7 @@ li.note {
}
.milestone {
- &.milestone-closed {
- background: $gray-light;
- }
-
.progress {
- margin-bottom: 0;
margin-top: 4px;
box-shadow: none;
background-color: $border-gray-light;
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 9cbaaa5dc8d..ea4cb9a0b75 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -68,8 +68,7 @@
}
.nav-sidebar {
- transition: width $sidebar-transition-duration,
- left $sidebar-transition-duration;
+ transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed;
z-index: 400;
width: $contextual-sidebar-width;
@@ -77,12 +76,12 @@
bottom: 0;
left: 0;
background-color: $gray-light;
- box-shadow: inset -2px 0 0 $border-color;
+ box-shadow: inset -1px 0 0 $border-color;
transform: translate3d(0, 0, 0);
&:not(.sidebar-collapsed-desktop) {
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
- box-shadow: inset -2px 0 0 $border-color,
+ box-shadow: inset -1px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
}
}
@@ -214,7 +213,7 @@
> li {
> a {
@include media-breakpoint-up(sm) {
- margin-right: 2px;
+ margin-right: 1px;
}
&:hover {
@@ -224,7 +223,7 @@
&.is-showing-fly-out {
> a {
- margin-right: 2px;
+ margin-right: 1px;
}
.sidebar-sub-level-items {
@@ -317,14 +316,14 @@
.toggle-sidebar-button,
.close-nav-button {
- width: $contextual-sidebar-width - 2px;
+ width: $contextual-sidebar-width - 1px;
transition: width $sidebar-transition-duration;
position: fixed;
bottom: 0;
padding: $gl-padding;
background-color: $gray-light;
border: 0;
- border-top: 2px solid $border-color;
+ border-top: 1px solid $border-color;
color: $gl-text-color-secondary;
display: flex;
align-items: center;
@@ -379,7 +378,7 @@
.toggle-sidebar-button {
padding: 16px;
- width: $contextual-sidebar-collapsed-width - 2px;
+ width: $contextual-sidebar-collapsed-width - 1px;
.collapse-text,
.icon-angle-double-left {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index f060254777c..00eac1688f2 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -322,14 +322,17 @@ span.idiff {
}
.file-title-flex-parent {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background-color: $gray-light;
- border-bottom: 1px solid $border-color;
- padding: 5px $gl-padding;
- margin: 0;
- border-radius: $border-radius-default $border-radius-default 0 0;
+ &,
+ .file-holder & {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $gray-light;
+ border-bottom: 1px solid $border-color;
+ padding: 5px $gl-padding;
+ margin: 0;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
.file-header-content {
white-space: nowrap;
@@ -337,6 +340,17 @@ span.idiff {
text-overflow: ellipsis;
padding-right: 30px;
position: relative;
+ width: auto;
+
+ @media (max-width: map-get($grid-breakpoints, sm)-1) {
+ width: 100%;
+ }
+ }
+
+ .file-holder & {
+ .file-actions {
+ position: static;
+ }
}
.btn-clipboard {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 2b2e6d69e33..282e424fc38 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -243,3 +243,15 @@ label {
}
}
}
+
+.input-icon-wrapper {
+ position: relative;
+
+ .input-icon-right {
+ position: absolute;
+ right: 0.8em;
+ top: 50%;
+ transform: translateY(-50%);
+ color: $theme-gray-600;
+ }
+}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 1d247671761..86de88729ee 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -45,4 +45,9 @@
&.status-box-upcoming {
background: $gl-text-color-secondary;
}
+
+ &.status-box-milestone {
+ color: $gl-text-color;
+ background: $gray-darker;
+ }
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 49226ae8eac..f75be4e01cd 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -261,12 +261,16 @@
vertical-align: baseline;
}
- a.autodevops-badge {
- color: $white-light;
- }
+ a {
+ color: $gl-text-color;
- a.autodevops-link {
- color: $gl-link-color;
+ &.autodevops-badge {
+ color: $white-light;
+ }
+
+ &.autodevops-link {
+ color: $gl-link-color;
+ }
}
.commit-row-description {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index d652982410a..a90a9c6e486 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -14,8 +14,8 @@
background-color: $gray-normal;
}
- .diff-toggle-caret {
- padding-right: 6px;
+ svg {
+ vertical-align: text-bottom;
}
}
@@ -737,6 +737,10 @@
max-width: 560px;
width: 100%;
z-index: 150;
+ min-height: $dropdown-min-height;
+ max-height: $dropdown-max-height;
+ overflow-y: auto;
+ margin-bottom: 0;
@include media-breakpoint-up(sm) {
left: $gl-padding;
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 79cac7f4ff0..391dfea0703 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -79,6 +79,7 @@
justify-content: space-between;
padding: $gl-padding;
border-radius: $border-radius-default;
+ border: 1px solid $theme-gray-100;
&.sortable-ghost {
opacity: 0.3;
@@ -89,6 +90,7 @@
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
+ border: 0;
&:active {
cursor: -webkit-grabbing;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index ccf5d632614..efd730af558 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -737,6 +737,10 @@
> *:not(:last-child) {
margin-right: .3em;
}
+
+ svg {
+ vertical-align: text-top;
+ }
}
.deploy-link {
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index dba83e56d72..46437ce5841 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -3,8 +3,20 @@
}
.milestones {
+ padding: $gl-padding-8;
+ margin-top: $gl-padding-8;
+ border-radius: $border-radius-default;
+ background-color: $theme-gray-100;
+
.milestone {
- padding: 10px 16px;
+ border: 0;
+ padding: $gl-padding-top $gl-padding;
+ border-radius: $border-radius-default;
+ background-color: $white-light;
+
+ &:not(:last-child) {
+ margin-bottom: $gl-padding-4;
+ }
h4 {
font-weight: $gl-font-weight-bold;
@@ -13,6 +25,24 @@
.progress {
width: 100%;
height: 6px;
+ margin-bottom: $gl-padding-4;
+ }
+
+ .milestone-progress {
+ a {
+ color: $gl-link-color;
+ }
+ }
+
+ .status-box {
+ font-size: $tooltip-font-size;
+ margin-top: 0;
+ margin-right: $gl-padding-4;
+
+ @include media-breakpoint-down(xs) {
+ line-height: unset;
+ padding: $gl-padding-4 $gl-input-padding;
+ }
}
}
}
@@ -229,6 +259,10 @@
}
}
+.milestone-range {
+ color: $gl-text-color-tertiary;
+}
+
@include media-breakpoint-down(xs) {
.milestone-banner-text,
.milestone-banner-link {
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 2f28031b9c8..e264b06c4b2 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -255,25 +255,12 @@
}
}
-.modal-doorkeepr-auth,
-.doorkeeper-app-form {
- .scope-description {
- color: $theme-gray-700;
- }
-}
-
.modal-doorkeepr-auth {
.modal-body {
padding: $gl-padding;
}
}
-.doorkeeper-app-form {
- .scope-description {
- margin: 0 0 5px 17px;
- }
-}
-
.deprecated-service {
cursor: default;
}
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index ee4ed674110..3f4962b543d 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -93,7 +93,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
end
def lfs_check_batch_operation!
- if upload_request? && Gitlab::Database.read_only?
+ if batch_operation_disallowed?
render(
json: {
message: lfs_read_only_message
@@ -105,6 +105,11 @@ class Projects::LfsApiController < Projects::GitHttpClientController
end
# Overridden in EE
+ def batch_operation_disallowed?
+ upload_request? && Gitlab::Database.read_only?
+ end
+
+ # Overridden in EE
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index f85dcfe6bfc..594563d1f6f 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -77,7 +77,7 @@ class Projects::MilestonesController < Projects::ApplicationController
def promote
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
- flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe
+ flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\"><u>group milestone</u></a>.".html_safe
respond_to do |format|
format.html do
redirect_to project_milestones_path(project)
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 5d5f72c4d86..6fdfd964fca 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -7,7 +7,7 @@
# current_user - which user use
# params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
-# state: 'opened' or 'closed' or 'all'
+# state: 'opened' or 'closed' or 'locked' or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
@@ -311,6 +311,8 @@ class IssuableFinder
items.respond_to?(:merged) ? items.merged : items.closed
when 'opened'
items.opened
+ when 'locked'
+ items.where(state: 'locked')
else
items
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 8d84ed4bdfb..40089c082c1 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -6,7 +6,7 @@
# current_user - which user use
# params:
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
-# state: 'open', 'closed', 'merged', or 'all'
+# state: 'open', 'closed', 'merged', 'locked', or 'all'
# group_id: integer
# project_id: integer
# milestone_title: string
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 82a7931c557..097be8a0643 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -108,7 +108,7 @@ module MergeRequestsHelper
data_attrs = {
action: tab.to_s,
target: "##{tab}",
- toggle: options.fetch(:force_link, false) ? '' : 'tab'
+ toggle: options.fetch(:force_link, false) ? '' : 'tabvue'
}
url = case tab
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index e1a0cf1604c..3fa2e5452c8 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -148,6 +148,7 @@ module NotesHelper
members: autocomplete,
issues: autocomplete,
mergeRequests: autocomplete,
+ epics: autocomplete,
milestones: autocomplete,
labels: autocomplete
}
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d3df2da14e2..4715d942c8d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -48,7 +48,7 @@ class Issue < ActiveRecord::Base
scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}
- scope :with_due_date, -> { where('due_date IS NOT NULL') }
+ scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
@@ -56,7 +56,7 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
- scope :order_closest_future_date, -> { reorder('CASE WHEN due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - due_date) ASC') }
+ scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') }
scope :preload_associations, -> { preload(:labels, project: :namespace) }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 6c96c8ca391..b4090fd8baf 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -128,14 +128,9 @@ class MergeRequest < ActiveRecord::Base
end
after_transition unchecked: :cannot_be_merged do |merge_request, transition|
- begin
- if merge_request.notify_conflict?
- NotificationService.new.merge_request_unmergeable(merge_request)
- TodoService.new.merge_request_became_unmergeable(merge_request)
- end
- rescue Gitlab::Git::CommandError
- # Checking mergeability can trigger exception, e.g. non-utf8
- # We ignore this type of errors.
+ if merge_request.notify_conflict?
+ NotificationService.new.merge_request_unmergeable(merge_request)
+ TodoService.new.merge_request_became_unmergeable(merge_request)
end
end
@@ -707,7 +702,14 @@ class MergeRequest < ActiveRecord::Base
end
def notify_conflict?
- (opened? || locked?) && !project.repository.can_be_merged?(diff_head_sha, target_branch)
+ (opened? || locked?) &&
+ has_commits? &&
+ !branch_missing? &&
+ !project.repository.can_be_merged?(diff_head_sha, target_branch)
+ rescue Gitlab::Git::CommandError
+ # Checking mergeability can trigger exception, e.g. non-utf8
+ # We ignore this type of errors.
+ false
end
def related_notes
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 3056c20516a..5f9894f1168 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -99,11 +99,11 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>"
end
- def commit(ref = 'HEAD')
+ def commit(ref = nil)
return nil unless exists?
return ref if ref.is_a?(::Commit)
- find_commit(ref)
+ find_commit(ref || root_ref)
end
# Finding a commit by the passed SHA
@@ -283,6 +283,10 @@ class Repository
)
end
+ def cached_methods
+ CACHED_METHODS
+ end
+
def expire_tags_cache
expire_method_caches(%i(tag_names tag_count))
@tags = nil
@@ -423,7 +427,7 @@ class Repository
# Runs code after the HEAD of a repository is changed.
def after_change_head
- expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
+ expire_all_method_caches
end
# Runs code after a repository has been forked/imported.
diff --git a/app/models/user.rb b/app/models/user.rb
index 8e0dc91b2a7..48629c58490 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -244,7 +244,7 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal }
- scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
+ scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 3cdeb103bb8..18f2c1a509f 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -2,7 +2,7 @@
- breadcrumb_title "Dashboard"
%div{ class: container_class }
- = render_if_exists "admin/licenses/breakdown", license: @license
+ = render_if_exists 'admin/licenses/breakdown', license: @license
.admin-dashboard.prepend-top-default
.row
@@ -22,7 +22,7 @@
%h3.text-center
Users:
= approximate_count_with_delimiters(@counts, User)
- = render_if_exists 'users_statistics'
+ = render_if_exists 'admin/dashboard/users_statistics'
%hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -101,7 +101,7 @@
%span.light.float-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
- = render_if_exists 'elastic_and_geo'
+ = render_if_exists 'admin/dashboard/elastic_and_geo'
- container_reg = "Container Registry"
%p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
@@ -151,7 +151,7 @@
%span.float-right
= Gitlab::Pages::VERSION
- = render_if_exists 'geo'
+ = render_if_exists 'admin/dashboard/geo'
%p
Ruby
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index 231c0f70882..946d868da01 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -7,10 +7,10 @@
- values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] }
= f.select :provider, values, { allow_blank: false }, class: 'form-control'
.form-group.row
- = f.label :extern_uid, "Identifier", class: 'col-form-label col-sm-2'
+ = f.label :extern_uid, _("Identifier"), class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :extern_uid, class: 'form-control', required: true
.form-actions
- = f.submit 'Save changes', class: "btn btn-save"
+ = f.submit _('Save changes'), class: "btn btn-save"
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index 50fe9478a78..5ed59809db5 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -5,8 +5,8 @@
= identity.extern_uid
%td
= link_to edit_admin_user_identity_path(@user, identity), class: 'btn btn-sm btn-grouped' do
- Edit
+ = _("Edit")
= link_to [:admin, @user, identity], method: :delete,
class: 'btn btn-sm btn-danger',
- data: { confirm: "Are you sure you want to remove this identity?" } do
- Delete
+ data: { confirm: _("Are you sure you want to remove this identity?") } do
+ = _('Delete')
diff --git a/app/views/admin/identities/edit.html.haml b/app/views/admin/identities/edit.html.haml
index 515d46b0f29..1ad6ce969cb 100644
--- a/app/views/admin/identities/edit.html.haml
+++ b/app/views/admin/identities/edit.html.haml
@@ -1,6 +1,6 @@
-- page_title "Edit", @identity.provider, "Identities", @user.name, "Users"
+- page_title _("Edit"), @identity.provider, _("Identities"), @user.name, _("Users")
%h3.page-title
- Edit identity for #{@user.name}
+ = _('Edit identity for %{user_name}') % { user_name: @user.name }
%hr
= render 'form'
diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml
index ee51fb3fda1..59373ee6752 100644
--- a/app/views/admin/identities/index.html.haml
+++ b/app/views/admin/identities/index.html.haml
@@ -1,15 +1,15 @@
-- page_title "Identities", @user.name, "Users"
+- page_title _("Identities"), @user.name, _("Users")
= render 'admin/users/head'
-= link_to 'New identity', new_admin_user_identity_path, class: 'float-right btn btn-new'
+= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-new'
- if @identities.present?
.table-holder
%table.table
%thead
%tr
- %th Provider
- %th Identifier
+ %th= _('Provider')
+ %th= _('Identifier')
%th
= render @identities
- else
- %h4 This user has no identities
+ %h4= _('This user has no identities')
diff --git a/app/views/admin/identities/new.html.haml b/app/views/admin/identities/new.html.haml
index e30bf0ef0ee..ee743b0fd3c 100644
--- a/app/views/admin/identities/new.html.haml
+++ b/app/views/admin/identities/new.html.haml
@@ -1,4 +1,4 @@
-- page_title "New Identity"
-%h3.page-title New identity
+- page_title _("New Identity")
+%h3.page-title= _('New identity')
%hr
= render 'form'
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 6d9c6b5572a..28cdc7607e0 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -35,7 +35,7 @@
- @pre_auth.scopes.each do |scope|
%li
%strong= t scope, scope: [:doorkeeper, :scopes]
- .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
+ .text-secondary= t scope, scope: [:doorkeeper, :scope_desc]
.form-actions.text-right
= form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :client_id, @pre_auth.client.uid
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 8037cf4b69d..5e1ae1dbe38 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -9,7 +9,7 @@
= render 'shared/issuable/nav', type: :issues
.nav-controls
= render 'shared/issuable/feed_buttons'
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues'
= render 'shared/issuable/search_bar', type: :issues
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 4ccd16f3e11..e2a317dbf67 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -7,7 +7,7 @@
= render 'shared/issuable/nav', type: :merge_requests
- if current_user
.nav-controls
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests, with_feature_enabled: 'merge_requests'
= render 'shared/issuable/search_bar', type: :merge_requests
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index b2eacabc21a..f7a5d85500f 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -24,23 +24,28 @@
There are no commits yet.
= custom_icon ('illustration_no_commits')
- else
- %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom
- %li.commits-tab
- = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
- Commits
- %span.badge.badge-pill= @commits.size
- - if @pipelines.any?
- %li.builds-tab
- = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
- Pipelines
- %span.badge.badge-pill= @pipelines.size
- %li.diffs-tab
- = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
- Changes
- %span.badge.badge-pill= @merge_request.diff_size
+ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
+ .merge-request-tabs-container
+ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.js-tabs-affix
+ %li.commits-tab.new-tab
+ = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do
+ Commits
+ %span.badge.badge-pill= @commits.size
+ - if @pipelines.any?
+ %li.builds-tab
+ = link_to url_for(safe_params.merge(action: 'pipelines')), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tabvue'} do
+ Pipelines
+ %span.badge.badge-pill= @pipelines.size
+ %li.diffs-tab
+ = link_to url_for(safe_params.merge(action: 'diffs')), data: {target: 'div#diffs', action: 'diffs', toggle: 'tabvue'} do
+ Changes
+ %span.badge.badge-pill= @merge_request.diff_size
- .tab-content
- #commits.commits.tab-pane.active
+ #diff-notes-app.tab-content
+ #new.commits.tab-pane.active
= render "projects/merge_requests/commits"
#diffs.diffs.tab-pane
-# This tab is always loaded via AJAX
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 4359362bb05..31c2616d283 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -63,4 +63,4 @@
.form-text.text-muted
= s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
- = f.submit 'Save changes', class: "btn btn-success prepend-top-15"
+ = f.submit _('Save changes'), class: "btn btn-success prepend-top-15"
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 7142a9d635e..5025460a2d0 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -4,20 +4,20 @@
= form_errors(@project)
%fieldset.builds-feature
.form-group.append-bottom-default.js-secret-runner-token
- = f.label :runners_token, "Runner token", class: 'label-light'
+ = f.label :runners_token, _("Runner token"), class: 'label-light'
.form-control.js-secret-value-placeholder
= '*' * 20
= f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89'
- %p.form-text.text-muted The secure token used by the Runner to checkout the project
+ %p.form-text.text-muted= _("The secure token used by the Runner to checkout the project")
%button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } }
= _('Reveal value')
%hr
.form-group
%h5.prepend-top-0
- Git strategy for pipelines
+ = _("Git strategy for pipelines")
%p
- Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
+ = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
.form-check
= f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' }
@@ -25,29 +25,29 @@
%strong git clone
%br
%span.descr
- Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job
+ = _("Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job")
.form-check
= f.radio_button :build_allow_git_fetch, 'true', { class: 'form-check-input' }
= f.label :build_allow_git_fetch_true, class: 'form-check-label' do
%strong git fetch
%br
%span.descr
- Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)
+ = _("Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)")
%hr
.form-group
- = f.label :build_timeout_human_readable, 'Timeout', class: 'label-light'
+ = f.label :build_timeout_human_readable, _('Timeout'), class: 'label-light'
= f.text_field :build_timeout_human_readable, class: 'form-control'
%p.form-text.text-muted
- Per job. If a job passes this threshold, it will be marked as failed
+ = _("Per job. If a job passes this threshold, it will be marked as failed")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
.form-group
- = f.label :ci_config_path, 'Custom CI config path', class: 'label-light'
+ = f.label :ci_config_path, _('Custom CI config path'), class: 'label-light'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted
- The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>
+ = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr
@@ -55,36 +55,35 @@
.form-check
= f.check_box :public_builds, { class: 'form-check-input' }
= f.label :public_builds, class: 'form-check-label' do
- %strong Public pipelines
+ %strong= _("Public pipelines")
.form-text.text-muted
- Allow public access to pipelines and job details, including output logs and artifacts
+ = _("Allow public access to pipelines and job details, including output logs and artifacts")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
.bs-callout.bs-callout-info
- %p If enabled:
+ %p #{_("If enabled")}:
%ul
%li
- For public projects, anyone can view pipelines and access job details (output logs and artifacts)
+ = _("For public projects, anyone can view pipelines and access job details (output logs and artifacts)")
%li
- For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)
+ = _("For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)")
%li
- For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)
+ = _("For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)")
%p
- If disabled, the access level will depend on the user's
- permissions in the project.
+ = _("If disabled, the access level will depend on the user's permissions in the project.")
%hr
.form-group
.form-check
= f.check_box :auto_cancel_pending_pipelines, { class: 'form-check-input' }, 'enabled', 'disabled'
= f.label :auto_cancel_pending_pipelines, class: 'form-check-label' do
- %strong Auto-cancel redundant, pending pipelines
+ %strong= _("Auto-cancel redundant, pending pipelines")
.form-text.text-muted
- New pipelines will cancel older, pending pipelines on the same branch
+ = _("New pipelines will cancel older, pending pipelines on the same branch")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'auto-cancel-pending-pipelines'), target: '_blank'
%hr
.form-group
- = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
+ = f.label :build_coverage_regex, _("Test coverage parsing"), class: 'label-light'
.input-group
%span.input-group-prepend
.input-group-text /
@@ -92,11 +91,10 @@
%span.input-group-append
.input-group-text /
%p.form-text.text-muted
- A regular expression that will be used to find the test coverage
- output in the job trace. Leave blank to disable
+ = _("A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
.bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
+ %p= _("Below are examples of regex for existing tools:")
%ul
%li
Simplecov (Ruby) -
@@ -120,7 +118,7 @@
JaCoCo (Java/Kotlin)
%code Total.*?([0-9]{1,3})%
- = f.submit 'Save changes', class: "btn btn-save"
+ = f.submit _('Save changes'), class: "btn btn-save"
%hr
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 4d096699a70..be22bbd7a9b 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,6 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
-- page_title "CI / CD Settings"
-- page_title "CI / CD"
+- page_title _("CI / CD Settings")
+- page_title _("CI / CD")
- expanded = Rails.env.test?
- general_expanded = @project.errors.empty? ? expanded : true
@@ -8,11 +8,11 @@
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
.settings-header
%h4
- General pipelines
+ = _("General pipelines")
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
- Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
+ = _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.")
.settings-content
= render 'form'
@@ -31,11 +31,11 @@
%section.qa-runners-settings.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- Runners
+ = _("Runners")
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
- Register and see your runners for this project.
+ = _("Register and see your runners for this project.")
.settings-content
= render 'projects/runners/index'
@@ -45,7 +45,7 @@
= _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p.append-bottom-0
= render "ci/variables/content"
.settings-content
@@ -54,12 +54,10 @@
%section.settings.no-animate#js-pipeline-triggers{ class: ('expanded' if expanded) }
.settings-header
%h4
- Pipeline triggers
+ = _("Pipeline triggers")
%button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
+ = expanded ? _('Collapse') : _('Expand')
%p
- Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
- impersonate their associated user including their access to projects and their project
- permissions.
+ = _("Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will impersonate their associated user including their access to projects and their project permissions.")
.settings-content
= render 'projects/triggers/index'
diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index 77d88aed883..ef445f2e139 100644
--- a/app/views/projects/settings/integrations/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
@@ -8,9 +8,9 @@
%span.badge.badge-gray.deploy-project-label= event.to_s.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
%span.append-right-10.inline
- SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
- = link_to 'Edit', edit_project_hook_path(@project, hook), class: 'btn btn-sm'
+ #{_("SSL Verification")}: #{hook.enable_ssl_verification ? _('enabled') : _('disabled')}
+ = link_to _('Edit'), edit_project_hook_path(@project, hook), class: 'btn btn-sm'
= render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: hook, button_class: 'btn-sm'
- = link_to project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-transparent' do
- %span.sr-only Remove
+ = link_to project_hook_path(@project, hook), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn btn-transparent' do
+ %span.sr-only= _("Remove")
= icon('trash')
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index 2f1a548e119..76770290f36 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
-- breadcrumb_title "Integrations Settings"
-- page_title 'Integrations'
+- breadcrumb_title _("Integrations Settings")
+- page_title _('Integrations')
= render 'projects/hooks/index'
= render 'projects/services/index'
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
index ea2cd36b212..5fca734222b 100644
--- a/app/views/projects/settings/members/show.html.haml
+++ b/app/views/projects/settings/members/show.html.haml
@@ -1,5 +1,5 @@
- @content_class = "limit-container-width" unless fluid_layout
-- page_title "Members"
+- page_title _("Members")
= render "projects/project_members/index"
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 5dda2ec28b4..98c609d7bd4 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title "Repository Settings"
-- page_title "Repository"
+- breadcrumb_title _("Repository Settings")
+- page_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
= render "projects/mirrors/show"
diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml
index 5e9007aaaac..099e3ac8462 100644
--- a/app/views/shared/_milestone_expired.html.haml
+++ b/app/views/shared/_milestone_expired.html.haml
@@ -1,7 +1,6 @@
- if milestone.expired? and not milestone.closed?
- %span.cred (Expired)
+ .status-box.status-box-expired.append-bottom-5 Expired
- if milestone.upcoming?
- %span.clgray (Upcoming)
-- if milestone.due_date || milestone.start_date
- %span
- = milestone_date_range(milestone)
+ .status-box.status-box-mr-merged.append-bottom-5 Upcoming
+- if milestone.closed?
+ .status-box.status-box-closed.append-bottom-5 Closed
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index b8b1f4ca42f..28407b543b9 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -9,13 +9,17 @@
= form_errors(token)
- .form-group
- = f.label :name, class: 'label-light'
- = f.text_field :name, class: "form-control", required: true
+ .row
+ .form-group.col-md-6
+ = f.label :name, class: 'label-light'
+ = f.text_field :name, class: "form-control", required: true
- .form-group
- = f.label :expires_at, class: 'label-light'
- = f.text_field :expires_at, class: "datepicker form-control"
+ .row
+ .form-group.col-md-6
+ = f.label :expires_at, class: 'label-light'
+ .input-icon-wrapper
+ = f.text_field :expires_at, class: "datepicker form-control", placeholder: 'YYYY-MM-DD'
+ = icon('calendar', { class: 'input-icon-right' })
.form-group
= f.label :scopes, class: 'label-light'
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 65de6172d89..03e008f5fa0 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -32,7 +32,7 @@
"v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.float-right{ type: "button", title: _("Delete list"), "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
- .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' }
+ .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"' }
%span.issue-count-badge-count.float-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent)
@@ -43,8 +43,7 @@
"title" => _("New issue"),
data: { placement: "top", container: "body" } }
= icon("plus", class: "js-no-trigger-collapse")
-
- %board-list{ "v-if" => 'list.type !== "blank"',
+ %board-list{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"',
":list" => "list",
":issues" => "list.issues",
":loading" => "list.loading",
@@ -55,3 +54,4 @@
"ref" => "board-list" }
- if can?(current_user, :admin_list, current_board_parent)
%board-blank-state{ "v-if" => 'list.id == "blank"' }
+ = render_if_exists 'shared/boards/board_promotion_state'
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index 774dafe5f2c..1ff956649ed 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -8,6 +8,7 @@
{{ issue.title }}
%br/
%span
+ = render_if_exists "shared/boards/components/sidebar/issue_project_path"
= precede "#" do
{{ issue.iid }}
%a.gutter-toggle.float-right{ role: "button",
@@ -17,9 +18,11 @@
= custom_icon("icon_close", size: 15)
.js-issuable-update
= render "shared/boards/components/sidebar/assignee"
+ = render_if_exists "shared/boards/components/sidebar/epic"
= render "shared/boards/components/sidebar/milestone"
= render "shared/boards/components/sidebar/due_date"
= render "shared/boards/components/sidebar/labels"
+ = render_if_exists "shared/boards/components/sidebar/weight"
= render "shared/boards/components/sidebar/notifications"
%remove-btn{ ":issue" => "issue",
":issue-update" => "issue.sidebarInfoEndpoint",
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index c7c33288e9d..2e26fe63d3e 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,7 +16,7 @@
- if has_button
.text-center
- if project_select_button
- = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues
+ = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues, with_feature_enabled: 'issues'
- else
= link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link'
- else
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 014220761a9..186139f3526 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -15,7 +15,7 @@
= _("Interested parties can even contribute by pushing commits if they want to.")
.text-center
- if project_select_button
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests, with_feature_enabled: 'merge_requests'
- else
= link_to _('New merge request'), button_path, class: 'btn btn-new', title: _('New merge request'), id: 'new_merge_request_link'
- else
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index c35d0b3751f..e49bdec386a 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -6,7 +6,7 @@
%div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true,
- autocomplete: 'off', class: 'form-control pad qa-issuable-form-title'
+ autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title')
- if issuable.respond_to?(:work_in_progress?)
%p.form-text.text-muted
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 09bbd04c2bf..c559945a9c9 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -1,76 +1,59 @@
- dashboard = local_assigns[:dashboard]
- custom_dom_id = dom_id(milestone.try(:milestones) ? milestone.milestones.first : milestone)
+- milestone_type = milestone.group_milestone? ? 'Group Milestone' : 'Project Milestone'
%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
.row
.col-sm-6
- %strong= link_to truncate(milestone.title, length: 100), milestone_path
- - if milestone.group_milestone?
- %span - Group Milestone
- - else
- %span - Project Milestone
+ .append-bottom-5
+ %strong= link_to truncate(milestone.title, length: 100), milestone_path
+ - if @group
+ = " - #{milestone_type}"
- .col-sm-6
- .float-right.light #{milestone.percent_complete(current_user)}% complete
- .row
- .col-sm-6
+ - if @project || milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
+ - if milestone.due_date || milestone.start_date
+ .milestone-range.append-bottom-5
+ = milestone_date_range(milestone)
+ %div
+ = render('shared/milestone_expired', milestone: milestone)
+ - if milestone.legacy_group_milestone?
+ .projects
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label-badge.label-badge-blue.d-inline-block.append-bottom-5
+ = dashboard ? milestone.project.full_name : milestone.project.name
+
+ .col-sm-4.milestone-progress
+ = milestone_progress_bar(milestone)
= link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
- .col-sm-6= milestone_progress_bar(milestone)
- - if milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
- .row
- .col-sm-6
- - if milestone.legacy_group_milestone?
- .expiration= render('shared/milestone_expired', milestone: milestone)
- .projects
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.badge.badge-gray
- = dashboard ? milestone.project.full_name : milestone.project.name
- - if @group
- .col-sm-6.milestone-actions
+ .float-lg-right.light #{milestone.percent_complete(current_user)}% complete
+ .col-sm-2
+ .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
+ - if @project
+ - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
+ - if @project.group
+ %button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
+ disabled: true,
+ type: 'button',
+ data: { url: promote_project_milestone_path(milestone.project, milestone),
+ milestone_title: milestone.title,
+ group_name: @project.group.name,
+ target: '#promote-milestone-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = sprite_icon('level-up', size: 14)
+
+ = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped"
+ - unless milestone.active?
+ = link_to 'Reopen Milestone', project_milestone_path(@project, milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+ - if @group
- if can?(current_user, :admin_milestones, @group)
- - if milestone.group_milestone?
- = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-sm btn-grouped" do
- Edit
- \
- if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else
= link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
-
- - if @project
- .row
- .col-sm-6
- = render('shared/milestone_expired', milestone: milestone)
- .col-sm-6.milestone-actions
- - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-sm btn-grouped" do
- Edit
- \
-
- - if @project.group
- %button.js-promote-project-milestone-button.btn.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
- disabled: true,
- type: 'button',
- data: { url: promote_project_milestone_path(milestone.project, milestone),
- milestone_title: milestone.title,
- group_name: @project.group.name,
- target: '#promote-milestone-modal',
- container: 'body',
- toggle: 'modal' } }
- = _('Promote')
-
- = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped"
-
- %button.js-delete-milestone-button.btn.btn-sm.btn-grouped.btn-danger{ data: { toggle: 'modal',
- target: '#delete-milestone-modal',
- milestone_id: milestone.id,
- milestone_title: markdown_field(milestone, :title),
- milestone_url: project_milestone_path(milestone.project, milestone),
- milestone_issue_count: milestone.issues.count,
- milestone_merge_request_count: milestone.merge_requests.count },
- disabled: true }
- = _('Delete')
- = icon('spin spinner', class: 'js-loading-icon hidden' )
+ - if dashboard
+ .status-box.status-box-milestone
+ = milestone_type
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index e5c82962f82..dcb3fca23f2 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -3,7 +3,7 @@
- token = local_assigns.fetch(:token)
- scopes.each do |scope|
- %fieldset
- = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
- = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light"
- .scope-description= t scope, scope: [:doorkeeper, :scope_desc]
+ %fieldset.form-group.form-check
+ = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: 'form-check-input'
+ = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-light form-check-label'
+ .text-secondary= t scope, scope: [:doorkeeper, :scope_desc]
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index d06f51b1828..b8b854853b7 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -20,6 +20,7 @@
- cronjob:ci_archive_traces_cron
- cronjob:trending_projects
- cronjob:issue_due_scheduler
+- cronjob:prune_web_hook_logs
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_provision
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index b0e1d8837d9..abe86066fb4 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -3,6 +3,7 @@
# Worker for updating any project specific caches.
class ProjectCacheWorker
include ApplicationWorker
+ include ExclusiveLeaseGuard
LEASE_TIMEOUT = 15.minutes.to_i
@@ -13,30 +14,30 @@ class ProjectCacheWorker
# statistics - An Array containing columns from ProjectStatistics to
# refresh, if empty all columns will be refreshed
def perform(project_id, files = [], statistics = [])
- project = Project.find_by(id: project_id)
+ @project = Project.find_by(id: project_id)
+ return unless @project&.repository&.exists?
- return unless project && project.repository.exists?
+ update_statistics(statistics)
- update_statistics(project, statistics.map(&:to_sym))
+ @project.repository.refresh_method_caches(files.map(&:to_sym))
- project.repository.refresh_method_caches(files.map(&:to_sym))
-
- project.cleanup
+ @project.cleanup
end
- def update_statistics(project, statistics = [])
- return unless try_obtain_lease_for(project.id, :update_statistics)
-
- Rails.logger.info("Updating statistics for project #{project.id}")
+ private
- project.statistics.refresh!(only: statistics)
+ def update_statistics(statistics = [])
+ try_obtain_lease do
+ Rails.logger.info("Updating statistics for project #{@project.id}")
+ @project.statistics.refresh!(only: statistics.to_a.map(&:to_sym))
+ end
end
- private
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
- def try_obtain_lease_for(project_id, section)
- Gitlab::ExclusiveLease
- .new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT)
- .try_obtain
+ def lease_key
+ "project_cache_worker:#{@project.id}:update_statistics"
end
end
diff --git a/app/workers/prune_web_hook_logs_worker.rb b/app/workers/prune_web_hook_logs_worker.rb
new file mode 100644
index 00000000000..45c7d32f7eb
--- /dev/null
+++ b/app/workers/prune_web_hook_logs_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# Worker that deletes a fixed number of outdated rows from the "web_hook_logs"
+# table.
+class PruneWebHookLogsWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ # The maximum number of rows to remove in a single job.
+ DELETE_LIMIT = 50_000
+
+ def perform
+ # MySQL doesn't allow "DELETE FROM ... WHERE id IN ( ... )" if the inner
+ # query refers to the same table. To work around this we wrap the IN body in
+ # another sub query.
+ WebHookLog
+ .where(
+ 'id IN (SELECT id FROM (?) ids_to_remove)',
+ WebHookLog
+ .select(:id)
+ .where('created_at < ?', 90.days.ago.beginning_of_day)
+ .limit(DELETE_LIMIT)
+ )
+ .delete_all
+ end
+end
diff --git a/changelogs/unreleased/39543-milestone-page-list-redesign.yml b/changelogs/unreleased/39543-milestone-page-list-redesign.yml
new file mode 100644
index 00000000000..dcd73c5eddf
--- /dev/null
+++ b/changelogs/unreleased/39543-milestone-page-list-redesign.yml
@@ -0,0 +1,5 @@
+---
+title: Milestone page list redesign
+merge_request: 19832
+author: Constance Okoghenun
+type: changed
diff --git a/changelogs/unreleased/43270-import-with-milestones-failing.yml b/changelogs/unreleased/43270-import-with-milestones-failing.yml
new file mode 100644
index 00000000000..13bf8072376
--- /dev/null
+++ b/changelogs/unreleased/43270-import-with-milestones-failing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix label and milestone duplicated records and IID errors
+merge_request: 19961
+author:
+type: fixed
diff --git a/changelogs/unreleased/44725-expire_correct_methods_after_change_head.yml b/changelogs/unreleased/44725-expire_correct_methods_after_change_head.yml
new file mode 100644
index 00000000000..21a65f142c3
--- /dev/null
+++ b/changelogs/unreleased/44725-expire_correct_methods_after_change_head.yml
@@ -0,0 +1,5 @@
+---
+title: Expire correct method caches after HEAD changed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/44726-cancel_lease_upon_completion_in_project_cache_worker.yml b/changelogs/unreleased/44726-cancel_lease_upon_completion_in_project_cache_worker.yml
new file mode 100644
index 00000000000..bae6c2a8987
--- /dev/null
+++ b/changelogs/unreleased/44726-cancel_lease_upon_completion_in_project_cache_worker.yml
@@ -0,0 +1,5 @@
+---
+title: Cancel ExclusiveLease upon completion in ProjectCacheWorker
+merge_request: 20103
+author:
+type: fixed
diff --git a/changelogs/unreleased/47462-issues-disabled-group-page.yml b/changelogs/unreleased/47462-issues-disabled-group-page.yml
new file mode 100644
index 00000000000..c8cad608cb3
--- /dev/null
+++ b/changelogs/unreleased/47462-issues-disabled-group-page.yml
@@ -0,0 +1,6 @@
+---
+title: Only show new issue / new merge request on group page when issues / merge requests
+ are enabled
+merge_request: 19869
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/47516-pipe-scroll.yml b/changelogs/unreleased/47516-pipe-scroll.yml
new file mode 100644
index 00000000000..3e283f649bd
--- /dev/null
+++ b/changelogs/unreleased/47516-pipe-scroll.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent pipeline job tooltip from scrolling off dropdown container
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/47661-merge-request-box-disappearing-on-chrome.yml b/changelogs/unreleased/47661-merge-request-box-disappearing-on-chrome.yml
new file mode 100644
index 00000000000..7e6ab8d448b
--- /dev/null
+++ b/changelogs/unreleased/47661-merge-request-box-disappearing-on-chrome.yml
@@ -0,0 +1,5 @@
+---
+title: Replace deprecated bs.affix in merge request tabs with sticky polyfill
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/47769-fix_ambiguous_due_date_for_issue_scopes.yml b/changelogs/unreleased/47769-fix_ambiguous_due_date_for_issue_scopes.yml
new file mode 100644
index 00000000000..b8bb70b3266
--- /dev/null
+++ b/changelogs/unreleased/47769-fix_ambiguous_due_date_for_issue_scopes.yml
@@ -0,0 +1,5 @@
+---
+title: Fix ambiguous due_date column for Issue scopes
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/47865-changelog-for-style-updates.yml b/changelogs/unreleased/47865-changelog-for-style-updates.yml
new file mode 100644
index 00000000000..2e4fbbda000
--- /dev/null
+++ b/changelogs/unreleased/47865-changelog-for-style-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Minor style changes to personal access token form and scope checkboxes
+merge_request: 20052
+author:
+type: other
diff --git a/changelogs/unreleased/48603-merge-request-refactor-title-and-copy-to-clipboard-button-are-behind-the-action-buttons.yml b/changelogs/unreleased/48603-merge-request-refactor-title-and-copy-to-clipboard-button-are-behind-the-action-buttons.yml
new file mode 100644
index 00000000000..792c7814f7e
--- /dev/null
+++ b/changelogs/unreleased/48603-merge-request-refactor-title-and-copy-to-clipboard-button-are-behind-the-action-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Fix overlapping file title and file actions in MR changes tag
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/48653-mr-target-branch-missing.yml b/changelogs/unreleased/48653-mr-target-branch-missing.yml
new file mode 100644
index 00000000000..c2b342b87d2
--- /dev/null
+++ b/changelogs/unreleased/48653-mr-target-branch-missing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix merge request page rendering error when its target/source branch is missing
+merge_request: 20280
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-title-placeholder-for-new-issues.yml b/changelogs/unreleased/add-title-placeholder-for-new-issues.yml
new file mode 100644
index 00000000000..ce9e3b4ac18
--- /dev/null
+++ b/changelogs/unreleased/add-title-placeholder-for-new-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Add title placeholder for new issues
+merge_request: 20271
+author: George Tsiolis
+type: changed
diff --git a/changelogs/unreleased/backstage-gb-stages-position-migration-clean-up.yml b/changelogs/unreleased/backstage-gb-stages-position-migration-clean-up.yml
new file mode 100644
index 00000000000..d2ada88870b
--- /dev/null
+++ b/changelogs/unreleased/backstage-gb-stages-position-migration-clean-up.yml
@@ -0,0 +1,5 @@
+---
+title: Fully migrate pipeline stages position
+merge_request: 19369
+author:
+type: performance
diff --git a/changelogs/unreleased/bw-fix-ee-dashboard.yml b/changelogs/unreleased/bw-fix-ee-dashboard.yml
new file mode 100644
index 00000000000..667181cdf73
--- /dev/null
+++ b/changelogs/unreleased/bw-fix-ee-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Restore showing Elasticsearch and Geo status on dashboard
+merge_request: 20276
+author:
+type: fixed
diff --git a/changelogs/unreleased/cr-add-locked-state-to-MR.yml b/changelogs/unreleased/cr-add-locked-state-to-MR.yml
new file mode 100644
index 00000000000..f290ddc0b87
--- /dev/null
+++ b/changelogs/unreleased/cr-add-locked-state-to-MR.yml
@@ -0,0 +1,5 @@
+---
+title: Adds the `locked` state to the merge request API so that it can be used as a search filter.
+merge_request: 20186
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-user-without-projects-performance.yml b/changelogs/unreleased/dm-user-without-projects-performance.yml
new file mode 100644
index 00000000000..e7fc0ae6d54
--- /dev/null
+++ b/changelogs/unreleased/dm-user-without-projects-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of listing users without projects
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-last-commit-author-link-is-blue.yml b/changelogs/unreleased/fix-last-commit-author-link-is-blue.yml
new file mode 100644
index 00000000000..aaceeaecfb1
--- /dev/null
+++ b/changelogs/unreleased/fix-last-commit-author-link-is-blue.yml
@@ -0,0 +1,5 @@
+---
+title: Updated last commit link color
+merge_request: 20234
+author: Constance Okoghenun
+type: fixed
diff --git a/changelogs/unreleased/fix-tooltip-flicker.yml b/changelogs/unreleased/fix-tooltip-flicker.yml
new file mode 100644
index 00000000000..c94723d83df
--- /dev/null
+++ b/changelogs/unreleased/fix-tooltip-flicker.yml
@@ -0,0 +1,5 @@
+---
+title: Fix tooltip flickering bug
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-extra-line.yml b/changelogs/unreleased/jprovazn-extra-line.yml
new file mode 100644
index 00000000000..2628620f8ec
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-extra-line.yml
@@ -0,0 +1,5 @@
+---
+title: Don't show context button for diffs of deleted files.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/prune-web-hook-logs.yml b/changelogs/unreleased/prune-web-hook-logs.yml
new file mode 100644
index 00000000000..e8c805b2a92
--- /dev/null
+++ b/changelogs/unreleased/prune-web-hook-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Prune web hook logs older than 90 days
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/transfer_project_api_endpoint.yml b/changelogs/unreleased/transfer_project_api_endpoint.yml
new file mode 100644
index 00000000000..60c704c62a0
--- /dev/null
+++ b/changelogs/unreleased/transfer_project_api_endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Add transfer project API endpoint
+merge_request: 20122
+author: Aram Visser
+type: added
diff --git a/changelogs/unreleased/update-bcrypt-to-support-libxcrypt.yml b/changelogs/unreleased/update-bcrypt-to-support-libxcrypt.yml
new file mode 100644
index 00000000000..c18a0f75d22
--- /dev/null
+++ b/changelogs/unreleased/update-bcrypt-to-support-libxcrypt.yml
@@ -0,0 +1,5 @@
+---
+title: update bcrypt to also support libxcrypt
+merge_request: 20260
+author: muhammadn
+type: other
diff --git a/changelogs/unreleased/update-external-link-icon-in-merge-request-widget.yml b/changelogs/unreleased/update-external-link-icon-in-merge-request-widget.yml
new file mode 100644
index 00000000000..c650c32f884
--- /dev/null
+++ b/changelogs/unreleased/update-external-link-icon-in-merge-request-widget.yml
@@ -0,0 +1,5 @@
+---
+title: Update external link icon in merge request widget
+merge_request: 20154
+author: George Tsiolis
+type: changed
diff --git a/config/aws.yml.example b/config/aws.yml.example
deleted file mode 100644
index bb10c3cec7b..00000000000
--- a/config/aws.yml.example
+++ /dev/null
@@ -1,22 +0,0 @@
-# See https://github.com/jnicklas/carrierwave#using-amazon-s3
-# for more options
-# If you change this file in a Merge Request, please also create
-# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-production:
- access_key_id: AKIA1111111111111UA
- secret_access_key: secret
- bucket: mygitlab.production.us
- region: us-east-1
-
-development:
- access_key_id: AKIA1111111111111UA
- secret_access_key: secret
- bucket: mygitlab.development.us
- region: us-east-1
-
-test:
- access_key_id: AKIA1111111111111UA
- secret_access_key: secret
- bucket: mygitlab.test.us
- region: us-east-1
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 550647ae1c6..693a2934a1b 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -338,6 +338,10 @@ Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *'
Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker'
+Settings.cron_jobs['prune_web_hook_logs_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['prune_web_hook_logs_worker']['cron'] ||= '0 */1 * * *'
+Settings.cron_jobs['prune_web_hook_logs_worker']['job_class'] = 'PruneWebHookLogsWorker'
+
#
# Sidekiq
#
diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb
deleted file mode 100644
index 5cde6cbb0ff..00000000000
--- a/config/initializers/carrierwave.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/
-
-aws_file = Rails.root.join('config', 'aws.yml')
-
-if File.exist?(aws_file)
- AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env]
-
- CarrierWave.configure do |config|
- config.fog_provider = 'fog/aws'
-
- config.fog_credentials = {
- provider: 'AWS', # required
- aws_access_key_id: AWS_CONFIG['access_key_id'], # required
- aws_secret_access_key: AWS_CONFIG['secret_access_key'], # required
- region: AWS_CONFIG['region'], # optional, defaults to 'us-east-1'
- }
-
- # required
- config.fog_directory = AWS_CONFIG['bucket']
-
- # optional, defaults to true
- config.fog_public = false
-
- # optional, defaults to {}
- config.fog_attributes = { 'Cache-Control' => 'max-age=315576000' }
-
- # optional time (in seconds) that authenticated urls will be valid.
- # when fog_public is false and provider is AWS or Google, defaults to 600
- config.fog_authenticated_url_expiration = 1 << 29
- end
-end
diff --git a/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb b/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb
new file mode 100644
index 00000000000..73c23dffca0
--- /dev/null
+++ b/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb
@@ -0,0 +1,43 @@
+class CleanupStagesPositionMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ TMP_INDEX_NAME = 'tmp_id_stage_position_partial_null_index'.freeze
+
+ disable_ddl_transaction!
+
+ class Stages < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'ci_stages'
+ end
+
+ def up
+ disable_statement_timeout
+
+ Gitlab::BackgroundMigration.steal('MigrateStageIndex')
+
+ unless index_exists_by_name?(:ci_stages, TMP_INDEX_NAME)
+ add_concurrent_index(:ci_stages, :id, where: 'position IS NULL', name: TMP_INDEX_NAME)
+ end
+
+ migratable = <<~SQL
+ position IS NULL AND EXISTS (
+ SELECT 1 FROM ci_builds WHERE stage_id = ci_stages.id AND stage_idx IS NOT NULL
+ )
+ SQL
+
+ Stages.where(migratable).each_batch(of: 1000) do |batch|
+ batch.pluck(:id).each do |stage|
+ Gitlab::BackgroundMigration::MigrateStageIndex.new.perform(stage, stage)
+ end
+ end
+
+ remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME)
+ end
+
+ def down
+ if index_exists_by_name?(:ci_stages, TMP_INDEX_NAME)
+ remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME)
+ end
+ end
+end
diff --git a/doc/README.md b/doc/README.md
index fee920f2012..32924942497 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -228,7 +228,7 @@ straight away.
### GitLab self-hosted
-With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Core, Starter, Premium, and Ultimate.
+With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/pricing/): Core, Starter, Premium, and Ultimate.
Every feature available in Core is also available in Starter, Premium, and Ultimate.
Starter features are also available in Premium and Ultimate, and Premium features are also
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 0e65f9a9963..922cc45d8c4 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -11,7 +11,7 @@ Regular users don't have access to GitLab administration tools and settings.
GitLab has two product distributions: the open source
[GitLab Community Edition (CE)](https://gitlab.com/gitlab-org/gitlab-ce),
and the open core [GitLab Enterprise Edition (EE)](https://gitlab.com/gitlab-org/gitlab-ee),
-available through [different subscriptions](https://about.gitlab.com/products/).
+available through [different subscriptions](https://about.gitlab.com/pricing/).
You can [install GitLab CE or GitLab EE](https://about.gitlab.com/installation/ce-or-ee/),
but the features you'll have access to depend on the subscription you choose
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index e59ab5a72e1..10228d027e8 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -91,9 +91,9 @@ _The artifacts are stored by default in
- [Introduced][ee-1762] in [GitLab Premium][eep] 9.4.
- Since version 9.5, artifacts are [browsable], when object storage is enabled.
9.4 lacks this feature.
-> Available in [GitLab Premium](https://about.gitlab.com/products/) and
+> Available in [GitLab Premium](https://about.gitlab.com/pricing/) and
[GitLab.com Silver](https://about.gitlab.com/gitlab-com/).
-> Since version 10.6, available in [GitLab CE](https://about.gitlab.com/products/)
+> Since version 10.6, available in [GitLab CE](https://about.gitlab.com/pricing/)
> Since version 11.0, we support direct_upload to S3.
If you don't want to use the local disk where GitLab is installed to store the
@@ -123,6 +123,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `provider` | Always `AWS` for compatible hosts | AWS |
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
+| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
index a5cd2b642dc..f1c5b194f4c 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -1,15 +1,29 @@
# Job traces (logs)
-By default, all job traces (logs) are saved to `/var/opt/gitlab/gitlab-ci/builds`
-and `/home/git/gitlab/builds` for Omnibus packages and installations from source
-respectively. The job logs are organized by year and month (for example, `2017_03`),
-and then by project ID.
+Job traces are sent by GitLab Runner while it's processing a job. You can see
+traces in job pages, pipelines, email notifications, etc.
There isn't a way to automatically expire old job logs, but it's safe to remove
them if they're taking up too much space. If you remove the logs manually, the
job output in the UI will be empty.
-## Changing the job traces location
+## Data flow
+
+In general, there are two states in job traces: "live trace" and "archived trace".
+In the following table you can see the phases a trace goes through.
+
+| Phase | State | Condition | Data flow | Stored path |
+| ----- | ----- | --------- | --------- | ----------- |
+| 1: patching | Live trace | When a job is running | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
+| 2: overwriting | Live trace | When a job is finished | GitLab Runner => Unicorn => file storage |`#{ROOT_PATH}/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log`|
+| 3: archiving | Archived trace | After a job is finished | Sidekiq moves live trace to artifacts folder |`#{ROOT_PATH}/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/trace.log`|
+| 4: uploading | Archived trace | After a trace is archived | Sidekiq moves archived trace to [object storage](#uploading-traces-to-object-storage) (if configured) |`#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/trace.log`|
+
+The `ROOT_PATH` varies per your environment. For Omnibus GitLab it
+would be `/var/opt/gitlab/gitlab-ci`, whereas for installations from source
+it would be `/home/git/gitlab`.
+
+## Changing the job traces local location
To change the location where the job logs will be stored, follow the steps below.
@@ -41,97 +55,110 @@ To change the location where the job logs will be stored, follow the steps below
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
+## Uploading traces to object storage
+
+An archived trace is considered as a [job artifact](job_artifacts.md).
+Therefore, when you [set up an object storage](job_artifacts.md#object-storage-settings),
+job traces are automatically migrated to it along with the other job artifacts.
+
+See [Data flow](#data-flow) to learn about the process.
+
## New live trace architecture
> [Introduced][ce-18169] in GitLab 10.4.
+> [Announced as General availability][ce-46097] in GitLab 11.0.
-> **Notes**:
-- This feature is still Beta, which could impact GitLab.com/on-premises instances, and in the worst case scenario, traces will be lost.
-- This feature is still being discussed in [an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/46097) for the performance improvements.
-- This feature is off by default. Please check below how to enable/disable this featrue.
+NOTE: **Note:**
+This feature is off by default. Check below how to [enable/disable](#enabling-live-trace) it.
-**What is "live trace"?**
+By combining the process with object storage settings, we can completely bypass
+the local file storage. This is a useful option if GitLab is installed as
+cloud-native, for example on Kubernetes.
-Job trace that is sent by runner while jobs are running. You can see live trace in job pages UI.
-The live traces are archived once job finishes.
+The data flow is the same as described in the [data flow section](#data-flow)
+with one change: _the stored path of the first two phases is different_. This new live
+trace architecture stores chunks of traces in Redis and the database instead of
+file storage. Redis is used as first-class storage, and it stores up-to 128KB
+of data. Once the full chunk is sent, it is flushed to database. After a while,
+the data in Redis and database will be archived to [object storage](#uploading-traces-to-object-storage).
-**What is new architecture?**
+The data are stored in the following Redis namespace: `Gitlab::Redis::SharedState`.
-So far, when GitLab Runner sends a job trace to GitLab-Rails, traces have been saved to file storage as text files.
-This was a problem for [Cloud Native-compatible GitLab application](https://gitlab.com/gitlab-com/migration/issues/23) where GitLab had to rely on File Storage.
+Here is the detailed data flow:
-This new live trace architecture stores chunks of traces in Redis and database instead of file storage.
-Redis is used as first-class storage, and it stores up-to 128kB. Once the full chunk is sent it will be flushed to database. Afterwhile, the data in Redis and database will be archived to ObjectStorage.
+1. GitLab Runner picks a job from GitLab
+1. GitLab Runner sends a piece of trace to GitLab
+1. GitLab appends the data to Redis
+1. Once the data in Redis reach 128KB, the data is flushed to the database.
+1. The above steps are repeated until the job is finished.
+1. Once the job is finished, GitLab schedules a Sidekiq worker to archive the trace.
+1. The Sidekiq worker archives the trace to object storage and cleans up the trace
+ in Redis and the database.
-Here is the detailed data flow.
+### Enabling live trace
-1. GitLab Runner picks a job from GitLab-Rails
-1. GitLab Runner sends a piece of trace to GitLab-Rails
-1. GitLab-Rails appends the data to Redis
-1. If the data in Redis is fulfilled 128kB, the data is flushed to Database.
-1. 2.~4. is continued until the job is finished
-1. Once the job is finished, GitLab-Rails schedules a sidekiq worker to archive the trace
-1. The sidekiq worker archives the trace to Object Storage, and cleanup the trace in Redis and Database
+The following commands are to be issues in a Rails console:
-**How to check if it's on or off?**
+```sh
+# Omnibus GitLab
+gitlab-rails console
+
+# Installation from source
+cd /home/git/gitlab
+sudo -u git -H bin/rails console RAILS_ENV=production
+```
+
+**To check if live trace is enabled:**
```ruby
Feature.enabled?('ci_enable_live_trace')
```
-**How to enable?**
+**To enable live trace:**
```ruby
Feature.enable('ci_enable_live_trace')
```
->**Note:**
-The transition period will be handled gracefully. Upcoming traces will be generated with the new architecture, and on-going live traces will stay with the legacy architecture (i.e. on-going live traces won't be re-generated forcibly with the new architecture).
+NOTE: **Note:**
+The transition period will be handled gracefully. Upcoming traces will be
+generated with the new architecture, and on-going live traces will stay with the
+legacy architecture, which means that on-going live traces won't be forcibly
+re-generated with the new architecture.
-**How to disable?**
+**To disable live trace:**
```ruby
Feature.disable('ci_enable_live_trace')
```
->**Note:**
-The transition period will be handled gracefully. Upcoming traces will be generated with the legacy architecture, and on-going live traces will stay with the new architecture (i.e. on-going live traces won't be re-generated forcibly with the legacy architecture).
-
-**Redis namespace:**
-
-`Gitlab::Redis::SharedState`
-
-**Potential impact:**
+NOTE: **Note:**
+The transition period will be handled gracefully. Upcoming traces will be generated
+with the legacy architecture, and on-going live traces will stay with the new
+architecture, which means that on-going live traces won't be forcibly re-generated
+with the legacy architecture.
-- This feature could incur data loss:
- - Case 1: When all data in Redis are accidentally flushed.
- - On-going live traces could be recovered by re-sending traces (This is supported by all versions of GitLab Runner)
- - Finished jobs which has not archived live traces will lose the last part (~128kB) of trace data.
- - Case 2: When sidekiq workers failed to archive (e.g. There was a bug that prevents archiving process, Sidekiq inconsistancy, etc):
- - Currently all trace data in Redis will be deleted after one week. If the sidekiq workers can't finish by the expiry date, the part of trace data will be lost.
-- This feature could consume all memory on Redis instance. If the number of jobs is 1000, 128MB (128kB * 1000) is consumed.
-- This feature could pressure Database replication lag. `INSERT` are generated to indicate that we have trace chunk. `UPDATE` with 128kB of data is issued once we receive multiple chunks.
-- and so on
+### Potential implications
-**How to test?**
+In some cases, having data stored on Redis could incur data loss:
-We're currently evaluating this feature on dev.gitalb.org or staging.gitlab.com to verify this features. Here is the list of tests/measurements.
+1. **Case 1: When all data in Redis are accidentally flushed**
+ - On going live traces could be recovered by re-sending traces (this is
+ supported by all versions of the GitLab Runner).
+ - Finished jobs which have not archived live traces will lose the last part
+ (~128KB) of trace data.
-- Features:
- - Live traces should be visible on job pages
- - Archived traces should be visible on job pages
- - Live traces should be archived to Object storage
- - Live traces should be cleaned up after archived
- - etc
-- Performance:
- - Schedule 1000~10000 jobs and let GitLab-runners process concurrently. Measure memoery presssure, IO load, etc.
- - etc
-- Failover:
- - Simulate Redis outage
- - etc
+1. **Case 2: When Sidekiq workers fail to archive (e.g., there was a bug that
+ prevents archiving process, Sidekiq inconsistency, etc.)**
+ - Currently all trace data in Redis will be deleted after one week. If the
+ Sidekiq workers can't finish by the expiry date, the part of trace data will be lost.
-**How to verify the correctnesss?**
+Another issue that might arise is that it could consume all memory on the Redis
+instance. If the number of jobs is 1000, 128MB (128KB * 1000) is consumed.
-- TBD
+Also, it could pressure the database replication lag. `INSERT`s are generated to
+indicate that we have trace chunk. `UPDATE`s with 128KB of data is issued once we
+receive multiple chunks.
-[ce-18169]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169 \ No newline at end of file
+[ce-18169]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18169
+[ce-46097]: https://gitlab.com/gitlab-org/gitlab-ce/issues/46097
diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md
index 7f0bd8f04e3..6688181c5a8 100644
--- a/doc/administration/uploads.md
+++ b/doc/administration/uploads.md
@@ -79,6 +79,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `provider` | Always `AWS` for compatible hosts | AWS |
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
+| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
diff --git a/doc/api/groups.md b/doc/api/groups.md
index a48905f2f15..53d72509423 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -147,6 +147,8 @@ Parameters:
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `starred` | boolean | no | Limit by projects starred by the current user |
+| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
+| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
Example response:
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index da74045b702..2057ed3588a 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -11,7 +11,7 @@ default it returns only merge requests created by the current user. To
get all merge requests, use parameter `scope=all`.
The `state` parameter can be used to get only merge requests with a
-given state (`opened`, `closed`, or `merged`) or all of them (`all`).
+given state (`opened`, `closed`, `locked`, or `merged`) or all of them (`all`). It should be noted that when searching by `locked` it will mostly return no results as it is a short-lived, transitional state.
The pagination parameters `page` and `per_page` can be used to
restrict the list of merge requests.
@@ -35,7 +35,7 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
| `milestone` | string | no | Return merge requests for a specific milestone |
@@ -122,7 +122,7 @@ Parameters:
## List project merge requests
Get all merge requests for this project.
-The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
+The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, `locked`, or `merged`) or all of them (`all`).
The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
```
@@ -155,7 +155,7 @@ Parameters:
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | integer | yes | The ID of a project |
| `iids[]` | Array[integer] | no | Return the request having the given `iid` |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
| `milestone` | string | no | Return merge requests for a specific milestone |
@@ -243,7 +243,7 @@ Parameters:
## List group merge requests
Get all merge requests for this group and its subgroups.
-The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
+The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, `locked`, or `merged`) or all of them (`all`).
The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
```
@@ -262,7 +262,7 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `id` | integer | yes | The ID of a group |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, `locked`, or `merged` |
| `order_by` | string | no | Return merge requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` |
| `milestone` | string | no | Return merge requests for a specific milestone |
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 30a41839f28..b4599fdc97e 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1390,6 +1390,16 @@ POST /projects/:id/housekeeping
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+### Transfer a project to a new namespace
+
+```
+PUT /projects/:id/transfer
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `namespace` | integer/string | yes | The ID or path of the namespace to transfer to project to |
+
## Branches
Read more in the [Branches](branches.md) documentation.
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 07b144f6ddd..fbac37e688e 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -134,9 +134,20 @@ In order to do that, follow the steps:
```yaml
image: docker:stable
- # When using dind, it's wise to use the overlayfs driver for
- # improved performance.
variables:
+ # When using dind service we need to instruct docker, to talk with the
+ # daemon started inside of the service. The daemon is available with
+ # a network connection instead of the default /var/run/docker.sock socket.
+ #
+ # The 'docker' hostname is the alias of the service container as described at
+ # https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#accessing-the-services
+ #
+ # Note that if you're using Kubernetes executor, the variable should be set to
+ # tcp://localhost:2375 because of how Kubernetes executor connects services
+ # to the job container
+ DOCKER_HOST: tcp://docker:2375/
+ # When using dind, it's wise to use the overlayfs driver for
+ # improved performance.
DOCKER_DRIVER: overlay2
services:
@@ -293,6 +304,7 @@ services:
variables:
CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
+ DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
before_script:
@@ -391,6 +403,9 @@ could look like:
image: docker:stable
services:
- docker:dind
+ variables:
+ DOCKER_HOST: tcp://docker:2375
+ DOCKER_DRIVER: overlay2
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
@@ -410,6 +425,8 @@ services:
- docker:dind
variables:
+ DOCKER_HOST: tcp://docker:2375
+ DOCKER_DRIVER: overlay2
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
before_script:
@@ -445,6 +462,8 @@ stages:
- deploy
variables:
+ DOCKER_HOST: tcp://docker:2375
+ DOCKER_DRIVER: overlay2
CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_COMMIT_REF_NAME
CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index cc19e090964..2c8865c6e50 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -46,4 +46,4 @@ configuration to reflect that change.
[cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index 92ff90507ee..af87c83a4e5 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -63,4 +63,4 @@ are still maintained they have been deprecated with GitLab 11.0 and may be remov
in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
configuration to reflect that change.
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md
index a8720f0b7ea..ff20f0b3b5e 100644
--- a/doc/ci/examples/dast.md
+++ b/doc/ci/examples/dast.md
@@ -60,4 +60,4 @@ so, the CI job must be named `dast` and the artifact path must be
`gl-dast-report.json`.
[Learn more about DAST results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html).
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index c507036aa6a..c213b096a14 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -219,7 +219,7 @@ removed with one of the future versions of GitLab. You are advised to
[ee-2017]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2017
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
[variables]: ../variables/README.md
[predef]: ../variables/README.md#predefined-variables-environment-variables
[registry]: ../../user/project/container_registry.md
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index a3da6515a19..2f991d86614 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -553,7 +553,7 @@ Below you can find supported syntax reference:
`/pattern/i` to make a pattern case-insensitive.
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI variables"
-[eep]: https://about.gitlab.com/products/ "Available only in GitLab Premium"
+[eep]: https://about.gitlab.com/pricing/ "Available only in GitLab Premium"
[envs]: ../environments.md
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 31117b5e723..6ca3e9e5a0a 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -2,7 +2,7 @@
## Software delivery
-There are two software distributions of GitLab: the open source [Community Edition](https://gitlab.com/gitlab-org/gitlab-ce/) (CE), and the open core [Enterprise Edition](https://gitlab.com/gitlab-org/gitlab-ee/) (EE). GitLab is available under [different subscriptions](https://about.gitlab.com/products/).
+There are two software distributions of GitLab: the open source [Community Edition](https://gitlab.com/gitlab-org/gitlab-ce/) (CE), and the open core [Enterprise Edition](https://gitlab.com/gitlab-org/gitlab-ee/) (EE). GitLab is available under [different subscriptions](https://about.gitlab.com/pricing/).
New versions of GitLab are released in stable branches and the master branch is for bleeding edge development.
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 7f061d06da8..32de741c9fe 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -5,7 +5,7 @@
- **Write documentation.**: Add documentation to the `doc/` directory. Describe
the feature and include screenshots, if applicable.
- **Submit a MR to the `www-gitlab-com` project.**: Add the new feature to the
- [EE features list][ee-features-list].
+ [EE features list](https://about.gitlab.com/features/).
## Act as CE when unlicensed
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 4ba9958e2c6..f7d703b8f0b 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -174,6 +174,8 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript.
# => When size == 2: 'There are 2 mice.'
```
+ Avoid using `%d` or count variables in sigular strings. This allows more natural translation in some languages.
+
- In JavaScript:
```js
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
index 070efdc15b5..d5afa544372 100644
--- a/doc/development/ux_guide/copy.md
+++ b/doc/development/ux_guide/copy.md
@@ -192,7 +192,7 @@ Portions of this page are modifications based on work created and shared by the
[material design]: https://material.io/guidelines/
[features]: https://about.gitlab.com/features/ "GitLab features page"
-[products]: https://about.gitlab.com/products/ "GitLab products page"
+[products]: https://about.gitlab.com/pricing/ "GitLab products page"
[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia"
[android project]: http://source.android.com/
[creative commons]: http://creativecommons.org/licenses/by/2.5/
diff --git a/doc/install/installation.md b/doc/install/installation.md
index e4011b1a4ab..259d8f73a22 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -12,7 +12,7 @@ Since installations from source don't have Runit, Sidekiq can't be terminated an
## Select Version to Install
-Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-0-stable`).
+Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-1-stable`).
You can select the branch in the version dropdown in the top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear please check the [GitLab Blog](https://about.gitlab.com/blog/) for installation guide links by version.
@@ -300,9 +300,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-0-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-1-stable gitlab
-**Note:** You can change `11-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `11-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 852a58a9afc..9aee6b9dc74 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -71,7 +71,7 @@ For most installations, only two parameters are required:
Other common configuration options:
- `baseIP`: the desired [external IP address](#external-ip-recommended)
-- `gitlab`: Choose the [desired edition](https://about.gitlab.com/products), either `ee` or `ce`. `ce` is the default.
+- `gitlab`: Choose the [desired edition](https://about.gitlab.com/pricing), either `ee` or `ce`. `ce` is the default.
- `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart
- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/).
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 1f399a8a3f7..5531dcde4e9 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -64,16 +64,14 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim
### Memory
-You need at least 4GB of addressable memory (RAM + swap) to install and use GitLab!
+You need at least 8GB of addressable memory (RAM + swap) to install and use GitLab!
The operating system and any other running applications will also be using memory
so keep in mind that you need at least 4GB available before running GitLab. With
less memory GitLab will give strange errors during the reconfigure run and 500
errors during usage.
-- 1GB RAM + 3GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the [unicorn worker section below](#unicorn-workers) for more advice.
-- 2GB RAM + 2GB swap supports up to 100 users but it will be very slow
-- **4GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
-- 8GB RAM supports up to 1,000 users
+- 4GB RAM + 4GB swap supports up to 100 users but it will be very slow
+- **8GB RAM** is the **recommended** memory size for all installations and supports up to 100 users
- 16GB RAM supports up to 2,000 users
- 32GB RAM supports up to 4,000 users
- 64GB RAM supports up to 8,000 users
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 95221d8b6b1..f1881e0f767 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -195,6 +195,12 @@ This example can be used for a bucket in Amsterdam (AMS3).
1. [Reconfigure GitLab] for the changes to take effect
+CAUTION: **Warning:**
+If you see `400 Bad Request` by using Digital Ocean Spaces, the cause may be the
+usage of backup encryption. Remove or comment the line that
+contains `gitlab_rails['backup_encryption']` since Digital Ocean Spaces
+doesn't support encryption.
+
#### Other S3 Providers
Not all S3 providers are fully-compatible with the Fog library. For example,
@@ -326,6 +332,16 @@ For installations from source:
1. [Restart GitLab] for the changes to take effect
+#### Specifying a custom directory for backups
+
+Note: This option only works for remote storage. If you want to group your backups
+you can pass a `DIRECTORY` environment variable:
+
+```
+sudo gitlab-rake gitlab:backup:create DIRECTORY=daily
+sudo gitlab-rake gitlab:backup:create DIRECTORY=weekly
+```
+
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
@@ -369,15 +385,6 @@ For installations from source:
remote_directory: 'gitlab_backups'
```
-### Specifying a custom directory for backups
-
-If you want to group your backups you can pass a `DIRECTORY` environment variable:
-
-```
-sudo gitlab-rake gitlab:backup:create DIRECTORY=daily
-sudo gitlab-rake gitlab:backup:create DIRECTORY=weekly
-```
-
### Backup archive permissions
The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 1d26a743500..bb323705ab3 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -91,7 +91,7 @@ To make full use of Auto DevOps, you will need:
for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner)
that are assigned to specific projects.
1. **Base domain** (needed for Auto Review Apps and Auto Deploy) - You will need
- a domain configured with wildcard DNS which is gonna be used by all of your
+ a domain configured with wildcard DNS which is going to be used by all of your
Auto DevOps applications. [Read the specifics](#auto-devops-base-domain).
1. **Kubernetes** (needed for Auto Review Apps, Auto Deploy, and Auto Monitoring) -
To enable deployments, you will need Kubernetes 1.5+. You need a [Kubernetes cluster][kubernetes-clusters]
@@ -840,5 +840,5 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/
[postgresql]: https://www.postgresql.org/
[Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml
[GitLab Omnibus Helm Chart]: ../../install/kubernetes/gitlab_omnibus.md
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
[ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507
diff --git a/doc/update/11.0-to-11.1.md b/doc/update/11.0-to-11.1.md
new file mode 100644
index 00000000000..306bd417ebf
--- /dev/null
+++ b/doc/update/11.0-to-11.1.md
@@ -0,0 +1,361 @@
+---
+comments: false
+---
+
+# From 11.0 to 11.1
+
+Make sure you view this update guide from the branch (version) of GitLab you would
+like to install (e.g., `11-1-stable`. You can select the branch in the version
+dropdown at the top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+NOTE: If you installed GitLab from source, make sure `rsync` is installed.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download Ruby and compile it:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz
+echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz
+cd ruby-2.4.4
+
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets.
+This requires a minimum version of node v6.0.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v6.0.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript
+dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 11.0 and higher only supports Go 1.9.x and newer, and dropped support for Go
+1.5.x through 1.8.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
+echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.10.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all --prune
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-1-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 11-1-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags --prune
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-0-stable:config/gitlab.yml.example origin/11-1-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/11-0-stable:lib/support/nginx/gitlab-ssl origin/11-1-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/11-0-stable:lib/support/nginx/gitlab origin/11-1-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-1-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-1-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/11-0-stable:lib/support/init.d/gitlab.default.example origin/11-1-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Compile GetText PO files
+
+sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (11.0)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 10.8 to 11.0](10.8-to-11.0.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-1-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-1-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/user/index.md b/doc/user/index.md
index edf50019c2f..90f0e2285c3 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -7,7 +7,7 @@ description: 'Read through the GitLab User documentation to learn how to use, co
Welcome to GitLab! We're glad to have you here!
As a GitLab user you'll have access to all the features
-your [subscription](https://about.gitlab.com/products/)
+your [subscription](https://about.gitlab.com/pricing/)
includes, except [GitLab administrator](../README.md#administrator-documentation)
settings, unless you have admin privileges to install, configure,
and upgrade your GitLab instance.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index a35bf48e62d..b6438397db8 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -313,4 +313,4 @@ Read through the documentation on [LDAP users permissions](https://docs.gitlab.c
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
[ee-998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998
-[eep]: https://about.gitlab.com/products/
+[eep]: https://about.gitlab.com/pricing/
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 20c46cafbe5..b25b09f7b1f 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -406,5 +406,5 @@ the deployment variables above, ensuring any pods you create are labelled with
- [Connecting and deploying to an Amazon EKS cluster](eks_and_gitlab/index.md)
[permissions]: ../../permissions.md
-[ee]: https://about.gitlab.com/products/
+[ee]: https://about.gitlab.com/pricing/
[Auto DevOps]: ../../../topics/autodevops/index.md
diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md
index 1e28646bc97..cffe456cbc2 100644
--- a/doc/user/project/integrations/bamboo.md
+++ b/doc/user/project/integrations/bamboo.md
@@ -41,8 +41,7 @@ service in GitLab.
1. Click 'Atlassian Bamboo CI'
1. Select the 'Active' checkbox.
1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
-1. Enter the build key from your Bamboo build plan. Build keys are a short,
- all capital letter, identifier that is unique. It will be something like PR-BLD
+1. Enter the build key from your Bamboo build plan. Build keys are typically made up from the Project Key and Plan Key that are set on project/plan creation and seperated with a '-' for example **PROJ-PLAN**. This is a short, all capital letter, identifier that is unique. When viewing a plan within Bamboo, the build key is also shown in the browser URL for example https://bamboo.example.com/browse/PROJ-PLAN
1. If necessary, enter username and password for a Bamboo user that has
access to trigger the build plan. Leave these fields blank if you do not require
authentication.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 19df78f4140..8c09927e2df 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -6,6 +6,10 @@ Starting from GitLab 8.5:
- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key
- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
+>**Note:**
+Starting from GitLab 11.1, the logs of web hooks are automatically removed after
+one month.
+
Project webhooks allow you to trigger a URL if for example new code is pushed or
a new issue is created. You can configure webhooks to listen for specific events
like pushes, issues or merge requests. GitLab will send a POST request with data
@@ -54,11 +58,11 @@ Below are described the supported events.
Triggered when you push to the repository except when pushing tags.
-> **Note:** When more than 20 commits are pushed at once, the `commits` web hook
- attribute will only contain the first 20 for performance reasons. Loading
- detailed commit data is expensive. Note that despite only 20 commits being
+> **Note:** When more than 20 commits are pushed at once, the `commits` web hook
+ attribute will only contain the first 20 for performance reasons. Loading
+ detailed commit data is expensive. Note that despite only 20 commits being
present in the `commits` attribute, the `total_commits_count` attribute will
- contain the actual total.
+ contain the actual total.
**Request header**:
@@ -1149,11 +1153,11 @@ From this page, you can repeat delivery with the same data by clicking `Resend R
When GitLab sends a webhook it expects a response in 10 seconds (set default value). If it does not receive one, it'll retry the webhook.
If the endpoint doesn't send its HTTP response within those 10 seconds, GitLab may decide the hook failed and retry it.
-If you are receiving multiple requests, you can try increasing the default value to wait for the HTTP response after sending the webhook
+If you are receiving multiple requests, you can try increasing the default value to wait for the HTTP response after sending the webhook
by uncommenting or adding the following setting to your `/etc/gitlab/gitlab.rb`:
```
-gitlab_rails['webhook_timeout'] = 10
+gitlab_rails['webhook_timeout'] = 10
```
## Example webhook receiver
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 10647e33f4c..e97b5d05529 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -70,7 +70,7 @@ beginning of the development lifecycle until deployed to production
### Use cases for Multiple Issue Boards
With [Multiple Issue Boards](#multiple-issue-boards), available only in
-[GitLab Enterprise Edition](https://about.gitlab.com/products/),
+[GitLab Enterprise Edition](https://about.gitlab.com/pricing/),
each team can have their own board to organize their workflow individually.
#### Scrum team
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index bf17731c523..d71273ba970 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -8,7 +8,7 @@ It allows you, your team, and your collaborators to share
and discuss proposals before and while implementing them.
GitLab Issues and the GitLab Issue Tracker are available in all
-[GitLab Products](https://about.gitlab.com/products/) as
+[GitLab Products](https://about.gitlab.com/pricing/) as
part of the [GitLab Workflow](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
## Use cases
@@ -35,7 +35,7 @@ your project public, open to collaboration.
### Streamline collaboration
With [Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html),
-available in [GitLab Starter](https://about.gitlab.com/products/)
+available in [GitLab Starter](https://about.gitlab.com/pricing/)
you can streamline collaboration and allow shared responsibilities to be clearly displayed.
All assignees are shown across your workflows and receive notifications (as they
would as single assignees), simplifying communication and ownership.
@@ -139,7 +139,7 @@ Find GitLab Issue Boards by navigating to your **Project's Dashboard** > **Issue
Read through the documentation for [Issue Boards](../issue_board.md)
to find out more about this feature.
-With [GitLab Starter](https://about.gitlab.com/products/), you can also
+With [GitLab Starter](https://about.gitlab.com/pricing/), you can also
create various boards per project with [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards).
### External Issue Tracker
diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md
index e9903b01c82..46f25417fde 100644
--- a/doc/user/project/issues/issues_functionalities.md
+++ b/doc/user/project/issues/issues_functionalities.md
@@ -47,7 +47,7 @@ Often multiple people likely work on the same issue together,
which can especially be difficult to track in large teams
where there is shared ownership of an issue.
-In [GitLab Starter](https://about.gitlab.com/products/), you can also
+In [GitLab Starter](https://about.gitlab.com/pricing/), you can also
select multiple assignees to an issue.
Learn more on the [Multiple Assignees documentation](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html).
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 5e2e0c3d171..483a54051d7 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -325,4 +325,4 @@ git checkout origin/merge-requests/1
```
[protected branches]: ../protected_branches.md
-[ee]: https://about.gitlab.com/products/ "GitLab Enterprise Edition"
+[ee]: https://about.gitlab.com/pricing/ "GitLab Enterprise Edition"
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index bda293bd00e..704c1777e62 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -82,7 +82,7 @@ your implementation with your team.
You can live preview changes submitted to a new branch with
[Review Apps](../../../ci/review_apps/index.md).
-With [GitLab Starter](https://about.gitlab.com/products/), you can also request
+With [GitLab Starter](https://about.gitlab.com/pricing/), you can also request
[approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers.
To create, delete, and [branches](branches/index.md) via GitLab's UI:
@@ -165,12 +165,12 @@ Find it under your project's **Repository > Compare**.
## Locked files
-> Available in [GitLab Premium](https://about.gitlab.com/products/).
+> Available in [GitLab Premium](https://about.gitlab.com/pricing/).
Lock your files to prevent any conflicting changes.
[File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html) is available only in
-[GitLab Premium](https://about.gitlab.com/products/).
+[GitLab Premium](https://about.gitlab.com/pricing/).
## Repository's API
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 9034a9b5179..f94671fcf87 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -32,7 +32,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.8 to current | 0.2.3 |
+| 11.1 to current | 0.2.4 |
+| 10.8 | 0.2.3 |
| 10.4 | 0.2.2 |
| 10.3 | 0.2.1 |
| 10.0 | 0.2.0 |
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 212e271ce6f..084d1161633 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -42,7 +42,7 @@ Set up your project's merge request settings:
### Service Desk
-Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) for your project to offer customer support. Service Desk is available in [GitLab Premium](https://about.gitlab.com/products/).
+Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) for your project to offer customer support. Service Desk is available in [GitLab Premium](https://about.gitlab.com/pricing/).
### Export project
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 8a2f230f505..6ac3bb8c0b4 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -90,6 +90,7 @@ Here is a configuration example with S3.
| `provider` | The provider name | AWS |
| `aws_access_key_id` | AWS credentials, or compatible | `ABC123DEF456` |
| `aws_secret_access_key` | AWS credentials, or compatible | `ABC123DEF456ABC123DEF456ABC123DEF456` |
+| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
@@ -235,5 +236,5 @@ See more information in [!19581](https://gitlab.com/gitlab-org/gitlab-ce/merge_r
[reconfigure gitlab]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
[restart gitlab]: ../../administration/restart_gitlab.md#installations-from-source "How to restart GitLab"
-[eep]: https://about.gitlab.com/products/ "GitLab Premium"
+[eep]: https://about.gitlab.com/pricing/ "GitLab Premium"
[ee-2760]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2760
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index c7f41aba854..f633dd88d06 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -56,6 +56,8 @@ module API
def find_group_projects(params)
group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group: group, current_user: current_user, params: project_finder_params).execute
+ projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
+ projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = reorder_projects(projects)
paginate(projects)
end
@@ -191,6 +193,8 @@ module API
desc: 'Return only the ID, URL, name, and path of each project'
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+ optional :with_issues_enabled, type: Boolean, default: false, desc: 'Limit by enabled issues feature'
+ optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature'
use :pagination
use :with_custom_attributes
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index af7d2471b34..0f46bc4c98e 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -72,8 +72,8 @@ module API
end
params :merge_requests_params do
- optional :state, type: String, values: %w[opened closed merged all], default: 'all',
- desc: 'Return opened, closed, merged, or all merge requests'
+ optional :state, type: String, values: %w[opened closed locked merged all], default: 'all',
+ desc: 'Return opened, closed, locked, merged, or all merge requests'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 3ef3680c5d9..b83da00502d 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -459,6 +459,23 @@ module API
conflict!(error.message)
end
end
+
+ desc 'Transfer a project to a new namespace'
+ params do
+ requires :namespace, type: String, desc: 'The ID or path of the new namespace'
+ end
+ put ":id/transfer" do
+ authorize! :change_namespace, user_project
+
+ namespace = find_namespace!(params[:namespace])
+ result = ::Projects::TransferService.new(user_project, current_user).execute(namespace)
+
+ if result
+ present user_project, with: Entities::Project
+ else
+ render_api_error!("Failed to transfer project #{user_project.errors.messages}", 400)
+ end
+ end
end
end
end
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index e1261e7bbbe..4eccd9d5ed5 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -3,10 +3,6 @@ module Banzai
# HTML filter that replaces :emoji: and unicode with images.
#
# Based on HTML::Pipeline::EmojiFilter
- #
- # Context options:
- # :asset_root
- # :asset_host
class EmojiFilter < HTML::Pipeline::Filter
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 40bcfa20e7d..a9e209d5b71 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -250,7 +250,7 @@ module Gitlab
last_line = lines.last
- if last_line.new_pos < total_blob_lines(blob)
+ if last_line.new_pos < total_blob_lines(blob) && !deleted_file?
match_line = Gitlab::Diff::Line.new("", 'match', nil, last_line.old_pos, last_line.new_pos)
lines.push(match_line)
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index b713fa7e1cd..53fe2f8e436 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.2.3'.freeze
+ VERSION = '0.2.4'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/group_project_object_builder.rb b/lib/gitlab/import_export/group_project_object_builder.rb
new file mode 100644
index 00000000000..6c2af770119
--- /dev/null
+++ b/lib/gitlab/import_export/group_project_object_builder.rb
@@ -0,0 +1,90 @@
+module Gitlab
+ module ImportExport
+ # Given a class, it finds or creates a new object
+ # (initializes in the case of Label) at group or project level.
+ # If it does not exist in the group, it creates it at project level.
+ #
+ # Example:
+ # `GroupProjectObjectBuilder.build(Label, label_attributes)`
+ # finds or initializes a label with the given attributes.
+ #
+ # It also adds some logic around Group Labels/Milestones for edge cases.
+ class GroupProjectObjectBuilder
+ def self.build(*args)
+ Project.transaction do
+ new(*args).find
+ end
+ end
+
+ def initialize(klass, attributes)
+ @klass = klass < Label ? Label : klass
+ @attributes = attributes
+ @group = @attributes['group']
+ @project = @attributes['project']
+ end
+
+ def find
+ find_object || @klass.create(project_attributes)
+ end
+
+ private
+
+ def find_object
+ @klass.where(where_clause).first
+ end
+
+ def where_clause
+ @attributes.slice('title').map do |key, value|
+ scope_clause = table[:project_id].eq(@project.id)
+ scope_clause = scope_clause.or(table[:group_id].eq(@group.id)) if @group
+
+ table[key].eq(value).and(scope_clause)
+ end.reduce(:or)
+ end
+
+ def table
+ @table ||= @klass.arel_table
+ end
+
+ def project_attributes
+ @attributes.except('group').tap do |atts|
+ if label?
+ atts['type'] = 'ProjectLabel' # Always create project labels
+ elsif milestone?
+ if atts['group_id'] # Transform new group milestones into project ones
+ atts['iid'] = nil
+ atts.delete('group_id')
+ else
+ claim_iid
+ end
+ end
+ end
+ end
+
+ def label?
+ @klass == Label
+ end
+
+ def milestone?
+ @klass == Milestone
+ end
+
+ # If an existing group milestone used the IID
+ # claim the IID back and set the group milestone to use one available
+ # This is necessary to fix situations like the following:
+ # - Importing into a user namespace project with exported group milestones
+ # where the IID of the Group milestone could conflict with a project one.
+ def claim_iid
+ # The milestone has to be a group milestone, as it's the only case where
+ # we set the IID as the maximum. The rest of them are fixed.
+ milestone = @project.milestones.find_by(iid: @attributes['iid'])
+
+ return unless milestone
+
+ milestone.iid = nil
+ milestone.ensure_project_iid!
+ milestone.save!
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 4eb67fbe11e..76b99b1de16 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -1,8 +1,8 @@
module Gitlab
module ImportExport
class ProjectTreeRestorer
- # Relations which cannot have both group_id and project_id at the same time
- RESTRICT_PROJECT_AND_GROUP = %i(milestone milestones).freeze
+ # Relations which cannot be saved at project level (and have a group assigned)
+ GROUP_MODELS = [GroupLabel, Milestone].freeze
def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json')
@@ -70,12 +70,23 @@ module Gitlab
def save_relation_hash(relation_hash_batch, relation_key)
relation_hash = create_relation(relation_key, relation_hash_batch)
+ remove_group_models(relation_hash) if relation_hash.is_a?(Array)
+
@saved = false unless restored_project.append_or_update_attribute(relation_key, relation_hash)
# Restore the project again, extra query that skips holding the AR objects in memory
@restored_project = Project.find(@project_id)
end
+ # Remove project models that became group models as we found them at group level.
+ # This no longer required saving them at the root project level.
+ # For example, in the case of an existing group label that matched the title.
+ def remove_group_models(relation_hash)
+ relation_hash.reject! do |value|
+ GROUP_MODELS.include?(value.class) && value.group_id
+ end
+ end
+
def default_relation_list
reader.tree.reject do |model|
model.is_a?(Hash) && model[:project_members]
@@ -170,7 +181,7 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
- relation_hash: parsed_relation_hash(relation_hash, relation.to_sym),
+ relation_hash: relation_hash,
members_mapper: members_mapper,
user: @user,
project: @restored_project,
@@ -180,18 +191,6 @@ module Gitlab
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end
- def parsed_relation_hash(relation_hash, relation_type)
- if RESTRICT_PROJECT_AND_GROUP.include?(relation_type)
- params = {}
- params['group_id'] = restored_project.group.try(:id) if relation_hash['group_id']
- params['project_id'] = restored_project.id if relation_hash['project_id']
- else
- params = { 'group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id }
- end
-
- relation_hash.merge(params)
- end
-
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index c5cf290f191..091e81028bb 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -54,6 +54,8 @@ module Gitlab
@project = project
@imported_object_retries = 0
+ @relation_hash['project_id'] = @project.id
+
# Remove excluded keys from relation_hash
# We don't do this in the parsed_relation_hash because of the 'transformed attributes'
# For example, MergeRequestDiffFiles exports its diff attribute as utf8_diff. Then,
@@ -80,15 +82,12 @@ module Gitlab
case @relation_name
when :merge_request_diff_files then setup_diff
when :notes then setup_note
- when :project_label, :project_labels then setup_label
- when :milestone, :milestones then setup_milestone
when 'Ci::Pipeline' then setup_pipeline
- else
- @relation_hash['project_id'] = @project.id
end
update_user_references
update_project_references
+ update_group_references
remove_duplicate_assignees
reset_tokens!
@@ -151,39 +150,23 @@ module Gitlab
end
def update_project_references
- project_id = @relation_hash.delete('project_id')
-
# If source and target are the same, populate them with the new project ID.
if @relation_hash['source_project_id']
- @relation_hash['source_project_id'] = same_source_and_target? ? project_id : MergeRequestParser::FORKED_PROJECT_ID
+ @relation_hash['source_project_id'] = same_source_and_target? ? @relation_hash['project_id'] : MergeRequestParser::FORKED_PROJECT_ID
end
- # project_id may not be part of the export, but we always need to populate it if required.
- @relation_hash['project_id'] = project_id
- @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
+ @relation_hash['target_project_id'] = @relation_hash['project_id'] if @relation_hash['target_project_id']
end
def same_source_and_target?
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
- def setup_label
- # If there's no group, move the label to a project label
- if @relation_hash['type'] == 'GroupLabel' && @relation_hash['group_id']
- @relation_hash['project_id'] = nil
- @relation_name = :group_label
- else
- @relation_hash['group_id'] = nil
- @relation_hash['type'] = 'ProjectLabel'
- end
- end
+ def update_group_references
+ return unless EXISTING_OBJECT_CHECK.include?(@relation_name)
+ return unless @relation_hash['group_id']
- def setup_milestone
- if @relation_hash['group_id']
- @relation_hash['group_id'] = @project.group.id
- else
- @relation_hash['project_id'] = @project.id
- end
+ @relation_hash['group_id'] = @project.group&.id
end
def reset_tokens!
@@ -271,15 +254,7 @@ module Gitlab
end
def existing_object
- @existing_object ||=
- begin
- existing_object = find_or_create_object!
-
- # Done in two steps, as MySQL behaves differently than PostgreSQL using
- # the +find_or_create_by+ method and does not return the ID the second time.
- existing_object.update!(parsed_relation_hash)
- existing_object
- end
+ @existing_object ||= find_or_create_object!
end
def unknown_service?
@@ -288,29 +263,16 @@ module Gitlab
end
def find_or_create_object!
- finder_attributes = if @relation_name == :group_label
- %w[title group_id]
- elsif parsed_relation_hash['project_id']
- %w[title project_id]
- else
- %w[title group_id]
- end
-
- finder_hash = parsed_relation_hash.slice(*finder_attributes)
-
- if label?
- label = relation_class.find_or_initialize_by(finder_hash)
- parsed_relation_hash.delete('priorities') if label.persisted?
-
- label.save!
- label
- else
- relation_class.find_or_create_by(finder_hash)
+ return relation_class.find_or_create_by(project_id: @project.id) if @relation_name == :project_feature
+
+ # Can't use IDs as validation exists calling `group` or `project` attributes
+ finder_hash = parsed_relation_hash.tap do |hash|
+ hash['group'] = @project.group if relation_class.attribute_method?('group_id')
+ hash['project'] = @project
+ hash.delete('project_id')
end
- end
- def label?
- @relation_name.to_s.include?('label')
+ GroupProjectObjectBuilder.build(relation_class, finder_hash)
end
end
end
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
index 45b644e6510..4a99b7cca5c 100644
--- a/lib/gitlab/middleware/read_only/controller.rb
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -4,8 +4,18 @@ module Gitlab
class Controller
DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
APPLICATION_JSON = 'application/json'.freeze
+ APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'.freeze
+ WHITELISTED_GIT_ROUTES = {
+ 'projects/git_http' => %w{git_upload_pack git_receive_pack}
+ }.freeze
+
+ WHITELISTED_GIT_LFS_ROUTES = {
+ 'projects/lfs_api' => %w{batch},
+ 'projects/lfs_locks_api' => %w{verify create unlock}
+ }.freeze
+
def initialize(app, env)
@app = app
@env = env
@@ -36,7 +46,7 @@ module Gitlab
end
def json_request?
- request.media_type == APPLICATION_JSON
+ APPLICATION_JSON_TYPES.include?(request.media_type)
end
def rack_flash
@@ -63,22 +73,27 @@ module Gitlab
grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
end
- def sidekiq_route
- request.path.start_with?('/admin/sidekiq')
- end
-
def grack_route
# Calling route_hash may be expensive. Only do it if we think there's a possible match
- return false unless request.path.end_with?('.git/git-upload-pack')
+ return false unless
+ request.path.end_with?('.git/git-upload-pack', '.git/git-receive-pack')
- route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack'
+ WHITELISTED_GIT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end
def lfs_route
# Calling route_hash may be expensive. Only do it if we think there's a possible match
- return false unless request.path.end_with?('/info/lfs/objects/batch')
+ unless request.path.end_with?('/info/lfs/objects/batch',
+ '/info/lfs/locks', '/info/lfs/locks/verify') ||
+ %r{/info/lfs/locks/\d+/unlock\z}.match?(request.path)
+ return false
+ end
+
+ WHITELISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
+ end
- route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch'
+ def sidekiq_route
+ request.path.start_with?('/admin/sidekiq')
end
end
end
diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb
index 7f64a8c9e46..2ec871f0754 100644
--- a/lib/gitlab/repository_cache_adapter.rb
+++ b/lib/gitlab/repository_cache_adapter.rb
@@ -25,6 +25,11 @@ module Gitlab
raise NotImplementedError
end
+ # List of cached methods. Should be overridden by the including class
+ def cached_methods
+ raise NotImplementedError
+ end
+
# Caches the supplied block both in a cache and in an instance variable.
#
# The cache key and instance variable are named the same way as the value of
@@ -67,6 +72,11 @@ module Gitlab
# Expires the caches of a specific set of methods
def expire_method_caches(methods)
methods.each do |key|
+ unless cached_methods.include?(key.to_sym)
+ Rails.logger.error "Requested to expire non-existent method '#{key}' for Repository"
+ next
+ end
+
cache.expire(key)
ivar = cache_instance_variable_name(key)
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
index 44074397c05..900dbf7be24 100644
--- a/lib/tasks/gitlab/import_export.rake
+++ b/lib/tasks/gitlab/import_export.rake
@@ -10,15 +10,22 @@ namespace :gitlab do
puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(SortKeys: true)
end
- desc 'GitLab | Bumps the Import/Export version for test_project_export.tar.gz'
- task bump_test_version: :environment do
- Dir.mktmpdir do |tmp_dir|
- system("tar -zxf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} > /dev/null")
- File.write(File.join(tmp_dir, 'VERSION'), Gitlab::ImportExport.version, mode: 'w')
- system("tar -zcvf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} . > /dev/null")
+ desc 'GitLab | Bumps the Import/Export version in fixtures and project templates'
+ task bump_version: :environment do
+ archives = Dir['vendor/project_templates/*.tar.gz']
+ archives.push('spec/features/projects/import_export/test_project_export.tar.gz')
+
+ archives.each do |archive|
+ raise ArgumentError unless File.exist?(archive)
+
+ Dir.mktmpdir do |tmp_dir|
+ system("tar -zxf #{archive} -C #{tmp_dir} > /dev/null")
+ File.write(File.join(tmp_dir, 'VERSION'), Gitlab::ImportExport.version, mode: 'w')
+ system("tar -zcvf #{archive} -C #{tmp_dir} . > /dev/null")
+ end
end
- puts "Updated to #{Gitlab::ImportExport.version}"
+ puts "Updated #{archives} to #{Gitlab::ImportExport.version}."
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 926bd708532..5c4e10bfd4a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-06-20 16:52+0300\n"
-"PO-Revision-Date: 2018-06-20 16:52+0300\n"
+"POT-Creation-Date: 2018-07-01 16:35+1000\n"
+"PO-Revision-Date: 2018-07-01 16:35+1000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -87,6 +87,9 @@ msgstr[1] ""
msgid "%{filePath} deleted"
msgstr ""
+msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
+msgstr ""
+
msgid "%{loadingIcon} Started"
msgstr ""
@@ -216,6 +219,9 @@ msgstr ""
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr ""
+msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgstr ""
+
msgid "A user with write access to the source branch selected this option"
msgstr ""
@@ -237,6 +243,9 @@ msgstr ""
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr ""
+msgid "Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report."
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -345,6 +354,9 @@ msgstr ""
msgid "Allow commits from members who can merge to the target branch."
msgstr ""
+msgid "Allow public access to pipelines and job details, including output logs and artifacts"
+msgstr ""
+
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
msgstr ""
@@ -357,6 +369,27 @@ msgstr ""
msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
+msgid "An error occured creating the new branch."
+msgstr ""
+
+msgid "An error occured whilst loading all the files."
+msgstr ""
+
+msgid "An error occured whilst loading the file content."
+msgstr ""
+
+msgid "An error occured whilst loading the file."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request changes."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request version data."
+msgstr ""
+
+msgid "An error occured whilst loading the merge request."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -438,6 +471,9 @@ msgstr ""
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
+msgid "Are you sure you want to remove this identity?"
+msgstr ""
+
msgid "Are you sure you want to reset registration token?"
msgstr ""
@@ -516,6 +552,9 @@ msgstr ""
msgid "Auto Review Apps and Auto Deploy need a domain name to work correctly."
msgstr ""
+msgid "Auto-cancel redundant, pending pipelines"
+msgstr ""
+
msgid "AutoDevOps|Auto DevOps"
msgstr ""
@@ -555,6 +594,9 @@ msgstr ""
msgid "Average per day: %{average}"
msgstr ""
+msgid "Background color"
+msgstr ""
+
msgid "Background jobs"
msgstr ""
@@ -630,6 +672,15 @@ msgstr ""
msgid "Begin with the selected commit"
msgstr ""
+msgid "Below are examples of regex for existing tools:"
+msgstr ""
+
+msgid "Boards"
+msgstr ""
+
+msgid "Branch %{branchName} was not found in this project's repository."
+msgstr ""
+
msgid "Branch (%{branch_count})"
msgid_plural "Branches (%{branch_count})"
msgstr[0] ""
@@ -785,6 +836,9 @@ msgstr ""
msgid "CI / CD"
msgstr ""
+msgid "CI / CD Settings"
+msgstr ""
+
msgid "CI/CD configuration"
msgstr ""
@@ -836,6 +890,9 @@ msgstr ""
msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages."
msgstr ""
+msgid "Can't find HEAD commit for this branch"
+msgstr ""
+
msgid "Cancel"
msgstr ""
@@ -899,6 +956,12 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
+msgid "Choose any color."
+msgstr ""
+
+msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent application code"
+msgstr ""
+
msgid "Choose file..."
msgstr ""
@@ -1094,7 +1157,7 @@ msgstr ""
msgid "ClusterIntegration|Environment scope"
msgstr ""
-msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
+msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
msgstr ""
msgid "ClusterIntegration|Fetching machine types"
@@ -1443,6 +1506,9 @@ msgstr ""
msgid "Committed by"
msgstr ""
+msgid "Commit…"
+msgstr ""
+
msgid "Compare"
msgstr ""
@@ -1560,6 +1626,9 @@ msgstr ""
msgid "Continuous Integration and Deployment"
msgstr ""
+msgid "Contribute to GitLab"
+msgstr ""
+
msgid "Contribution"
msgstr ""
@@ -1692,6 +1761,9 @@ msgstr ""
msgid "CurrentUser|Settings"
msgstr ""
+msgid "Custom CI config path"
+msgstr ""
+
msgid "Custom notification events"
msgstr ""
@@ -1743,6 +1815,9 @@ msgstr ""
msgid "Delete"
msgstr ""
+msgid "Delete list"
+msgstr ""
+
msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] ""
@@ -1955,12 +2030,18 @@ msgstr ""
msgid "Edit"
msgstr ""
+msgid "Edit Label"
+msgstr ""
+
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
msgid "Edit files in the editor and commit changes here"
msgstr ""
+msgid "Edit identity for %{user_name}"
+msgstr ""
+
msgid "Email"
msgstr ""
@@ -2177,6 +2258,9 @@ msgstr ""
msgid "Failure"
msgstr ""
+msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
+msgstr ""
+
msgid "Feb"
msgstr ""
@@ -2210,6 +2294,15 @@ msgstr ""
msgid "FirstPushedBy|pushed by"
msgstr ""
+msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
+msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
+msgstr ""
+
msgid "Fork"
msgid_plural "Forks"
msgstr[0] ""
@@ -2248,6 +2341,9 @@ msgstr ""
msgid "General"
msgstr ""
+msgid "General pipelines"
+msgstr ""
+
msgid "Generate a default set of labels"
msgstr ""
@@ -2260,6 +2356,9 @@ msgstr ""
msgid "Git storage health information has been reset"
msgstr ""
+msgid "Git strategy for pipelines"
+msgstr ""
+
msgid "Git version"
msgstr ""
@@ -2341,6 +2440,9 @@ msgstr ""
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
+msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2427,6 +2529,9 @@ msgstr ""
msgid "I accept the|Terms of Service and Privacy Policy"
msgstr ""
+msgid "ID"
+msgstr ""
+
msgid "IDE|Commit"
msgstr ""
@@ -2442,6 +2547,18 @@ msgstr ""
msgid "IDE|Review"
msgstr ""
+msgid "Identifier"
+msgstr ""
+
+msgid "Identities"
+msgstr ""
+
+msgid "If disabled, the access level will depend on the user's permissions in the project."
+msgstr ""
+
+msgid "If enabled"
+msgstr ""
+
msgid "If you already have files you can push them using the %{link_to_cli} below."
msgstr ""
@@ -2490,6 +2607,9 @@ msgstr ""
msgid "Integrations"
msgstr ""
+msgid "Integrations Settings"
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -2505,6 +2625,9 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
+msgid "Issue Board"
+msgstr ""
+
msgid "Issue events"
msgstr ""
@@ -2523,6 +2646,9 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Job"
+msgstr ""
+
msgid "Job has been erased"
msgstr ""
@@ -2837,6 +2963,9 @@ msgstr ""
msgid "Name new label"
msgstr ""
+msgid "Name your individual key via a title"
+msgstr ""
+
msgid "Nav|Help"
msgstr ""
@@ -2849,6 +2978,9 @@ msgstr ""
msgid "Nav|Sign out and sign in with a different account"
msgstr ""
+msgid "New Identity"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
@@ -2860,6 +2992,9 @@ msgstr ""
msgid "New Kubernetes cluster"
msgstr ""
+msgid "New Label"
+msgstr ""
+
msgid "New Pipeline Schedule"
msgstr ""
@@ -2878,6 +3013,9 @@ msgstr ""
msgid "New group"
msgstr ""
+msgid "New identity"
+msgstr ""
+
msgid "New issue"
msgstr ""
@@ -2887,6 +3025,9 @@ msgstr ""
msgid "New merge request"
msgstr ""
+msgid "New pipelines will cancel older, pending pipelines on the same branch"
+msgstr ""
+
msgid "New project"
msgstr ""
@@ -3076,6 +3217,9 @@ msgstr ""
msgid "Options"
msgstr ""
+msgid "Or you can choose one of the suggested colors below"
+msgstr ""
+
msgid "Other Labels"
msgstr ""
@@ -3112,12 +3256,18 @@ msgstr ""
msgid "Password"
msgstr ""
+msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
+msgstr ""
+
msgid "Pause"
msgstr ""
msgid "Pending"
msgstr ""
+msgid "Per job. If a job passes this threshold, it will be marked as failed"
+msgstr ""
+
msgid "Perform advanced options such as changing path, transferring, or removing the group."
msgstr ""
@@ -3142,6 +3292,9 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
+msgid "Pipeline triggers"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -3298,6 +3451,9 @@ msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
+msgid "Please try again"
+msgstr ""
+
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
@@ -3553,12 +3709,18 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Provider"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
msgid "Public - The project can be accessed without any authentication."
msgstr ""
+msgid "Public pipelines"
+msgstr ""
+
msgid "Push events"
msgstr ""
@@ -3571,6 +3733,9 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
+msgid "Re-deploy"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -3589,6 +3754,9 @@ msgstr ""
msgid "Register and see your runners for this group."
msgstr ""
+msgid "Register and see your runners for this project."
+msgstr ""
+
msgid "Registry"
msgstr ""
@@ -3634,6 +3802,9 @@ msgstr ""
msgid "Repository"
msgstr ""
+msgid "Repository Settings"
+msgstr ""
+
msgid "Repository maintenance"
msgstr ""
@@ -3699,6 +3870,12 @@ msgstr ""
msgid "Reviewing (merge request !%{mergeRequestId})"
msgstr ""
+msgid "Rollback"
+msgstr ""
+
+msgid "Runner token"
+msgstr ""
+
msgid "Runners"
msgstr ""
@@ -3714,6 +3891,12 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
+msgid "SSL Verification"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
msgid "Save changes"
msgstr ""
@@ -3905,6 +4088,9 @@ msgstr ""
msgid "Size and domain settings for static websites"
msgstr ""
+msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
+msgstr ""
+
msgid "Snippets"
msgstr ""
@@ -4058,6 +4244,9 @@ msgstr ""
msgid "Stage"
msgstr ""
+msgid "Stage & Commit"
+msgstr ""
+
msgid "Stage all changes"
msgstr ""
@@ -4222,6 +4411,9 @@ msgstr ""
msgid "Terms of Service and Privacy Policy"
msgstr ""
+msgid "Test coverage parsing"
+msgstr ""
+
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project"
msgstr ""
@@ -4252,6 +4444,9 @@ msgstr ""
msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
msgstr ""
+msgid "The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>"
+msgstr ""
+
msgid "The phase of the development lifecycle."
msgstr ""
@@ -4279,6 +4474,9 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
+msgid "The secure token used by the Runner to checkout the project"
+msgstr ""
+
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr ""
@@ -4303,6 +4501,9 @@ msgstr ""
msgid "There are no issues to show"
msgstr ""
+msgid "There are no labels yet"
+msgstr ""
+
msgid "There are no merge requests to show"
msgstr ""
@@ -4417,6 +4618,9 @@ msgstr ""
msgid "This source diff could not be displayed because it is too large."
msgstr ""
+msgid "This user has no identities"
+msgstr ""
+
msgid "Time before an issue gets scheduled"
msgstr ""
@@ -4573,6 +4777,9 @@ msgstr ""
msgid "Timeago|right now"
msgstr ""
+msgid "Timeout"
+msgstr ""
+
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] ""
@@ -4592,6 +4799,9 @@ msgstr ""
msgid "To GitLab"
msgstr ""
+msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
+msgstr ""
+
msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
@@ -4643,6 +4853,9 @@ msgstr ""
msgid "Trigger this manual action"
msgstr ""
+msgid "Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will impersonate their associated user including their access to projects and their project permissions."
+msgstr ""
+
msgid "Try again"
msgstr ""
@@ -4691,6 +4904,11 @@ msgstr ""
msgid "Up to date"
msgstr ""
+msgid "Update %{files}"
+msgid_plural "Update %{files} files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Update your group name, description, avatar, and other general settings."
msgstr ""
@@ -4724,6 +4942,9 @@ msgstr ""
msgid "User and IP Rate Limits"
msgstr ""
+msgid "Users"
+msgstr ""
+
msgid "Variables"
msgstr ""
@@ -4940,9 +5161,6 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
-msgid "Write a commit message..."
-msgstr ""
-
msgid "Yes"
msgstr ""
diff --git a/package.json b/package.json
index c42bbbb0351..6980416503e 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"d3-shape": "^1.2.0",
"d3-time": "^1.0.8",
"d3-time-format": "^2.1.1",
+ "dateformat": "^3.0.3",
"deckar01-task_list": "^2.0.0",
"diff": "^3.4.0",
"document-register-element": "1.3.0",
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index 1466bc2e0bf..0f739f61db9 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -16,7 +16,7 @@ module QA # rubocop:disable Naming/FileName
element :domain_field, 'text_field :domain'
element :enable_auto_devops_button, "%strong= s_('CICD|Enable Auto DevOps')"
element :domain_input, "%strong= _('Domain')"
- element :save_changes_button, "submit 'Save changes'"
+ element :save_changes_button, "submit _('Save changes')"
end
def expand_runners_settings(&block)
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 02b30f9bc6d..b1d83246238 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -124,7 +124,7 @@ describe Projects::MilestonesController do
it 'shows group milestone' do
post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
- expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\">group milestone</a>.")
+ expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\"><u>group milestone</u></a>.")
expect(response).to redirect_to(project_milestones_path(project))
end
end
diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb
index 04217fec06c..5828d833ae9 100644
--- a/spec/features/groups/empty_states_spec.rb
+++ b/spec/features/groups/empty_states_spec.rb
@@ -59,6 +59,18 @@ feature 'Group empty states' do
end
end
+ shared_examples "no projects" do
+ it 'displays an empty state' do
+ expect(page).to have_selector('.empty-state')
+ end
+
+ it "does not show a new #{issuable_name} button" do
+ within '.empty-state' do
+ expect(page).not_to have_link("create #{issuable_name}")
+ end
+ end
+ end
+
context 'group without a project' do
context 'group has a subgroup', :nested_groups do
let(:subgroup) { create(:group, parent: group) }
@@ -92,16 +104,18 @@ feature 'Group empty states' do
visit path
end
- it 'displays an empty state' do
- expect(page).to have_selector('.empty-state')
- end
+ it_behaves_like "no projects"
+ end
+ end
- it "shows a new #{issuable_name} button" do
- within '.empty-state' do
- expect(page).not_to have_link("create #{issuable_name}")
- end
- end
+ context 'group has only a project with issues disabled' do
+ let(:project_with_issues_disabled) { create(:empty_project, :issues_disabled, group: group) }
+
+ before do
+ visit path
end
+
+ it_behaves_like "no projects"
end
end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 111a24c0d94..e131ded3688 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -5,6 +5,7 @@ feature 'Group issues page' do
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group)}
+ let(:project_with_issues_disabled) { create(:project, :issues_disabled, group: group) }
let(:path) { issues_group_path(group) }
context 'with shared examples' do
@@ -76,4 +77,25 @@ feature 'Group issues page' do
end
end
end
+
+ context 'projects with issues disabled' do
+ describe 'issue dropdown' do
+ let(:user_in_group) { create(:group_member, :master, user: create(:user), group: group ).user }
+
+ before do
+ [project, project_with_issues_disabled].each { |project| project.add_master(user_in_group) }
+ sign_in(user_in_group)
+ visit issues_group_path(group)
+ end
+
+ it 'shows projects only with issues feature enabled', :js do
+ find('.new-project-item-link').click
+
+ page.within('.select2-results') do
+ expect(page).to have_content(project.full_name)
+ expect(page).not_to have_content(project_with_issues_disabled.full_name)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 672ae785c2d..921a447f6ee 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -56,4 +56,21 @@ feature 'Group merge requests page' do
expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
end
end
+
+ describe 'new merge request dropdown' do
+ let(:project_with_merge_requests_disabled) { create(:project, :merge_requests_disabled, group: group) }
+
+ before do
+ visit path
+ end
+
+ it 'shows projects only with merge requests feature enabled', :js do
+ find('.new-project-item-link').click
+
+ page.within('.select2-results') do
+ expect(page).to have_content(project.name_with_namespace)
+ expect(page).not_to have_content(project_with_merge_requests_disabled.name_with_namespace)
+ end
+ end
+ end
end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 20337f1d3b0..2108d763028 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -107,19 +107,6 @@ feature 'Group milestones' do
expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1)
end
- it 'updates milestone' do
- page.within(".milestones #milestone_#{active_group_milestone.id}") do
- click_link('Edit')
- end
-
- page.within('.milestone-form') do
- fill_in 'milestone_title', with: 'new title'
- click_button('Update milestone')
- end
-
- expect(find('#content-body h2')).to have_content('new title')
- end
-
it 'shows milestone detail and supports its edit' do
page.within(".milestones #milestone_#{active_group_milestone.id}") do
click_link(active_group_milestone.title)
diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb
index 90d02f7e40f..a4d05c25a90 100644
--- a/spec/features/ics/dashboard_issues_spec.rb
+++ b/spec/features/ics/dashboard_issues_spec.rb
@@ -5,6 +5,7 @@ describe 'Dashboard Issues Calendar Feed' do
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:project) { create(:project) }
+ let(:milestone) { create(:milestone, project_id: project.id, title: 'v1.0') }
before do
project.add_master(user)
@@ -14,7 +15,9 @@ describe 'Dashboard Issues Calendar Feed' do
context 'with no referer' do
it 'renders calendar feed' do
sign_in user
- visit issues_dashboard_path(:ics)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date')
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -25,19 +28,37 @@ describe 'Dashboard Issues Calendar Feed' do
it 'renders calendar feed as text/plain' do
sign_in user
page.driver.header('Referer', issues_dashboard_url(host: Settings.gitlab.base_url))
- visit issues_dashboard_path(:ics)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date')
expect(response_headers['Content-Type']).to have_content('text/plain')
expect(body).to have_text('BEGIN:VCALENDAR')
end
end
+
+ context 'when filtered by milestone' do
+ it 'renders calendar feed' do
+ sign_in user
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ milestone_title: milestone.title)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
end
context 'when authenticated via personal access token' do
it 'renders calendar feed' do
personal_access_token = create(:personal_access_token, user: user)
- visit issues_dashboard_path(:ics, private_token: personal_access_token.token)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ private_token: personal_access_token.token)
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -46,7 +67,10 @@ describe 'Dashboard Issues Calendar Feed' do
context 'when authenticated via feed token' do
it 'renders calendar feed' do
- visit issues_dashboard_path(:ics, feed_token: user.feed_token)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ feed_token: user.feed_token)
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -60,7 +84,10 @@ describe 'Dashboard Issues Calendar Feed' do
end
it 'renders issue fields' do
- visit issues_dashboard_path(:ics, feed_token: user.feed_token)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ feed_token: user.feed_token)
expect(body).to have_text("SUMMARY:test title (in #{project.full_path})")
# line length for ics is 75 chars
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
index 414702daba4..9d4a68239d3 100644
--- a/spec/features/milestones/user_deletes_milestone_spec.rb
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -13,6 +13,7 @@ describe "User deletes milestone", :js do
end
it "deletes milestone" do
+ click_link(milestone.title)
click_button("Delete")
click_button("Delete milestone")
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 72ab2d71f35..ceba4dfec57 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issues/user_creates_issue_spec.rb b/spec/features/projects/issues/user_creates_issue_spec.rb
index e76f7c5589d..5e8662100c5 100644
--- a/spec/features/projects/issues/user_creates_issue_spec.rb
+++ b/spec/features/projects/issues/user_creates_issue_spec.rb
@@ -17,6 +17,9 @@ describe "User creates issue" do
expect(page).to have_no_content("Assign to")
.and have_no_content("Labels")
.and have_no_content("Milestone")
+
+ expect(page.find('#issue_title')['placeholder']).to eq 'Title'
+ expect(page.find('#issue_description')['placeholder']).to eq 'Write a comment or drag your files here…'
end
issue_title = "500 error on profile"
diff --git a/spec/features/projects/milestones/new_spec.rb b/spec/features/projects/milestones/new_spec.rb
index f7900210fe6..6595bff549b 100644
--- a/spec/features/projects/milestones/new_spec.rb
+++ b/spec/features/projects/milestones/new_spec.rb
@@ -9,9 +9,9 @@ feature 'Creating a new project milestone', :js do
visit new_project_milestone_path(project)
end
- it 'description has autocomplete' do
+ it 'description has emoji autocomplete' do
find('#milestone_description').native.send_keys('')
- fill_in 'milestone_description', with: '@'
+ fill_in 'milestone_description', with: ':'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 706894f4b32..733e6c89de7 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -242,7 +242,7 @@ describe "User creates wiki page" do
end
end
- it "shows the autocompletion dropdown" do
+ it "shows the emoji autocompletion dropdown" do
click_link("New page")
page.within("#modal-new-wiki") do
@@ -254,7 +254,7 @@ describe "User creates wiki page" do
page.within(".wiki-form") do
find("#wiki_content").native.send_keys("")
- fill_in(:wiki_content, with: "@")
+ fill_in(:wiki_content, with: ":")
end
expect(page).to have_selector(".atwho-view")
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 272dac127dd..2ccbc15b6da 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -96,11 +96,11 @@ describe 'User updates wiki page' do
expect(find('textarea#wiki_content').value).to eq('')
end
- it 'shows the autocompletion dropdown', :js do
+ it 'shows the emoji autocompletion dropdown', :js do
click_link('Edit')
find('#wiki_content').native.send_keys('')
- fill_in(:wiki_content, with: '@')
+ fill_in(:wiki_content, with: ':')
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 8a8f6933fa5..6701f575a23 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -75,9 +75,9 @@ feature 'Master creates tag' do
visit new_project_tag_path(project)
end
- it 'description has autocomplete', :js do
+ it 'description has emoji autocomplete', :js do
find('#release_description').native.send_keys('')
- fill_in 'release_description', with: '@'
+ fill_in 'release_description', with: ':'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb
index 1c370a99b13..26f51bee887 100644
--- a/spec/features/tags/master_updates_tag_spec.rb
+++ b/spec/features/tags/master_updates_tag_spec.rb
@@ -25,13 +25,13 @@ feature 'Master updates tag' do
expect(page).to have_content 'Awesome release notes'
end
- scenario 'description has autocomplete', :js do
+ scenario 'description has emoji autocomplete', :js do
page.within(first('.content-list .controls')) do
click_link 'Edit release notes'
end
find('#release_description').native.send_keys('')
- fill_in 'release_description', with: '@'
+ fill_in 'release_description', with: ':'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index c8a43ddf410..669ec602f11 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -19,7 +19,7 @@ describe MergeRequestsFinder do
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') }
- let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) }
+ let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked') }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) }
let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4) }
@@ -35,7 +35,7 @@ describe MergeRequestsFinder do
it 'filters by scope' do
params = { scope: 'authored', state: 'opened' }
merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(4)
+ expect(merge_requests.size).to eq(3)
end
it 'filters by project' do
@@ -90,6 +90,14 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request2)
end
+ it 'filters by state' do
+ params = { state: 'locked' }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request3)
+ end
+
context 'filtering by group milestone' do
let!(:group) { create(:group, :public) }
let(:group_milestone) { create(:milestone, group: group) }
@@ -199,7 +207,7 @@ describe MergeRequestsFinder do
it 'returns the number of rows for the default state' do
finder = described_class.new(user)
- expect(finder.row_count).to eq(4)
+ expect(finder.row_count).to eq(3)
end
it 'returns the number of rows for a given state' do
diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz
deleted file mode 100644
index bef7e2ff8ee..00000000000
--- a/spec/fixtures/exported-project.gz
+++ /dev/null
Binary files differ
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 3008528e60c..885204062fe 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -54,7 +54,7 @@ describe MergeRequestsHelper do
let(:options) { { force_link: true } }
it 'removes the data-toggle attributes' do
- is_expected.not_to match(/data-toggle="tab"/)
+ is_expected.not_to match(/data-toggle="tabvue"/)
end
end
end
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index c53b6da4b48..54cb6d84109 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -242,7 +242,7 @@ describe('Api', () => {
},
]);
- Api.groupProjects(groupId, query, response => {
+ Api.groupProjects(groupId, query, {}, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js
index 05c6d587e9c..fc4288eb15b 100644
--- a/spec/javascripts/helpers/init_vue_mr_page_helper.js
+++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js
@@ -5,12 +5,16 @@ import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_dat
import diffFileMockData from '../diffs/mock_data/diff_file';
export default function initVueMRPage() {
+ const mrTestEl = document.createElement('div');
+ mrTestEl.className = 'js-merge-request-test';
+ document.body.appendChild(mrTestEl);
+
const diffsAppEndpoint = '/diffs/app/endpoint';
const diffsAppProjectPath = 'testproject';
const mrEl = document.createElement('div');
mrEl.className = 'merge-request fixture-mr';
mrEl.setAttribute('data-mr-action', 'diffs');
- document.body.appendChild(mrEl);
+ mrTestEl.appendChild(mrEl);
const mrDiscussionsEl = document.createElement('div');
mrDiscussionsEl.id = 'js-vue-mr-discussions';
@@ -18,18 +22,18 @@ export default function initVueMRPage() {
mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock));
mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock));
mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request');
- document.body.appendChild(mrDiscussionsEl);
+ mrTestEl.appendChild(mrDiscussionsEl);
const discussionCounterEl = document.createElement('div');
discussionCounterEl.id = 'js-vue-discussion-counter';
- document.body.appendChild(discussionCounterEl);
+ mrTestEl.appendChild(discussionCounterEl);
const diffsAppEl = document.createElement('div');
diffsAppEl.id = 'js-diffs-app';
diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint);
diffsAppEl.setAttribute('data-project-path', diffsAppProjectPath);
diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
- document.body.appendChild(diffsAppEl);
+ mrTestEl.appendChild(diffsAppEl);
const mock = new MockAdapter(axios);
mock.onGet(diffsAppEndpoint).reply(200, {
diff --git a/spec/javascripts/ide/components/error_message_spec.js b/spec/javascripts/ide/components/error_message_spec.js
index 70d13a6ceb2..430e8e2baa3 100644
--- a/spec/javascripts/ide/components/error_message_spec.js
+++ b/spec/javascripts/ide/components/error_message_spec.js
@@ -36,13 +36,15 @@ describe('IDE error message component', () => {
});
describe('with action', () => {
+ let actionSpy;
+
beforeEach(done => {
- vm.message.action = 'testAction';
+ actionSpy = jasmine.createSpy('action').and.returnValue(Promise.resolve());
+
+ vm.message.action = actionSpy;
vm.message.actionText = 'test action';
vm.message.actionPayload = 'testActionPayload';
- spyOn(vm.$store, 'dispatch').and.returnValue(Promise.resolve());
-
vm.$nextTick(done);
});
@@ -63,7 +65,7 @@ describe('IDE error message component', () => {
vm.$el.querySelector('.flash-action').click();
vm.$nextTick(() => {
- expect(vm.$store.dispatch).toHaveBeenCalledWith('testAction', 'testActionPayload');
+ expect(actionSpy).toHaveBeenCalledWith('testActionPayload');
done();
});
@@ -74,7 +76,7 @@ describe('IDE error message component', () => {
vm.$el.querySelector('.flash-action').click();
- expect(vm.$store.dispatch).not.toHaveBeenCalledWith();
+ expect(actionSpy).not.toHaveBeenCalledWith();
});
it('resets isLoading after click', done => {
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
index 6bf309fb4bf..30cd92b2ca4 100644
--- a/spec/javascripts/ide/components/repo_commit_section_spec.js
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import store from '~/ide/stores';
-import service from '~/ide/services';
import router from '~/ide/ide_router';
import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
@@ -68,23 +67,6 @@ describe('RepoCommitSection', () => {
vm.$mount();
- spyOn(service, 'getTreeData').and.returnValue(
- Promise.resolve({
- headers: {
- 'page-title': 'test',
- },
- json: () =>
- Promise.resolve({
- last_commit_path: 'last_commit_path',
- parent_tree_url: 'parent_tree_url',
- path: '/',
- trees: [{ name: 'tree' }],
- blobs: [{ name: 'blob' }],
- submodules: [{ name: 'submodule' }],
- }),
- }),
- );
-
Vue.nextTick(done);
});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 5746683917e..58d3ffc6d94 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import * as actions from '~/ide/stores/actions/file';
import * as types from '~/ide/stores/mutation_types';
@@ -9,11 +11,16 @@ import { file, resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store file actions', () => {
+ let mock;
+
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
spyOn(router, 'push');
});
afterEach(() => {
+ mock.restore();
resetStore(store);
});
@@ -183,94 +190,125 @@ describe('IDE store file actions', () => {
let localFile;
beforeEach(() => {
- spyOn(service, 'getFileData').and.returnValue(
- Promise.resolve({
- headers: {
- 'page-title': 'testing getFileData',
- },
- json: () =>
- Promise.resolve({
- blame_path: 'blame_path',
- commits_path: 'commits_path',
- permalink: 'permalink',
- raw_path: 'raw_path',
- binary: false,
- html: '123',
- render_error: '',
- }),
- }),
- );
+ spyOn(service, 'getFileData').and.callThrough();
localFile = file(`newCreate-${Math.random()}`);
- localFile.url = 'getFileDataURL';
+ localFile.url = `${gl.TEST_HOST}/getFileDataURL`;
store.state.entries[localFile.path] = localFile;
});
- it('calls the service', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).replyOnce(
+ 200,
+ {
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ },
+ {
+ 'page-title': 'testing getFileData',
+ },
+ );
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('calls the service', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(service.getFileData).toHaveBeenCalledWith(`${gl.TEST_HOST}/getFileDataURL`);
- it('sets the file data', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(localFile.blamePath).toBe('blame_path');
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('sets the file data', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(localFile.blamePath).toBe('blame_path');
- it('sets document title', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(document.title).toBe('testing getFileData');
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('sets document title', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(document.title).toBe('testing getFileData');
- it('sets the file as active', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(localFile.active).toBeTruthy();
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('sets the file as active', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
- it('sets the file not as active if we pass makeFileActive false', done => {
- store
- .dispatch('getFileData', { path: localFile.path, makeFileActive: false })
- .then(() => {
- expect(localFile.active).toBeFalsy();
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
+ it('sets the file not as active if we pass makeFileActive false', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path, makeFileActive: false })
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds the file to open files', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(localFile.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('adds the file to open files', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(store.state.openFiles.length).toBe(1);
- expect(store.state.openFiles[0].name).toBe(localFile.name);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).networkError();
+ });
- done();
- })
- .catch(done.fail);
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ actions
+ .getFileData({ state: store.state, commit() {}, dispatch }, { path: localFile.path })
+ .then(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the file.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ path: localFile.path,
+ makeFileActive: true,
+ },
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
});
@@ -278,48 +316,84 @@ describe('IDE store file actions', () => {
let tmpFile;
beforeEach(() => {
- spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
+ spyOn(service, 'getRawFileData').and.callThrough();
tmpFile = file('tmpFile');
store.state.entries[tmpFile.path] = tmpFile;
});
- it('calls getRawFileData service method', done => {
- store
- .dispatch('getRawFileData', { path: tmpFile.path })
- .then(() => {
- expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/(.*)/).replyOnce(200, 'raw');
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('calls getRawFileData service method', done => {
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path })
+ .then(() => {
+ expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
- it('updates file raw data', done => {
- store
- .dispatch('getRawFileData', { path: tmpFile.path })
- .then(() => {
- expect(tmpFile.raw).toBe('raw');
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('updates file raw data', done => {
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path })
+ .then(() => {
+ expect(tmpFile.raw).toBe('raw');
- it('calls also getBaseRawFileData service method', done => {
- spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
+ done();
+ })
+ .catch(done.fail);
+ });
- tmpFile.mrChange = { new_file: false };
+ it('calls also getBaseRawFileData service method', done => {
+ spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
- store
- .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
- .then(() => {
- expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
- expect(tmpFile.baseRaw).toBe('baseraw');
+ tmpFile.mrChange = { new_file: false };
- done();
- })
- .catch(done.fail);
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
+ .then(() => {
+ expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
+ expect(tmpFile.baseRaw).toBe('baseraw');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/(.*)/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ actions
+ .getRawFileData(
+ { state: store.state, commit() {}, dispatch },
+ { path: tmpFile.path, baseSha: tmpFile.baseSha },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the file content.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ path: tmpFile.path,
+ baseSha: tmpFile.baseSha,
+ },
+ });
+
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
index b4ec4a0b173..c99ccc70c6a 100644
--- a/spec/javascripts/ide/stores/actions/merge_request_spec.js
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -1,110 +1,239 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
+import {
+ getMergeRequestData,
+ getMergeRequestChanges,
+ getMergeRequestVersions,
+} from '~/ide/stores/actions/merge_request';
import service from '~/ide/services';
import { resetStore } from '../../helpers';
describe('IDE store merge request actions', () => {
+ let mock;
+
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
store.state.projects.abcproject = {
mergeRequests: {},
};
});
afterEach(() => {
+ mock.restore();
resetStore(store);
});
describe('getMergeRequestData', () => {
- beforeEach(() => {
- spyOn(service, 'getProjectMergeRequestData').and.returnValue(
- Promise.resolve({ data: { title: 'mergerequest' } }),
- );
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequestData').and.callThrough();
+
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/)
+ .reply(200, { title: 'mergerequest' });
+ });
+
+ it('calls getProjectMergeRequestData service method', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Object', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
+ expect(store.state.currentMergeRequestId).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('calls getProjectMergeRequestData service method', done => {
- store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
-
- done();
- })
- .catch(done.fail);
- });
-
- it('sets the Merge Request Object', done => {
- store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
- expect(store.state.currentMergeRequestId).toBe(1);
-
- done();
- })
- .catch(done.fail);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ getMergeRequestData(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: 'abcproject', mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the merge request.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: 'abcproject',
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
});
});
describe('getMergeRequestChanges', () => {
beforeEach(() => {
- spyOn(service, 'getProjectMergeRequestChanges').and.returnValue(
- Promise.resolve({ data: { title: 'mergerequest' } }),
- );
-
store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
});
- it('calls getProjectMergeRequestChanges service method', done => {
- store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
-
- done();
- })
- .catch(done.fail);
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequestChanges').and.callThrough();
+
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/)
+ .reply(200, { title: 'mergerequest' });
+ });
+
+ it('calls getProjectMergeRequestChanges service method', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Changes Object', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
+ 'mergerequest',
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('sets the Merge Request Changes Object', done => {
- store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
- 'mergerequest',
- );
- done();
- })
- .catch(done.fail);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ getMergeRequestChanges(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: 'abcproject', mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the merge request changes.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: 'abcproject',
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
});
});
describe('getMergeRequestVersions', () => {
beforeEach(() => {
- spyOn(service, 'getProjectMergeRequestVersions').and.returnValue(
- Promise.resolve({ data: [{ id: 789 }] }),
- );
-
store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
});
- it('calls getProjectMergeRequestVersions service method', done => {
- store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
-
- done();
- })
- .catch(done.fail);
+ describe('success', () => {
+ beforeEach(() => {
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/)
+ .reply(200, [{ id: 789 }]);
+ spyOn(service, 'getProjectMergeRequestVersions').and.callThrough();
+ });
+
+ it('calls getProjectMergeRequestVersions service method', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Versions Object', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('sets the Merge Request Versions Object', done => {
- store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
- done();
- })
- .catch(done.fail);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ getMergeRequestVersions(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: 'abcproject', mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the merge request version data.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: 'abcproject',
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
});
});
});
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index c1f9fc4f148..ca79edafb7e 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -110,7 +110,7 @@ describe('IDE store project actions', () => {
type: 'setErrorMessage',
payload: {
text: "Branch <strong>master</strong> was not found in this project's repository.",
- action: 'createNewBranchFromDefault',
+ action: jasmine.any(Function),
actionText: 'Create branch',
actionPayload: 'master',
},
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index 612a439cfbf..6860e6cdb91 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -1,5 +1,4 @@
import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
import testAction from 'spec/helpers/vuex_action_helper';
import { showTreeEntry, getFiles } from '~/ide/stores/actions/tree';
import * as types from '~/ide/stores/mutation_types';
@@ -117,6 +116,40 @@ describe('Multi-file store tree actions', () => {
done();
});
});
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatchSpy');
+
+ store.state.projects = {
+ 'abc/def': {
+ web_url: `${gl.TEST_HOST}/files`,
+ },
+ };
+
+ mock.onGet(/(.*)/).replyOnce(500);
+
+ getFiles(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading all the files.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: { projectId: 'abc/def', branchId: 'master-testing' },
+ });
+ done();
+ });
+ });
});
});
@@ -168,72 +201,4 @@ describe('Multi-file store tree actions', () => {
);
});
});
-
- describe('getLastCommitData', () => {
- beforeEach(() => {
- spyOn(service, 'getTreeLastCommit').and.returnValue(
- Promise.resolve({
- headers: {
- 'more-logs-url': null,
- },
- json: () =>
- Promise.resolve([
- {
- type: 'tree',
- file_name: 'testing',
- commit: {
- message: 'commit message',
- authored_date: '123',
- },
- },
- ]),
- }),
- );
-
- store.state.trees['abcproject/mybranch'] = {
- tree: [],
- };
-
- projectTree = store.state.trees['abcproject/mybranch'];
- projectTree.tree.push(file('testing', '1', 'tree'));
- projectTree.lastCommitPath = 'lastcommitpath';
- });
-
- it('calls service with lastCommitPath', done => {
- store
- .dispatch('getLastCommitData', projectTree)
- .then(() => {
- expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('updates trees last commit data', done => {
- store
- .dispatch('getLastCommitData', projectTree)
- .then(Vue.nextTick)
- .then(() => {
- expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('does not update entry if not found', done => {
- projectTree.tree[0].name = 'a';
-
- store
- .dispatch('getLastCommitData', projectTree)
- .then(Vue.nextTick)
- .then(() => {
- expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
-
- done();
- })
- .catch(done.fail);
- });
- });
});
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 22eb0ad7143..7502f1fa2e1 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -19,9 +19,11 @@ import IssuablesHelper from '~/helpers/issuables_helper';
spyOn(axios, 'patch').and.callThrough();
mock = new MockAdapter(axios);
- mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {});
+ mock
+ .onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`)
+ .reply(200, {});
- return this.merge = new MergeRequest();
+ return (this.merge = new MergeRequest());
});
afterEach(() => {
@@ -32,17 +34,22 @@ import IssuablesHelper from '~/helpers/issuables_helper';
spyOn($, 'ajax').and.stub();
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
- $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent);
+ $('input[type=checkbox]')
+ .attr('checked', true)[0]
+ .dispatchEvent(changeEvent);
return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
- it('submits an ajax request on tasklist:changed', (done) => {
+ it('submits an ajax request on tasklist:changed', done => {
$('.js-task-list-field').trigger('tasklist:changed');
setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, {
- merge_request: { description: '- [ ] Task List Item' },
- });
+ expect(axios.patch).toHaveBeenCalledWith(
+ `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
+ {
+ merge_request: { description: '- [ ] Task List Item' },
+ },
+ );
done();
});
});
@@ -119,4 +126,4 @@ import IssuablesHelper from '~/helpers/issuables_helper';
});
});
});
-}).call(window);
+}.call(window));
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 08928e13985..7251ce19a90 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -40,6 +40,7 @@ describe('MergeRequestTabs', function() {
this.class.unbindEvents();
this.class.destroyPipelinesView();
mrPageMock.restore();
+ $('.js-merge-request-test').remove();
});
describe('opensInNewTab', function() {
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index 073dae56c25..9c55a19ebc7 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -135,4 +135,34 @@ describe('pipeline graph job component', () => {
expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test - success');
});
});
+
+ describe('tooltip placement', () => {
+ const tooltipBoundary = 'a[data-boundary="viewport"]';
+
+ it('does not set tooltip boundary by default', () => {
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ });
+
+ expect(component.$el.querySelector(tooltipBoundary)).toBeNull();
+ });
+
+ it('sets tooltip boundary to viewport for small dropdowns', () => {
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ dropdownLength: 1,
+ });
+
+ expect(component.$el.querySelector(tooltipBoundary)).not.toBeNull();
+ });
+
+ it('does not set tooltip boundary for large lists', () => {
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ dropdownLength: 7,
+ });
+
+ expect(component.$el.querySelector(tooltipBoundary)).toBeNull();
+ });
+ });
});
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 5dfbb8e71f8..ebeb05d6e02 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -26,6 +26,21 @@ describe Gitlab::Diff::File do
end
end
+ describe '#diff_lines_for_serializer' do
+ it 'includes bottom match line if not in the end' do
+ expect(diff_file.diff_lines_for_serializer.last.type).to eq('match')
+ end
+
+ context 'when deleted' do
+ let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') }
+ let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') }
+
+ it 'does not include bottom match line' do
+ expect(diff_file.diff_lines_for_serializer.last.type).not_to eq('match')
+ end
+ end
+ end
+
describe '#mode_changed?' do
it { expect(diff_file.mode_changed?).to be_falsey }
end
diff --git a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb
new file mode 100644
index 00000000000..6a803c48b34
--- /dev/null
+++ b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::GroupProjectObjectBuilder do
+ let(:project) do
+ create(:project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ context 'labels' do
+ it 'finds the right group label' do
+ group_label = create(:group_label, 'name': 'group label', 'group': project.group)
+
+ expect(described_class.build(Label,
+ 'title' => 'group label',
+ 'project' => project,
+ 'group' => project.group)).to eq(group_label)
+ end
+
+ it 'creates a new label' do
+ label = described_class.build(Label,
+ 'title' => 'group label',
+ 'project' => project,
+ 'group' => project.group)
+
+ expect(label.persisted?).to be true
+ end
+ end
+
+ context 'milestones' do
+ it 'finds the right group milestone' do
+ milestone = create(:milestone, 'name' => 'group milestone', 'group' => project.group)
+
+ expect(described_class.build(Milestone,
+ 'title' => 'group milestone',
+ 'project' => project,
+ 'group' => project.group)).to eq(milestone)
+ end
+
+ it 'creates a new milestone' do
+ milestone = described_class.build(Milestone,
+ 'title' => 'group milestone',
+ 'project' => project,
+ 'group' => project.group)
+
+ expect(milestone.persisted?).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index 991e354f499..c074e61da26 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -4,14 +4,14 @@ describe Gitlab::ImportExport::Importer do
let(:user) { create(:user) }
let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
let(:shared) { project.import_export_shared }
- let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) }
+ let(:project) { create(:project, import_source: File.join(test_path, 'test_project_export.tar.gz')) }
subject(:importer) { described_class.new(project) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
FileUtils.mkdir_p(shared.export_path)
- FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path)
+ FileUtils.cp(Rails.root.join('spec/features/projects/import_export/test_project_export.tar.gz'), test_path)
allow(subject).to receive(:remove_import_file)
end
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
index c13cf4a0507..ba2248073f5 100644
--- a/spec/lib/gitlab/import_export/project.light.json
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -7,7 +7,7 @@
"milestones": [
{
"id": 1,
- "title": "Project milestone",
+ "title": "A milestone",
"project_id": 8,
"description": "Project-level milestone",
"due_date": null,
@@ -66,8 +66,8 @@
"group_milestone_id": null,
"milestone": {
"id": 1,
- "title": "Project milestone",
- "project_id": 8,
+ "title": "A milestone",
+ "group_id": 8,
"description": "Project-level milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
@@ -86,7 +86,7 @@
"updated_at": "2017-08-15T18:37:40.795Z",
"label": {
"id": 6,
- "title": "Another project label",
+ "title": "Another label",
"color": "#A8D695",
"project_id": null,
"created_at": "2017-08-15T18:37:19.698Z",
diff --git a/spec/lib/gitlab/import_export/project.milestone-iid.json b/spec/lib/gitlab/import_export/project.milestone-iid.json
new file mode 100644
index 00000000000..b028147b5eb
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.milestone-iid.json
@@ -0,0 +1,80 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "import_type": "gitlab_project",
+ "creator_id": 123,
+ "visibility_level": 10,
+ "archived": false,
+ "issues": [
+ {
+ "id": 1,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 20,
+ "updated_by_id": 1,
+ "confidential": false,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "Group-level milestone",
+ "description": "Group-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": 8
+ }
+ },
+ {
+ "id": 2,
+ "title": "est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 21,
+ "updated_by_id": 1,
+ "confidential": false,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 2,
+ "title": "Another milestone",
+ "project_id": 8,
+ "description": "milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ }
+ }
+ ],
+ "snippets": [],
+ "hooks": []
+}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 68ddc947e02..bac5693c830 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -189,8 +189,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
@project.pipelines.zip([2, 2, 2, 2, 2])
.each do |(pipeline, expected_status_size)|
- expect(pipeline.statuses.size).to eq(expected_status_size)
- end
+ expect(pipeline.statuses.size).to eq(expected_status_size)
+ end
end
end
@@ -246,13 +246,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(project.issues.size).to eq(results.fetch(:issues, 0))
end
- it 'has issue with group label and project label' do
- labels = project.issues.first.labels
-
- expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0))
- expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0)
- end
-
it 'does not set params that are excluded from import_export settings' do
expect(project.import_type).to be_nil
expect(project.creator_id).not_to eq 123
@@ -268,12 +261,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it 'has group milestone' do
expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0))
end
-
- it 'has issue with group label' do
- labels = project.issues.first.labels
-
- expect(labels.where(type: "GroupLabel").count).to eq(results.fetch(:first_issue_labels, 0))
- end
end
context 'Light JSON' do
@@ -360,13 +347,72 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it_behaves_like 'restores project correctly',
issues: 2,
labels: 1,
- milestones: 1,
+ milestones: 2,
first_issue_labels: 1
it_behaves_like 'restores group correctly',
- labels: 1,
- milestones: 1,
+ labels: 0,
+ milestones: 0,
first_issue_labels: 1
end
+
+ context 'with existing group models' do
+ let!(:project) do
+ create(:project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ before do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+ end
+
+ it 'imports labels' do
+ create(:group_label, name: 'Another label', group: project.group)
+
+ expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
+
+ restored_project_json
+
+ expect(project.labels.count).to eq(1)
+ end
+
+ it 'imports milestones' do
+ create(:milestone, name: 'A milestone', group: project.group)
+
+ expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
+
+ restored_project_json
+
+ expect(project.group.milestones.count).to eq(1)
+ expect(project.milestones.count).to eq(0)
+ end
+ end
+
+ context 'with clashing milestones on IID' do
+ let!(:project) do
+ create(:project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ it 'preserves the project milestone IID' do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.milestone-iid.json")
+
+ expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
+
+ restored_project_json
+
+ expect(project.milestones.count).to eq(2)
+ expect(Milestone.find_by_title('Another milestone').iid).to eq(1)
+ expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index 39ec2f37a83..5c398bc2063 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::Middleware::ReadOnly do
include Rack::Test::Methods
+ using RSpec::Parameterized::TableSyntax
RSpec::Matchers.define :be_a_redirect do
match do |response|
@@ -117,39 +118,41 @@ describe Gitlab::Middleware::ReadOnly do
context 'whitelisted requests' do
it 'expects a POST internal request to be allowed' do
expect(Rails.application.routes).not_to receive(:recognize_path)
-
response = request.post("/api/#{API::API.version}/internal")
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
- it 'expects a POST LFS request to batch URL to be allowed' do
- expect(Rails.application.routes).to receive(:recognize_path).and_call_original
- response = request.post('/root/rouge.git/info/lfs/objects/batch')
+ it 'expects requests to sidekiq admin to be allowed' do
+ response = request.post('/admin/sidekiq')
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
- end
- it 'expects a POST request to git-upload-pack URL to be allowed' do
- expect(Rails.application.routes).to receive(:recognize_path).and_call_original
- response = request.post('/root/rouge.git/git-upload-pack')
+ response = request.get('/admin/sidekiq')
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
- it 'expects requests to sidekiq admin to be allowed' do
- response = request.post('/admin/sidekiq')
-
- expect(response).not_to be_a_redirect
- expect(subject).not_to disallow_request
+ where(:description, :path) do
+ 'LFS request to batch' | '/root/rouge.git/info/lfs/objects/batch'
+ 'LFS request to locks verify' | '/root/rouge.git/info/lfs/locks/verify'
+ 'LFS request to locks create' | '/root/rouge.git/info/lfs/locks'
+ 'LFS request to locks unlock' | '/root/rouge.git/info/lfs/locks/1/unlock'
+ 'request to git-upload-pack' | '/root/rouge.git/git-upload-pack'
+ 'request to git-receive-pack' | '/root/rouge.git/git-receive-pack'
+ end
- response = request.get('/admin/sidekiq')
+ with_them do
+ it "expects a POST #{description} URL to be allowed" do
+ expect(Rails.application.routes).to receive(:recognize_path).and_call_original
+ response = request.post(path)
- expect(response).not_to be_a_redirect
- expect(subject).not_to disallow_request
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
end
end
end
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index 85971f2a7ef..5bd4d6c6a48 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -67,10 +67,18 @@ describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' do
it 'expires the caches of the given methods' do
- expect(cache).to receive(:expire).with(:readme)
+ expect(cache).to receive(:expire).with(:rendered_readme)
expect(cache).to receive(:expire).with(:gitignore)
- repository.expire_method_caches(%i(readme gitignore))
+ repository.expire_method_caches(%i(rendered_readme gitignore))
+ end
+
+ it 'does not expire caches for non-existent methods' do
+ expect(cache).not_to receive(:expire).with(:nonexistent)
+ expect(Rails.logger).to(
+ receive(:error).with("Requested to expire non-existent method 'nonexistent' for Repository"))
+
+ repository.expire_method_caches(%i(nonexistent))
end
end
end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 5410bfbeb31..b7687d48c68 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Mattermost::Session, type: :request do
+ include ExclusiveLeaseHelpers
+
let(:user) { create(:user) }
let(:gitlab_url) { "http://gitlab.com" }
@@ -97,26 +99,20 @@ describe Mattermost::Session, type: :request do
end
end
- context 'with lease' do
- before do
- allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk')
- end
+ context 'exclusive lease' do
+ let(:lease_key) { 'mattermost:session' }
it 'tries to obtain a lease' do
- expect(subject).to receive(:lease_try_obtain)
- expect(Gitlab::ExclusiveLease).to receive(:cancel)
+ expect_to_obtain_exclusive_lease(lease_key, 'uuid')
+ expect_to_cancel_exclusive_lease(lease_key, 'uuid')
# Cannot setup a session, but we should still cancel the lease
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
- end
- context 'without lease' do
- before do
- allow(subject).to receive(:lease_try_obtain).and_return(nil)
- end
+ it 'returns a NoSessionError error without lease' do
+ stub_exclusive_lease_taken(lease_key)
- it 'returns a NoSessionError error' do
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
end
diff --git a/spec/migrations/cleanup_stages_position_migration_spec.rb b/spec/migrations/cleanup_stages_position_migration_spec.rb
new file mode 100644
index 00000000000..dde5a777487
--- /dev/null
+++ b/spec/migrations/cleanup_stages_position_migration_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180604123514_cleanup_stages_position_migration.rb')
+
+describe CleanupStagesPositionMigration, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::MigrateStageIndex)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are pending background migrations' do
+ it 'processes pending jobs synchronously' do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker
+ .perform_in(2.minutes, 'MigrateStageIndex', [1, 1])
+ BackgroundMigrationWorker
+ .perform_async('MigrateStageIndex', [1, 1])
+
+ migrate!
+
+ expect(migration).to have_received(:perform).with(1, 1).twice
+ end
+ end
+ end
+
+ context 'when there are no background migrations pending' do
+ it 'does nothing' do
+ Sidekiq::Testing.disable! do
+ migrate!
+
+ expect(migration).not_to have_received(:perform)
+ end
+ end
+ end
+
+ context 'when there are still unmigrated stages present' do
+ let(:stages) { table('ci_stages') }
+ let(:builds) { table('ci_builds') }
+
+ let!(:entities) do
+ %w[build test broken].map do |name|
+ stages.create(name: name)
+ end
+ end
+
+ before do
+ stages.update_all(position: nil)
+
+ builds.create(name: 'unit', stage_id: entities.first.id, stage_idx: 1, ref: 'master')
+ builds.create(name: 'unit', stage_id: entities.second.id, stage_idx: 1, ref: 'master')
+ end
+
+ it 'migrates stages sequentially for every stage' do
+ expect(stages.all).to all(have_attributes(position: nil))
+
+ migrate!
+
+ expect(migration).to have_received(:perform)
+ .with(entities.first.id, entities.first.id)
+ expect(migration).to have_received(:perform)
+ .with(entities.second.id, entities.second.id)
+ expect(migration).not_to have_received(:perform)
+ .with(entities.third.id, entities.third.id)
+ end
+ end
+end
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index c5d550cba1b..464897de306 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
set(:build) { create(:ci_build, :running) }
let(:chunk_index) { 0 }
let(:data_store) { :redis }
@@ -322,7 +324,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
describe 'ExclusiveLock' do
before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
+ stub_exclusive_lease_taken
stub_const('Ci::BuildTraceChunk::WRITE_LOCK_RETRY', 1)
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index f2a3df50c1a..0f156619e9e 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe ReactiveCaching, :use_clean_rails_memory_store_caching do
+ include ExclusiveLeaseHelpers
include ReactiveCachingHelpers
class CacheTest
@@ -106,8 +107,8 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
it 'takes and releases the lease' do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000")
- expect(Gitlab::ExclusiveLease).to receive(:cancel).with(cache_key, "000000")
+ expect_to_obtain_exclusive_lease(cache_key, 'uuid')
+ expect_to_cancel_exclusive_lease(cache_key, 'uuid')
go!
end
@@ -153,11 +154,9 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
context 'when the lease is already taken' do
- before do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(nil)
- end
-
it 'skips the calculation' do
+ stub_exclusive_lease_taken(cache_key)
+
expect(instance).to receive(:calculate_reactive_cache).never
go!
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index ec72fefd137..8c6b411ec9a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2190,6 +2190,22 @@ describe MergeRequest do
end
end
end
+
+ context 'source branch is missing' do
+ subject { create(:merge_request, :invalid, :opened, merge_status: :unchecked, target_branch: 'master') }
+
+ before do
+ allow(subject.project.repository).to receive(:can_be_merged?).and_call_original
+ end
+
+ it 'does not raise error' do
+ expect(notification_service).not_to receive(:merge_request_unmergeable)
+ expect(todo_service).not_to receive(:merge_request_became_unmergeable)
+
+ expect { subject.mark_as_unmergeable }.not_to raise_error
+ expect(subject.cannot_be_merged?).to eq(true)
+ end
+ end
end
describe 'check_state?' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 27a14ff5d5b..cfa78c4472c 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -479,6 +479,14 @@ describe Repository do
end
end
+ context 'when ref is not specified' do
+ it 'is using a root ref' do
+ expect(repository).to receive(:find_commit).with('master')
+
+ repository.commit
+ end
+ end
+
context 'when ref is not valid' do
context 'when preceding tree element exists' do
it 'returns nil' do
@@ -1689,19 +1697,29 @@ describe Repository do
end
describe '#after_change_head' do
- it 'flushes the readme cache' do
+ it 'flushes the method caches' do
expect(repository).to receive(:expire_method_caches).with([
- :readme,
+ :size,
+ :commit_count,
+ :rendered_readme,
+ :contribution_guide,
:changelog,
- :license,
- :contributing,
+ :license_blob,
+ :license_key,
:gitignore,
- :koding,
- :gitlab_ci,
+ :koding_yml,
+ :gitlab_ci_yml,
+ :branch_names,
+ :tag_names,
+ :branch_count,
+ :tag_count,
:avatar,
- :issue_template,
- :merge_request_template,
- :xcode_config
+ :exists?,
+ :root_ref,
+ :has_visible_content?,
+ :issue_template_names,
+ :merge_request_template_names,
+ :xcode_project?
])
repository.after_change_head
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index d4ebfc3f782..eba39bb6ccc 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -14,6 +14,7 @@ describe API::MergeRequests do
let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
let!(:label) do
@@ -85,7 +86,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), scope: :all
- expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request)
+ expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request, merge_request_locked)
expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id)
end
@@ -158,7 +159,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
@@ -166,7 +167,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
@@ -219,6 +220,14 @@ describe API::MergeRequests do
expect_response_ordered_exactly(merge_request)
end
end
+
+ context 'state param' do
+ it 'returns merge requests with the given state' do
+ get api('/merge_requests', user), state: 'locked'
+
+ expect_response_contain_exactly(merge_request_locked)
+ end
+ end
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 99103039f77..abf9ad738bd 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1990,6 +1990,38 @@ describe API::Projects do
end
end
+ describe 'PUT /projects/:id/transfer' do
+ context 'when authenticated as owner' do
+ let(:group) { create :group }
+
+ it 'transfers the project to the new namespace' do
+ group.add_owner(user)
+
+ put api("/projects/#{project.id}/transfer", user), namespace: group.id
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'fails when transferring to a non owned namespace' do
+ put api("/projects/#{project.id}/transfer", user), namespace: group.id
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'fails when transferring to an unknown namespace' do
+ put api("/projects/#{project.id}/transfer", user), namespace: 'unknown'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'fails on missing namespace' do
+ put api("/projects/#{project.id}/transfer", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
it_behaves_like 'custom attributes endpoints', 'projects' do
let(:attributable) { project }
let(:other_attributable) { project2 }
diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
index bf038595a4d..eb0bdb61ee3 100644
--- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
+++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
describe Clusters::Applications::CheckIngressIpAddressService do
+ include ExclusiveLeaseHelpers
+
let(:application) { create(:clusters_applications_ingress, :installed) }
let(:service) { described_class.new(application) }
let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
let(:ingress) { [{ ip: '111.222.111.222' }] }
- let(:exclusive_lease) { instance_double(Gitlab::ExclusiveLease, try_obtain: true) }
+ let(:lease_key) { "check_ingress_ip_address_service:#{application.id}" }
let(:kube_service) do
::Kubeclient::Resource.new(
@@ -22,11 +24,8 @@ describe Clusters::Applications::CheckIngressIpAddressService do
subject { service.execute }
before do
+ stub_exclusive_lease(lease_key, timeout: 15.seconds.to_i)
allow(application.cluster).to receive(:kubeclient).and_return(kubeclient)
- allow(Gitlab::ExclusiveLease)
- .to receive(:new)
- .with("check_ingress_ip_address_service:#{application.id}", timeout: 15.seconds.to_i)
- .and_return(exclusive_lease)
end
describe '#execute' do
@@ -47,13 +46,9 @@ describe Clusters::Applications::CheckIngressIpAddressService do
end
context 'when the exclusive lease cannot be obtained' do
- before do
- allow(exclusive_lease)
- .to receive(:try_obtain)
- .and_return(false)
- end
-
it 'does not call kubeclient' do
+ stub_exclusive_lease_taken(lease_key, timeout: 15.seconds.to_i)
+
subject
expect(kubeclient).not_to have_received(:get_service)
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 08fd26d67fd..e5fde07a6eb 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Users::RefreshAuthorizedProjectsService do
+ include ExclusiveLeaseHelpers
+
# We're using let! here so that any expectations for the service class are not
# triggered twice.
let!(:project) { create(:project) }
@@ -10,12 +12,10 @@ describe Users::RefreshAuthorizedProjectsService do
describe '#execute', :clean_gitlab_redis_shared_state do
it 'refreshes the authorizations using a lease' do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return('foo')
-
- expect(Gitlab::ExclusiveLease).to receive(:cancel)
- .with(an_instance_of(String), 'foo')
+ lease_key = "refresh_authorized_projects:#{user.id}"
+ expect_to_obtain_exclusive_lease(lease_key, 'uuid')
+ expect_to_cancel_exclusive_lease(lease_key, 'uuid')
expect(service).to receive(:execute_without_lease)
service.execute
diff --git a/spec/support/helpers/exclusive_lease_helpers.rb b/spec/support/helpers/exclusive_lease_helpers.rb
new file mode 100644
index 00000000000..383cc7dee81
--- /dev/null
+++ b/spec/support/helpers/exclusive_lease_helpers.rb
@@ -0,0 +1,36 @@
+module ExclusiveLeaseHelpers
+ def stub_exclusive_lease(key = nil, uuid = 'uuid', renew: false, timeout: nil)
+ key ||= instance_of(String)
+ timeout ||= instance_of(Integer)
+
+ lease = instance_double(
+ Gitlab::ExclusiveLease,
+ try_obtain: uuid,
+ exists?: true,
+ renew: renew
+ )
+
+ allow(Gitlab::ExclusiveLease)
+ .to receive(:new)
+ .with(key, timeout: timeout)
+ .and_return(lease)
+
+ lease
+ end
+
+ def stub_exclusive_lease_taken(key = nil, timeout: nil)
+ stub_exclusive_lease(key, nil, timeout: timeout)
+ end
+
+ def expect_to_obtain_exclusive_lease(key, uuid = 'uuid', timeout: nil)
+ lease = stub_exclusive_lease(key, uuid, timeout: timeout)
+
+ expect(lease).to receive(:try_obtain)
+ end
+
+ def expect_to_cancel_exclusive_lease(key, uuid)
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:cancel)
+ .with(key, uuid)
+ end
+end
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
index 6dbe0f6f980..db723a323f8 100644
--- a/spec/support/shared_examples/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -247,8 +247,10 @@ shared_examples_for 'common trace features' do
end
context 'when another process has already been archiving', :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
before do
- Gitlab::ExclusiveLease.new("trace:archive:#{trace.job.id}", timeout: 1.hour).try_obtain
+ stub_exclusive_lease_taken("trace:archive:#{trace.job.id}", timeout: 1.hour)
end
it 'blocks concurrent archiving' do
diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb
index d5e22b8cb56..a401f7541f0 100644
--- a/spec/support/shared_examples/requests/api/merge_requests_list.rb
+++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb
@@ -29,7 +29,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
@@ -53,7 +53,7 @@ shared_examples 'merge requests list' do
expect(response).to include_pagination_headers
expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
expect(json_response.last['iid']).to eq(merge_request.iid)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
@@ -70,7 +70,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
expect(json_response.last['title']).to eq(merge_request.title)
end
@@ -216,7 +216,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
@@ -229,7 +229,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -242,7 +242,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -255,7 +255,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
@@ -265,7 +265,7 @@ shared_examples 'merge requests list' do
it 'returns merge requests with the given source branch' do
get api(endpoint_path, user), source_branch: merge_request_closed.source_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
@@ -273,7 +273,7 @@ shared_examples 'merge requests list' do
it 'returns merge requests with the given target branch' do
get api(endpoint_path, user), target_branch: merge_request_closed.target_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
end
diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
index 19800c6638f..1bd176280c5 100644
--- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
@@ -76,8 +76,10 @@ shared_examples "migrates" do |to_store:, from_store: nil|
end
context 'when migrate! is occupied by another process' do
+ include ExclusiveLeaseHelpers
+
before do
- @uuid = Gitlab::ExclusiveLease.new(subject.exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+ stub_exclusive_lease_taken(subject.exclusive_lease_key, timeout: 1.hour.to_i)
end
it 'does not execute migrate!' do
@@ -91,10 +93,6 @@ shared_examples "migrates" do |to_store:, from_store: nil|
expect { subject.use_file }.to raise_error(ObjectStorage::ExclusiveLeaseTaken)
end
-
- after do
- Gitlab::ExclusiveLease.cancel(subject.exclusive_lease_key, @uuid)
- end
end
context 'migration is unsuccessful' do
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 6b1f2ff3227..8c4daac5f80 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -1,49 +1,58 @@
require 'spec_helper'
describe ProjectCacheWorker do
+ include ExclusiveLeaseHelpers
+
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
let(:statistics) { project.statistics }
+ let(:lease_key) { "project_cache_worker:#{project.id}:update_statistics" }
+ let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT }
- describe '#perform' do
- before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return(true)
- end
+ before do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+ allow(Project).to receive(:find_by)
+ .with(id: project.id)
+ .and_return(project)
+ end
+
+ describe '#perform' do
context 'with a non-existing project' do
- it 'does nothing' do
- expect(worker).not_to receive(:update_statistics)
+ it 'does not update statistic' do
+ allow(Project).to receive(:find_by).with(id: -1).and_return(nil)
- worker.perform(-1)
+ expect(subject).not_to receive(:update_statistics)
+
+ subject.perform(-1)
end
end
context 'with an existing project without a repository' do
- it 'does nothing' do
- allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
+ it 'does not update statistics' do
+ allow(project.repository).to receive(:exists?).and_return(false)
- expect(worker).not_to receive(:update_statistics)
+ expect(subject).not_to receive(:update_statistics)
- worker.perform(project.id)
+ subject.perform(project.id)
end
end
context 'with an existing project' do
it 'updates the project statistics' do
- expect(worker).to receive(:update_statistics)
- .with(kind_of(Project), %i(repository_size))
- .and_call_original
+ expect(subject).to receive(:update_statistics)
+ .with(%w(repository_size))
+ .and_call_original
- worker.perform(project.id, [], %w(repository_size))
+ subject.perform(project.id, [], %w(repository_size))
end
it 'refreshes the method caches' do
- expect_any_instance_of(Repository).to receive(:refresh_method_caches)
- .with(%i(readme))
- .and_call_original
+ expect(project.repository).to receive(:refresh_method_caches)
+ .with(%i(readme))
+ .and_call_original
- worker.perform(project.id, %w(readme))
+ subject.perform(project.id, %w(readme))
end
context 'with plain readme' do
@@ -51,39 +60,40 @@ describe ProjectCacheWorker do
allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false)
allow(MarkupHelper).to receive(:plain?).and_return(true)
- expect_any_instance_of(Repository).to receive(:refresh_method_caches)
- .with(%i(readme))
- .and_call_original
- worker.perform(project.id, %w(readme))
+ expect(project.repository).to receive(:refresh_method_caches)
+ .with(%i(readme))
+ .and_call_original
+
+ subject.perform(project.id, %w(readme))
end
end
end
- end
- describe '#update_statistics' do
context 'when a lease could not be obtained' do
it 'does not update the repository size' do
- allow(worker).to receive(:try_obtain_lease_for)
- .with(project.id, :update_statistics)
- .and_return(false)
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- expect(statistics).not_to receive(:refresh!)
+ expect(project.statistics).not_to receive(:refresh!)
- worker.update_statistics(project)
+ subject.perform(project.id, [], %w(repository_size))
end
end
context 'when a lease could be obtained' do
it 'updates the project statistics' do
- allow(worker).to receive(:try_obtain_lease_for)
- .with(project.id, :update_statistics)
- .and_return(true)
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+
+ expect(project.statistics).to receive(:refresh!)
+ .with(only: %i(repository_size))
+ .and_call_original
+
+ subject.perform(project.id, [], %i(repository_size))
+ end
- expect(statistics).to receive(:refresh!)
- .with(only: %i(repository_size))
- .and_call_original
+ it 'cancels the lease after statistics has been updated' do
+ expect(subject).to receive(:release_lease).with('uuid')
- worker.update_statistics(project, %i(repository_size))
+ subject.perform(project.id, [], %i(repository_size))
end
end
end
diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
index 2e3951e7afc..9551e358af1 100644
--- a/spec/workers/project_migrate_hashed_storage_worker_spec.rb
+++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
@@ -1,53 +1,47 @@
require 'spec_helper'
describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
describe '#perform' do
let(:project) { create(:project, :empty_repo) }
- let(:pending_delete_project) { create(:project, :empty_repo, pending_delete: true) }
+ let(:lease_key) { "project_migrate_hashed_storage_worker:#{project.id}" }
+ let(:lease_timeout) { ProjectMigrateHashedStorageWorker::LEASE_TIMEOUT }
+
+ it 'skips when project no longer exists' do
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+
+ subject.perform(-1)
+ end
- context 'when have exclusive lease' do
- before do
- lease = subject.lease_for(project.id)
+ it 'skips when project is pending delete' do
+ pending_delete_project = create(:project, :empty_repo, pending_delete: true)
- allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease)
- allow(lease).to receive(:try_obtain).and_return(true)
- end
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- it 'skips when project no longer exists' do
- nonexistent_id = 999999999999
+ subject.perform(pending_delete_project.id)
+ end
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- subject.perform(nonexistent_id)
- end
+ it 'delegates removal to service class when have exclusive lease' do
+ stub_exclusive_lease(lease_key, 'uuid', timeout: lease_timeout)
- it 'skips when project is pending delete' do
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ migration_service = spy
- subject.perform(pending_delete_project.id)
- end
+ allow(::Projects::HashedStorageMigrationService)
+ .to receive(:new).with(project, subject.logger)
+ .and_return(migration_service)
- it 'delegates removal to service class' do
- service = double('service')
- expect(::Projects::HashedStorageMigrationService).to receive(:new).with(project, subject.logger).and_return(service)
- expect(service).to receive(:execute)
+ subject.perform(project.id)
- subject.perform(project.id)
- end
+ expect(migration_service).to have_received(:execute)
end
- context 'when dont have exclusive lease' do
- before do
- lease = subject.lease_for(project.id)
-
- allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease)
- allow(lease).to receive(:try_obtain).and_return(false)
- end
+ it 'skips when dont have lease when dont have exclusive lease' do
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- it 'skips when dont have lease' do
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- subject.perform(project.id)
- end
+ subject.perform(project.id)
end
end
end
diff --git a/spec/workers/propagate_service_template_worker_spec.rb b/spec/workers/propagate_service_template_worker_spec.rb
index b8b65ead9b3..af1fb80a51d 100644
--- a/spec/workers/propagate_service_template_worker_spec.rb
+++ b/spec/workers/propagate_service_template_worker_spec.rb
@@ -1,29 +1,29 @@
require 'spec_helper'
describe PropagateServiceTemplateWorker do
- let!(:service_template) do
- PushoverService.create(
- template: true,
- active: true,
- properties: {
- device: 'MyDevice',
- sound: 'mic',
- priority: 4,
- user_key: 'asdf',
- api_key: '123456789'
- })
- end
-
- before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return(true)
- end
+ include ExclusiveLeaseHelpers
describe '#perform' do
it 'calls the propagate service with the template' do
- expect(Projects::PropagateServiceTemplate).to receive(:propagate).with(service_template)
+ template = PushoverService.create(
+ template: true,
+ active: true,
+ properties: {
+ device: 'MyDevice',
+ sound: 'mic',
+ priority: 4,
+ user_key: 'asdf',
+ api_key: '123456789'
+ })
+
+ stub_exclusive_lease("propagate_service_template_worker:#{template.id}",
+ timeout: PropagateServiceTemplateWorker::LEASE_TIMEOUT)
+
+ expect(Projects::PropagateServiceTemplate)
+ .to receive(:propagate)
+ .with(template)
- subject.perform(service_template.id)
+ subject.perform(template.id)
end
end
end
diff --git a/spec/workers/prune_web_hook_logs_worker_spec.rb b/spec/workers/prune_web_hook_logs_worker_spec.rb
new file mode 100644
index 00000000000..d7d64a1f641
--- /dev/null
+++ b/spec/workers/prune_web_hook_logs_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe PruneWebHookLogsWorker do
+ describe '#perform' do
+ before do
+ hook = create(:project_hook)
+
+ 5.times do
+ create(:web_hook_log, web_hook: hook, created_at: 5.months.ago)
+ end
+
+ create(:web_hook_log, web_hook: hook, response_status: '404')
+ end
+
+ it 'removes all web hook logs older than one month' do
+ described_class.new.perform
+
+ expect(WebHookLog.count).to eq(1)
+ expect(WebHookLog.first.response_status).to eq('404')
+ end
+ end
+end
diff --git a/spec/workers/repository_remove_remote_worker_spec.rb b/spec/workers/repository_remove_remote_worker_spec.rb
index 5968c5da3c9..a653f6f926c 100644
--- a/spec/workers/repository_remove_remote_worker_spec.rb
+++ b/spec/workers/repository_remove_remote_worker_spec.rb
@@ -1,44 +1,50 @@
require 'rails_helper'
describe RepositoryRemoveRemoteWorker do
- subject(:worker) { described_class.new }
+ include ExclusiveLeaseHelpers
describe '#perform' do
- let(:remote_name) { 'joe'}
let!(:project) { create(:project, :repository) }
+ let(:remote_name) { 'joe'}
+ let(:lease_key) { "remove_remote_#{project.id}_#{remote_name}" }
+ let(:lease_timeout) { RepositoryRemoveRemoteWorker::LEASE_TIMEOUT }
- context 'when it cannot obtain lease' do
- it 'logs error' do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
-
- expect_any_instance_of(Repository).not_to receive(:remove_remote)
- expect(worker).to receive(:log_error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
-
- worker.perform(project.id, remote_name)
- end
+ it 'returns nil when project does not exist' do
+ expect(subject.perform(-1, 'remote_name')).to be_nil
end
- context 'when it gets the lease' do
+ context 'when project exists' do
before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true)
+ allow(Project)
+ .to receive(:find_by)
+ .with(id: project.id)
+ .and_return(project)
end
- context 'when project does not exist' do
- it 'returns nil' do
- expect(worker.perform(-1, 'remote_name')).to be_nil
- end
- end
+ it 'does not remove remote when cannot obtain lease' do
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
+
+ expect(project.repository)
+ .not_to receive(:remove_remote)
- context 'when project exists' do
- it 'removes remote from repository' do
- masterrev = project.repository.find_branch('master').dereferenced_target
+ expect(subject)
+ .to receive(:log_error)
+ .with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
- create_remote_branch(remote_name, 'remote_branch', masterrev)
+ subject.perform(project.id, remote_name)
+ end
- expect_any_instance_of(Repository).to receive(:remove_remote).with(remote_name).and_call_original
+ it 'removes remote from repository when obtain a lease' do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+ masterrev = project.repository.find_branch('master').dereferenced_target
+ create_remote_branch(remote_name, 'remote_branch', masterrev)
- worker.perform(project.id, remote_name)
- end
+ expect(project.repository)
+ .to receive(:remove_remote)
+ .with(remote_name)
+ .and_call_original
+
+ subject.perform(project.id, remote_name)
end
end
end
@@ -47,6 +53,7 @@ describe RepositoryRemoveRemoteWorker do
rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
project.repository.rugged
end
+
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
end
end
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
index 2605c14334f..856886e3df5 100644
--- a/spec/workers/stuck_ci_jobs_worker_spec.rb
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -1,14 +1,21 @@
require 'spec_helper'
describe StuckCiJobsWorker do
+ include ExclusiveLeaseHelpers
+
let!(:runner) { create :ci_runner }
let!(:job) { create :ci_build, runner: runner }
- let(:worker) { described_class.new }
- let(:exclusive_lease_uuid) { SecureRandom.uuid }
+ let(:trace_lease_key) { "trace:archive:#{job.id}" }
+ let(:trace_lease_uuid) { SecureRandom.uuid }
+ let(:worker_lease_key) { StuckCiJobsWorker::EXCLUSIVE_LEASE_KEY }
+ let(:worker_lease_uuid) { SecureRandom.uuid }
+
+ subject(:worker) { described_class.new }
before do
+ stub_exclusive_lease(worker_lease_key, worker_lease_uuid)
+ stub_exclusive_lease(trace_lease_key, trace_lease_uuid)
job.update!(status: status, updated_at: updated_at)
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
end
shared_examples 'job is dropped' do
@@ -44,16 +51,19 @@ describe StuckCiJobsWorker do
context 'when job was not updated for more than 1 day ago' do
let(:updated_at) { 2.days.ago }
+
it_behaves_like 'job is dropped'
end
context 'when job was updated in less than 1 day ago' do
let(:updated_at) { 6.hours.ago }
+
it_behaves_like 'job is unchanged'
end
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -65,11 +75,14 @@ describe StuckCiJobsWorker do
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
+
it_behaves_like 'job is dropped'
end
- context 'when job was updated in less than 1 hour ago' do
+ context 'when job was updated in less than 1
+ hour ago' do
let(:updated_at) { 30.minutes.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -80,11 +93,13 @@ describe StuckCiJobsWorker do
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
+
it_behaves_like 'job is dropped'
end
context 'when job was updated in less than 1 hour ago' do
let(:updated_at) { 30.minutes.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -93,6 +108,7 @@ describe StuckCiJobsWorker do
context "when job is #{status}" do
let(:status) { status }
let(:updated_at) { 2.days.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -119,23 +135,27 @@ describe StuckCiJobsWorker do
it 'is guard by exclusive lease when executed concurrently' do
expect(worker).to receive(:drop).at_least(:once).and_call_original
expect(worker2).not_to receive(:drop)
+
worker.perform
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false)
+
+ stub_exclusive_lease_taken(worker_lease_key)
+
worker2.perform
end
it 'can be executed in sequence' do
expect(worker).to receive(:drop).at_least(:once).and_call_original
expect(worker2).to receive(:drop).at_least(:once).and_call_original
+
worker.perform
worker2.perform
end
- it 'cancels exclusive lease after worker perform' do
- worker.perform
+ it 'cancels exclusive leases after worker perform' do
+ expect_to_cancel_exclusive_lease(trace_lease_key, trace_lease_uuid)
+ expect_to_cancel_exclusive_lease(worker_lease_key, worker_lease_uuid)
- expect(Gitlab::ExclusiveLease.new(described_class::EXCLUSIVE_LEASE_KEY, timeout: 1.hour))
- .not_to be_exists
+ worker.perform
end
end
end
diff --git a/vendor/assets/javascripts/date.format.js b/vendor/assets/javascripts/date.format.js
deleted file mode 100644
index 2c9b4825443..00000000000
--- a/vendor/assets/javascripts/date.format.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Date Format 1.2.3
- * (c) 2007-2009 Steven Levithan <stevenlevithan.com>
- * MIT license
- *
- * Includes enhancements by Scott Trenda <scott.trenda.net>
- * and Kris Kowal <cixar.com/~kris.kowal/>
- *
- * Accepts a date, a mask, or a date and a mask.
- * Returns a formatted version of the given date.
- * The date defaults to the current date/time.
- * The mask defaults to dateFormat.masks.default.
- */
- (function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global.dateFormat = factory());
- }(this, (function () { 'use strict';
- var dateFormat = function () {
- var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
- timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
- timezoneClip = /[^-+\dA-Z]/g,
- pad = function (val, len) {
- val = String(val);
- len = len || 2;
- while (val.length < len) val = "0" + val;
- return val;
- };
-
- // Regexes and supporting functions are cached through closure
- return function (date, mask, utc) {
- var dF = dateFormat;
-
- // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
- if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
- mask = date;
- date = undefined;
- }
-
- // Passing date through Date applies Date.parse, if necessary
- date = date ? new Date(date) : new Date;
- if (isNaN(date)) throw SyntaxError("invalid date");
-
- mask = String(dF.masks[mask] || mask || dF.masks["default"]);
-
- // Allow setting the utc argument via the mask
- if (mask.slice(0, 4) == "UTC:") {
- mask = mask.slice(4);
- utc = true;
- }
-
- var _ = utc ? "getUTC" : "get",
- d = date[_ + "Date"](),
- D = date[_ + "Day"](),
- m = date[_ + "Month"](),
- y = date[_ + "FullYear"](),
- H = date[_ + "Hours"](),
- M = date[_ + "Minutes"](),
- s = date[_ + "Seconds"](),
- L = date[_ + "Milliseconds"](),
- o = utc ? 0 : date.getTimezoneOffset(),
- flags = {
- d: d,
- dd: pad(d),
- ddd: dF.i18n.dayNames[D],
- dddd: dF.i18n.dayNames[D + 7],
- m: m + 1,
- mm: pad(m + 1),
- mmm: dF.i18n.monthNames[m],
- mmmm: dF.i18n.monthNames[m + 12],
- yy: String(y).slice(2),
- yyyy: y,
- h: H % 12 || 12,
- hh: pad(H % 12 || 12),
- H: H,
- HH: pad(H),
- M: M,
- MM: pad(M),
- s: s,
- ss: pad(s),
- l: pad(L, 3),
- L: pad(L > 99 ? Math.round(L / 10) : L),
- t: H < 12 ? "a" : "p",
- tt: H < 12 ? "am" : "pm",
- T: H < 12 ? "A" : "P",
- TT: H < 12 ? "AM" : "PM",
- Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
- o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
- S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
- };
-
- return mask.replace(token, function ($0) {
- return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
- });
- };
- }();
-
- // Some common format strings
- dateFormat.masks = {
- "default": "ddd mmm dd yyyy HH:MM:ss",
- shortDate: "m/d/yy",
- mediumDate: "mmm d, yyyy",
- longDate: "mmmm d, yyyy",
- fullDate: "dddd, mmmm d, yyyy",
- shortTime: "h:MM TT",
- mediumTime: "h:MM:ss TT",
- longTime: "h:MM:ss TT Z",
- isoDate: "yyyy-mm-dd",
- isoTime: "HH:MM:ss",
- isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
- isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
- };
-
- // Internationalization strings
- dateFormat.i18n = {
- dayNames: [
- "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
- "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
- ],
- monthNames: [
- "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
- "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
- ]
- };
-
- // For convenience...
- Date.prototype.format = function (mask, utc) {
- return dateFormat(this, mask, utc);
- };
-
- return dateFormat;
-})));
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index 8dd5fa36987..fb357639a69 100644
--- a/vendor/project_templates/express.tar.gz
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index 89337dc5c31..8454d2fc03b 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
index 31c90d0820f..55e25fdbe7c 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index ef7fa659d6e..30d49ad276a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2348,6 +2348,10 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+dateformat@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
+
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"