diff options
author | Mike Greiling <mike@pixelcog.com> | 2017-10-12 17:52:09 -0500 |
---|---|---|
committer | Mike Greiling <mike@pixelcog.com> | 2017-10-12 17:52:09 -0500 |
commit | 53e11239ce9e7f9513c890a78df41c611bcc2ae1 (patch) | |
tree | 08300d49b78dff43f95e52cc298fa8732b9a32a9 | |
parent | d8cc4540ab83c98a09b11d1654fac593c47384c8 (diff) | |
parent | 8d47e9f8e13c2ca43520b348dde0424fe6460cc9 (diff) | |
download | gitlab-ce-53e11239ce9e7f9513c890a78df41c611bcc2ae1.tar.gz |
Merge branch 'master' into sh-headless-chrome-support
* master: (104 commits)
Update licenses
fix a wrong method call in the refactor
fix a whitespace
fix for discussion
Update templates via:
Update doc accordingly to:
Cache issuable template names
Fix the format of rugged alternate directory list
Match full file path in FileDetector
Fixes from CSS refactor
Resolve "Prometheus service page shows error"
Explicit state integration deletion
Move all API authentication code to APIGuard
Shorten example translation for inclusive language
[ci-skip] add changelog
fix the merger override to remove source branch
add spec
Removed d3.js from the users and graphs bundle
Cleanup data-page attribute after each Karma test
Update template description wording
...
313 files changed, 4748 insertions, 3366 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b261bd5938..83e41a11e52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -296,9 +296,9 @@ might be edited to make them small and simple. Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker. -For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should -be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may -need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself. +For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should +be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may +need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself. If you want to create something yourself, consider opening an issue first to discuss whether it is interesting to include this in GitLab. @@ -658,7 +658,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [license-finder-doc]: doc/development/licensing.md [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues [polling-etag]: https://docs.gitlab.com/ce/development/polling.html -[testing]: doc/development/testing.md +[testing]: doc/development/testing_guide/index.md [^1]: Please note that specs other than JavaScript specs are considered backend code. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 8298bb08b2d..301092317fe 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.43.0 +0.46.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 5508e17c23b..c5b7013b9c5 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1,2 +1 @@ -5.9.3 - +5.9.4 @@ -398,7 +398,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.39.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.41.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 0b56058defc..5bf1d6a8bc2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -274,7 +274,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.39.0) + gitaly-proto (0.41.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -1023,7 +1023,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.39.0) + gitaly-proto (~> 0.41.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) diff --git a/PROCESS.md b/PROCESS.md index 5e65bb59246..06963243b25 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -1,4 +1,4 @@ -## GitLab Core Team & GitLab Inc. Contribution Process +## GitLab core team & GitLab Inc. contribution process --- diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 4f01345ee3b..622764107ad 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,8 +1,8 @@ /* eslint-disable class-methods-use-this */ -/* global Flash */ import _ from 'underscore'; import Cookies from 'js-cookie'; import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils'; +import Flash from './flash'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js index 8641a6fdae6..062577af385 100644 --- a/app/assets/javascripts/blob/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq_viewer.js @@ -1,9 +1,8 @@ -/* global Flash */ - +import Flash from '../flash'; import BalsamiqViewer from './balsamiq/balsamiq_viewer'; function onError() { - const flash = new window.Flash('Balsamiq file could not be loaded.'); + const flash = new Flash('Balsamiq file could not be loaded.'); return flash; } diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index a20c6ca7a21..583e5faa506 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -1,6 +1,5 @@ /* eslint-disable class-methods-use-this */ -/* global Flash */ - +import Flash from '../flash'; import FileTemplateTypeSelector from './template_selectors/type_selector'; import BlobCiYamlSelector from './template_selectors/ci_yaml_selector'; import DockerfileSelector from './template_selectors/dockerfile_selector'; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index e0b73f13d36..54132e8537b 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -1,4 +1,4 @@ -/* global Flash */ +import Flash from '../../flash'; import { handleLocationHash } from '../../lib/utils/common_utils'; export default class BlobViewer { diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 815248f38ee..ef4093b59e3 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,10 +1,10 @@ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* global BoardService */ -/* global Flash */ import _ from 'underscore'; import Vue from 'vue'; import VueResource from 'vue-resource'; +import Flash from '../flash'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; import './models/issue'; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 590b7be36e3..7f3afefc9cc 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -3,9 +3,9 @@ /* global MilestoneSelect */ /* global LabelsSelect */ /* global Sidebar */ -/* global Flash */ import Vue from 'vue'; +import Flash from '../../flash'; import eventHub from '../../sidebar/event_hub'; import AssigneeTitle from '../../sidebar/components/assignees/assignee_title'; import Assignees from '../../sidebar/components/assignees/assignees'; diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index a656f0546c0..de9e44cef35 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -1,7 +1,7 @@ /* eslint-disable no-new */ -/* global Flash */ import Vue from 'vue'; +import Flash from '../../../flash'; import './lists_dropdown'; const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 1e623cf58b7..1ad97211934 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -1,7 +1,7 @@ /* eslint-disable no-new */ -/* global Flash */ import Vue from 'vue'; +import Flash from '../../../flash'; const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index 19388f1f9ae..ace89398943 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -1,30 +1,30 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */ +/* eslint-disable func-names, prefer-arrow-callback, no-return-assign */ import { visitUrl } from './lib/utils/url_utility'; import { convertPermissionToBoolean } from './lib/utils/common_utils'; -window.BuildArtifacts = (function() { - function BuildArtifacts() { +export default class BuildArtifacts { + constructor() { this.disablePropagation(); this.setupEntryClick(); this.setupTooltips(); } - - BuildArtifacts.prototype.disablePropagation = function() { - $('.top-block').on('click', '.download', function(e) { + // eslint-disable-next-line class-methods-use-this + disablePropagation() { + $('.top-block').on('click', '.download', function (e) { return e.stopPropagation(); }); - return $('.tree-holder').on('click', 'tr[data-link] a', function(e) { + return $('.tree-holder').on('click', 'tr[data-link] a', function (e) { return e.stopImmediatePropagation(); }); - }; - - BuildArtifacts.prototype.setupEntryClick = function() { - return $('.tree-holder').on('click', 'tr[data-link]', function(e) { + } + // eslint-disable-next-line class-methods-use-this + setupEntryClick() { + return $('.tree-holder').on('click', 'tr[data-link]', function () { visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink)); }); - }; - - BuildArtifacts.prototype.setupTooltips = function() { + } + // eslint-disable-next-line class-methods-use-this + setupTooltips() { $('.js-artifact-tree-tooltip').tooltip({ placement: 'bottom', // Stop the tooltip from hiding when we stop hovering the element directly @@ -41,7 +41,5 @@ window.BuildArtifacts = (function() { .on('mouseleave', (e) => { $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide'); }); - }; - - return BuildArtifacts; -})(); + } +} diff --git a/app/assets/javascripts/build_variables.js b/app/assets/javascripts/build_variables.js index c955a9ac2ea..35edf3e0017 100644 --- a/app/assets/javascripts/build_variables.js +++ b/app/assets/javascripts/build_variables.js @@ -1,8 +1,10 @@ -/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */ +/* eslint-disable func-names*/ -$(function() { - $('.reveal-variables').off('click').on('click', function() { - $('.js-build-variables').toggle(); - $(this).hide(); - }); -}); +export default function handleRevealVariables() { + $('.js-reveal-variables') + .off('click') + .on('click', function () { + $('.js-build-variables').toggle(); + $(this).hide(); + }); +} diff --git a/app/assets/javascripts/ci_lint_editor.js b/app/assets/javascripts/ci_lint_editor.js index dd4a08a2f31..b9469e5b7cb 100644 --- a/app/assets/javascripts/ci_lint_editor.js +++ b/app/assets/javascripts/ci_lint_editor.js @@ -1,7 +1,4 @@ - -window.gl = window.gl || {}; - -class CILintEditor { +export default class CILintEditor { constructor() { this.editor = window.ace.edit('ci-editor'); this.textarea = document.querySelector('#content'); @@ -13,5 +10,3 @@ class CILintEditor { }); } } - -gl.CILintEditor = CILintEditor; diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index ff2f2c81971..bf40eb3ee11 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -1,5 +1,5 @@ /* eslint-disable no-new */ -/* global Flash */ +import Flash from './flash'; import DropLab from './droplab/drop_lab'; import ISetter from './droplab/plugins/input_setter'; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index cdf5e3c0290..49bb6c52180 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -1,7 +1,6 @@ -/* global Flash */ - import Vue from 'vue'; import Cookies from 'js-cookie'; +import Flash from '../flash'; import Translate from '../vue_shared/translate'; import banner from './components/banner.vue'; import stageCodeComponent from './components/stage_code_component.vue'; diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index a663e30dfd0..54e13b79a4f 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -1,5 +1,5 @@ <script> - /* global Flash */ + import Flash from '../../flash'; import eventHub from '../eventhub'; import DeployKeysService from '../service'; import DeployKeysStore from '../store'; diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index efb6ced9f46..20ddcbfb8bd 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -1,9 +1,9 @@ /* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */ /* global CommentsStore */ /* global ResolveService */ -/* global Flash */ import Vue from 'vue'; +import Flash from '../../flash'; const ResolveBtn = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index 2f063f6fe1f..6eae54f830b 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -1,7 +1,7 @@ -/* global Flash */ /* global CommentsStore */ import Vue from 'vue'; +import Flash from '../../flash'; import '../../vue_shared/vue_resource_interceptor'; window.gl = window.gl || {}; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 1edd460f380..0a653d7fefc 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -12,7 +12,8 @@ /* global NotificationsDropdown */ /* global GroupAvatar */ /* global LineHighlighter */ -/* global BuildArtifacts */ +import BuildArtifacts from './build_artifacts'; +import CILintEditor from './ci_lint_editor'; /* global GroupsSelect */ /* global Search */ /* global Admin */ @@ -28,7 +29,8 @@ /* global ProjectNew */ /* global ProjectShow */ /* global ProjectImport */ -/* global Labels */ +import Labels from './labels'; +import LabelManager from './label_manager'; /* global Shortcuts */ /* global ShortcutsFindFile */ /* global Sidebar */ @@ -78,6 +80,9 @@ import initChangesDropdown from './init_changes_dropdown'; import AbuseReports from './abuse_reports'; import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; import AjaxLoadingSpinner from './ajax_loading_spinner'; +import GlFieldErrors from './gl_field_errors'; +import GLForm from './gl_form'; +import U2FAuthenticate from './u2f/authenticate'; (function() { var Dispatcher; @@ -90,8 +95,8 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; } Dispatcher.prototype.initPageScripts = function() { - var page, path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl; - page = $('body').attr('data-page'); + var path, shortcut_handler, fileBlobPermalinkUrlElement, fileBlobPermalinkUrl; + const page = $('body').attr('data-page'); if (!page) { return false; } @@ -228,7 +233,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; case 'groups:milestones:update': new ZenMode(); new gl.DueDateSelectors(); - new gl.GLForm($('.milestone-form'), true); + new GLForm($('.milestone-form'), true); break; case 'projects:compare:show': new gl.Diff(); @@ -245,7 +250,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; case 'projects:issues:new': case 'projects:issues:edit': shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.issue-form'), true); + new GLForm($('.issue-form'), true); new IssuableForm($('.issue-form')); new LabelsSelect(); new MilestoneSelect(); @@ -269,7 +274,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; case 'projects:merge_requests:edit': new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); - new gl.GLForm($('.merge-request-form'), true); + new GLForm($('.merge-request-form'), true); new IssuableForm($('.merge-request-form')); new LabelsSelect(); new MilestoneSelect(); @@ -278,7 +283,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; break; case 'projects:tags:new': new ZenMode(); - new gl.GLForm($('.tag-form'), true); + new GLForm($('.tag-form'), true); new RefSelectDropdown($('.js-branch-select')); break; case 'projects:snippets:show': @@ -288,17 +293,17 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; case 'projects:snippets:edit': case 'projects:snippets:create': case 'projects:snippets:update': - new gl.GLForm($('.snippet-form'), true); + new GLForm($('.snippet-form'), true); break; case 'snippets:new': case 'snippets:edit': case 'snippets:create': case 'snippets:update': - new gl.GLForm($('.snippet-form'), false); + new GLForm($('.snippet-form'), false); break; case 'projects:releases:edit': new ZenMode(); - new gl.GLForm($('.release-form'), true); + new GLForm($('.release-form'), true); break; case 'projects:merge_requests:show': new gl.Diff(); @@ -457,7 +462,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; case 'groups:labels:index': case 'projects:labels:index': if ($('.prioritized-labels').length) { - new gl.LabelManager(); + new LabelManager(); } $('.label-subscription').each((i, el) => { const $el = $(el); @@ -505,7 +510,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; break; case 'ci:lints:create': case 'ci:lints:show': - new gl.CILintEditor(); + new CILintEditor(); break; case 'users:show': new UserCallout(); @@ -535,14 +540,16 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; case 'sessions': case 'omniauth_callbacks': if (!gon.u2f) break; - gl.u2fAuthenticate = new gl.U2FAuthenticate( + const u2fAuthenticate = new U2FAuthenticate( $('#js-authenticate-u2f'), '#js-login-u2f-form', gon.u2f, document.querySelector('#js-login-2fa-device'), document.querySelector('.js-2fa-form'), ); - gl.u2fAuthenticate.start(); + u2fAuthenticate.start(); + // needed in rspec + gl.u2fAuthenticate = u2fAuthenticate; case 'admin': new Admin(); switch (path[1]) { @@ -602,7 +609,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; new Wikis(); shortcut_handler = new ShortcutsWiki(); new ZenMode(); - new gl.GLForm($('.wiki-form'), true); + new GLForm($('.wiki-form'), true); break; case 'snippets': shortcut_handler = new ShortcutsNavigation(); @@ -627,12 +634,6 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; shortcut_handler = new ShortcutsNavigation(); } break; - case 'users': - const action = path[1]; - import(/* webpackChunkName: 'user_profile' */ './users') - .then(user => user.default(action)) - .catch(() => {}); - break; } // If we haven't installed a custom shortcut handler, install the default one if (!shortcut_handler) { @@ -653,7 +654,7 @@ import AjaxLoadingSpinner from './ajax_loading_spinner'; Dispatcher.prototype.initFieldErrors = function() { $('.gl-show-field-errors').each((i, form) => { - new gl.GlFieldErrors(form); + new GlFieldErrors(form); }); }; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 1cba65d17cd..bd45da8c422 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -237,9 +237,12 @@ window.DropzoneInput = (function() { }; const insertToTextArea = function(filename, url) { - return $(child).val(function(index, val) { + const $child = $(child); + $child.val(function(index, val) { return val.replace(`{{${filename}}}`, url); }); + + $child.trigger('change'); }; const appendToTextArea = function(url) { diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index ce5f6219a3e..c039ae85cfb 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -1,6 +1,6 @@ <script> -/* global Flash */ import Visibility from 'visibilityjs'; +import Flash from '../../flash'; import EnvironmentsService from '../services/environments_service'; import environmentTable from './environments_table.vue'; import EnvironmentsStore from '../stores/environments_store'; diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 01e70c0bbb7..b155560df9d 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -1,6 +1,6 @@ <script> -/* global Flash */ import Visibility from 'visibilityjs'; +import Flash from '../../flash'; import EnvironmentsService from '../services/environments_service'; import environmentTable from '../components/environments_table.vue'; import EnvironmentsStore from '../stores/environments_store'; diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js index ada14d2053c..a6cc079d720 100644 --- a/app/assets/javascripts/filtered_search/dropdown_emoji.js +++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js @@ -1,7 +1,6 @@ -/* global Flash */ - -import Ajax from '~/droplab/plugins/ajax'; -import Filter from '~/droplab/plugins/filter'; +import Flash from '../flash'; +import Ajax from '../droplab/plugins/ajax'; +import Filter from '../droplab/plugins/filter'; import './filtered_search_dropdown'; class DropdownEmoji extends gl.FilteredSearchDropdown { diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index b32d589481d..788fb1dc614 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -1,7 +1,6 @@ -/* global Flash */ - -import Ajax from '~/droplab/plugins/ajax'; -import Filter from '~/droplab/plugins/filter'; +import Flash from '../flash'; +import Ajax from '../droplab/plugins/ajax'; +import Filter from '../droplab/plugins/filter'; import './filtered_search_dropdown'; class DropdownNonUser extends gl.FilteredSearchDropdown { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index ce8817b1b2e..a9e2b65def0 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -1,6 +1,5 @@ -/* global Flash */ - -import AjaxFilter from '~/droplab/plugins/ajax_filter'; +import Flash from '../flash'; +import AjaxFilter from '../droplab/plugins/ajax_filter'; import './filtered_search_dropdown'; import { addClassIfElementExists } from '../lib/utils/dom_utils'; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index a44dc279a6f..7b233842d5a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,3 +1,4 @@ +import Flash from '../flash'; import FilteredSearchContainer from './container'; import RecentSearchesRoot from './recent_searches_root'; import RecentSearchesStore from './stores/recent_searches_store'; @@ -36,7 +37,7 @@ class FilteredSearchManager { .catch((error) => { if (error.name === 'RecentSearchesServiceError') return undefined; // eslint-disable-next-line no-new - new window.Flash('An error occurred while parsing recent searches'); + new Flash('An error occurred while parsing recent searches'); // Gracefully fail to empty array return []; }) diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 28e8240169d..dd24fc44d2a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,5 +1,5 @@ import AjaxCache from '../lib/utils/ajax_cache'; -import '../flash'; /* global Flash */ +import Flash from '../flash'; import FilteredSearchContainer from './container'; import UsersCache from '../lib/utils/users_cache'; diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index ccff8f0ace7..bc5cd818e1c 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,71 +1,94 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */ +import _ from 'underscore'; -window.Flash = (function() { - var hideFlash; +const hideFlash = (flashEl, fadeTransition = true) => { + if (fadeTransition) { + Object.assign(flashEl.style, { + transition: 'opacity .3s', + opacity: '0', + }); + } - hideFlash = function() { - return $(this).fadeOut(); - }; + flashEl.addEventListener('transitionend', () => { + flashEl.remove(); + }, { + once: true, + passive: true, + }); - /** - * Flash banner supports different types of Flash configurations - * along with ability to provide actionConfig which can be used to show - * additional action or link on banner next to message - * - * @param {String} message Flash message - * @param {String} type Type of Flash, it can be `notice` or `alert` (default) - * @param {Object} parent Reference to Parent element under which Flash needs to appear - * @param {Object} actionConfig Map of config to show action on banner - * @param {String} href URL to which action link should point (default '#') - * @param {String} title Title of action - * @param {Function} clickHandler Method to call when action is clicked on - */ - function Flash(message, type, parent, actionConfig) { - var flash, textDiv, actionLink; - if (type == null) { - type = 'alert'; - } - if (parent == null) { - parent = null; - } - if (parent) { - this.flashContainer = parent.find('.flash-container'); - } else { - this.flashContainer = $('.flash-container-page'); - } - this.flashContainer.html(''); - flash = $('<div/>', { - "class": "flash-" + type - }); - flash.on('click', hideFlash); - textDiv = $('<div/>', { - "class": 'flash-text', - text: message - }); - textDiv.appendTo(flash); + if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend')); +}; - if (actionConfig) { - const actionLinkConfig = { - class: 'flash-action', - href: actionConfig.href || '#', - text: actionConfig.title - }; +const createAction = config => ` + <a + href="${config.href || '#'}" + class="flash-action" + ${config.href ? '' : 'role="button"'} + > + ${_.escape(config.title)} + </a> +`; - if (!actionConfig.href) { - actionLinkConfig.role = 'button'; - } +const createFlashEl = (message, type, isInContentWrapper = false) => ` + <div + class="flash-${type}" + > + <div + class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}" + > + ${_.escape(message)} + </div> + </div> +`; - actionLink = $('<a/>', actionLinkConfig); +/* + * Flash banner supports different types of Flash configurations + * along with ability to provide actionConfig which can be used to show + * additional action or link on banner next to message + * + * @param {String} message Flash message text + * @param {String} type Type of Flash, it can be `notice` or `alert` (default) + * @param {Object} parent Reference to parent element under which Flash needs to appear + * @param {Object} actonConfig Map of config to show action on banner + * @param {String} href URL to which action config should point to (default: '#') + * @param {String} title Title of action + * @param {Function} clickHandler Method to call when action is clicked on + * @param {Boolean} fadeTransition Boolean to determine whether to fade the alert out + */ +const createFlash = function createFlash( + message, + type = 'alert', + parent = document, + actionConfig = null, + fadeTransition = true, +) { + const flashContainer = parent.querySelector('.flash-container'); - actionLink.appendTo(flash); - this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler); - } - if (this.flashContainer.parent().hasClass('content-wrapper')) { - textDiv.addClass('container-fluid container-limited'); + if (!flashContainer) return null; + + const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper'); + + flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper); + + const flashEl = flashContainer.querySelector(`.flash-${type}`); + flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition)); + + if (actionConfig) { + flashEl.innerHTML += createAction(actionConfig); + + if (actionConfig.clickHandler) { + flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e)); } - flash.appendTo(this.flashContainer); - this.flashContainer.show(); } - return Flash; -})(); + flashContainer.style.display = 'block'; + + return flashContainer; +}; + +export { + createFlash as default, + createFlashEl, + createAction, + hideFlash, +}; +window.Flash = createFlash; diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js index 0add7075254..bd63f6f16f0 100644 --- a/app/assets/javascripts/gl_field_error.js +++ b/app/assets/javascripts/gl_field_error.js @@ -54,7 +54,7 @@ const inputErrorClass = 'gl-field-error-outline'; const errorAnchorSelector = '.gl-field-error-anchor'; const ignoreInputSelector = '.gl-field-error-ignore'; -class GlFieldError { +export default class GlFieldError { constructor({ input, formErrors }) { this.inputElement = $(input); this.inputDomElement = this.inputElement.get(0); @@ -159,6 +159,3 @@ class GlFieldError { this.fieldErrorElement.hide(); } } - -window.gl = window.gl || {}; -window.gl.GlFieldError = GlFieldError; diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index 4bef60264bb..73bcbd93565 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -1,42 +1,40 @@ -/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ - -import './gl_field_error'; +import GlFieldError from './gl_field_error'; const customValidationFlag = 'gl-field-error-ignore'; -class GlFieldErrors { +export default class GlFieldErrors { constructor(form) { this.form = $(form); this.state = { inputs: [], - valid: false + valid: false, }; this.initValidators(); } - initValidators () { + initValidators() { // register selectors here as needed const validateSelectors = [':text', ':password', '[type=email]'] - .map((selector) => `input${selector}`).join(','); + .map(selector => `input${selector}`).join(','); this.state.inputs = this.form.find(validateSelectors).toArray() - .filter((input) => !input.classList.contains(customValidationFlag)) - .map((input) => new window.gl.GlFieldError({ input, formErrors: this })); + .filter(input => !input.classList.contains(customValidationFlag)) + .map(input => new GlFieldError({ input, formErrors: this })); - this.form.on('submit', this.catchInvalidFormSubmit); + this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit); } /* Neccessary to prevent intercept and override invalid form submit * because Safari & iOS quietly allow form submission when form is invalid * and prevents disabling of invalid submit button by application.js */ - catchInvalidFormSubmit (event) { - const $form = $(event.currentTarget); + static catchInvalidFormSubmit(e) { + const $form = $(e.currentTarget); if (!$form.attr('novalidate')) { - if (!event.currentTarget.checkValidity()) { - event.preventDefault(); - event.stopPropagation(); + if (!e.currentTarget.checkValidity()) { + e.preventDefault(); + e.stopPropagation(); } } } @@ -50,11 +48,9 @@ class GlFieldErrors { }); } - focusOnFirstInvalid () { - const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; + focusOnFirstInvalid() { + const firstInvalid = this.state.inputs + .filter(input => !input.inputDomElement.validity.valid)[0]; firstInvalid.inputElement.focus(); } } - -window.gl = window.gl || {}; -window.gl.GlFieldErrors = GlFieldErrors; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 4e8141b2956..48d0c12143a 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,104 +1,99 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ -/* global GitLab */ /* global DropzoneInput */ /* global autosize */ import GfmAutoComplete from './gfm_auto_complete'; -window.gl = window.gl || {}; - -function GLForm(form, enableGFM = false) { - this.form = form; - this.textarea = this.form.find('textarea.js-gfm-input'); - this.enableGFM = enableGFM; - // Before we start, we should clean up any previous data for this form - this.destroy(); - // Setup the form - this.setupForm(); - this.form.data('gl-form', this); -} - -GLForm.prototype.destroy = function() { - // Clean form listeners - this.clearEventListeners(); - if (this.autoComplete) { - this.autoComplete.destroy(); +export default class GLForm { + constructor(form, enableGFM = false) { + this.form = form; + this.textarea = this.form.find('textarea.js-gfm-input'); + this.enableGFM = enableGFM; + // Before we start, we should clean up any previous data for this form + this.destroy(); + // Setup the form + this.setupForm(); + this.form.data('gl-form', this); } - return this.form.data('gl-form', null); -}; -GLForm.prototype.setupForm = function() { - var isNewForm; - isNewForm = this.form.is(':not(.gfm-form)'); - this.form.removeClass('js-new-note-form'); - if (isNewForm) { - this.form.find('.div-dropzone').remove(); - this.form.addClass('gfm-form'); - // remove notify commit author checkbox for non-commit notes - gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); - this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); - this.autoComplete.setup(this.form.find('.js-gfm-input'), { - emojis: true, - members: this.enableGFM, - issues: this.enableGFM, - milestones: this.enableGFM, - mergeRequests: this.enableGFM, - labels: this.enableGFM, - }); - new DropzoneInput(this.form); - autosize(this.textarea); + destroy() { + // Clean form listeners + this.clearEventListeners(); + if (this.autoComplete) { + this.autoComplete.destroy(); + } + this.form.data('gl-form', null); } - // form and textarea event listeners - this.addEventListeners(); - gl.text.init(this.form); - // hide discard button - this.form.find('.js-note-discard').hide(); - this.form.show(); - if (this.isAutosizeable) this.setupAutosize(); -}; -GLForm.prototype.setupAutosize = function () { - this.textarea.off('autosize:resized') - .on('autosize:resized', this.setHeightData.bind(this)); + setupForm() { + const isNewForm = this.form.is(':not(.gfm-form)'); + this.form.removeClass('js-new-note-form'); + if (isNewForm) { + this.form.find('.div-dropzone').remove(); + this.form.addClass('gfm-form'); + // remove notify commit author checkbox for non-commit notes + gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); + this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); + this.autoComplete.setup(this.form.find('.js-gfm-input'), { + emojis: true, + members: this.enableGFM, + issues: this.enableGFM, + milestones: this.enableGFM, + mergeRequests: this.enableGFM, + labels: this.enableGFM, + }); + new DropzoneInput(this.form); // eslint-disable-line no-new + autosize(this.textarea); + } + // form and textarea event listeners + this.addEventListeners(); + gl.text.init(this.form); + // hide discard button + this.form.find('.js-note-discard').hide(); + this.form.show(); + if (this.isAutosizeable) this.setupAutosize(); + } - this.textarea.off('mouseup.autosize') - .on('mouseup.autosize', this.destroyAutosize.bind(this)); + setupAutosize() { + this.textarea.off('autosize:resized') + .on('autosize:resized', this.setHeightData.bind(this)); - setTimeout(() => { - autosize(this.textarea); - this.textarea.css('resize', 'vertical'); - }, 0); -}; + this.textarea.off('mouseup.autosize') + .on('mouseup.autosize', this.destroyAutosize.bind(this)); -GLForm.prototype.setHeightData = function () { - this.textarea.data('height', this.textarea.outerHeight()); -}; + setTimeout(() => { + autosize(this.textarea); + this.textarea.css('resize', 'vertical'); + }, 0); + } -GLForm.prototype.destroyAutosize = function () { - const outerHeight = this.textarea.outerHeight(); + setHeightData() { + this.textarea.data('height', this.textarea.outerHeight()); + } - if (this.textarea.data('height') === outerHeight) return; + destroyAutosize() { + const outerHeight = this.textarea.outerHeight(); - autosize.destroy(this.textarea); + if (this.textarea.data('height') === outerHeight) return; - this.textarea.data('height', outerHeight); - this.textarea.outerHeight(outerHeight); - this.textarea.css('max-height', window.outerHeight); -}; + autosize.destroy(this.textarea); -GLForm.prototype.clearEventListeners = function() { - this.textarea.off('focus'); - this.textarea.off('blur'); - return gl.text.removeListeners(this.form); -}; + this.textarea.data('height', outerHeight); + this.textarea.outerHeight(outerHeight); + this.textarea.css('max-height', window.outerHeight); + } -GLForm.prototype.addEventListeners = function() { - this.textarea.on('focus', function() { - return $(this).closest('.md-area').addClass('is-focused'); - }); - return this.textarea.on('blur', function() { - return $(this).closest('.md-area').removeClass('is-focused'); - }); -}; + clearEventListeners() { + this.textarea.off('focus'); + this.textarea.off('blur'); + gl.text.removeListeners(this.form); + } -window.gl.GLForm = GLForm; + addEventListeners() { + this.textarea.on('focus', function focusTextArea() { + $(this).closest('.md-area').addClass('is-focused'); + }); + this.textarea.on('blur', function blurTextArea() { + $(this).closest('.md-area').removeClass('is-focused'); + }); + } +} diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 9ad8e5c6052..600bae24b52 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -1,6 +1,5 @@ -/* global Flash */ - import Vue from 'vue'; +import Flash from '../flash'; import GroupFilterableList from './groups_filterable_list'; import GroupsComponent from './components/groups.vue'; import GroupFolder from './components/group_folder.vue'; diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index dc170c60456..ea2e2205077 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,7 +1,16 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */ +import { highCountTrim } from '~/lib/utils/text_utility'; -$(document).on('todo:toggle', function(e, count) { - var $todoPendingCount = $('.todos-count'); - $todoPendingCount.text(gl.text.highCountTrim(count)); - $todoPendingCount.toggleClass('hidden', count === 0); +/** + * Updates todo counter when todos are toggled. + * When count is 0, we hide the badge. + * + * @param {jQuery.Event} e + * @param {String} count + */ +$(document).on('todo:toggle', (e, count) => { + const parsedCount = parseInt(count, 10); + const $todoPendingCount = $('.todos-count'); + + $todoPendingCount.text(highCountTrim(parsedCount)); + $todoPendingCount.toggleClass('hidden', parsedCount === 0); }); diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index cf1e6a14725..32415a8791f 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -1,4 +1,4 @@ -/* global Flash */ +import Flash from '../flash'; export default class IntegrationSettingsForm { constructor(formSelector) { @@ -102,7 +102,7 @@ export default class IntegrationSettingsForm { }) .done((res) => { if (res.error) { - new Flash(`${res.message} ${res.service_response}`, null, null, { + new Flash(`${res.message} ${res.service_response}`, 'alert', document, { title: 'Save anyway', clickHandler: (e) => { e.preventDefault(); diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index c39ffdb2e0f..eb15949603f 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ /* global IssuableIndex */ -/* global Flash */ import _ from 'underscore'; +import Flash from './flash'; export default { init({ container, form, issues, prefixId } = {}) { diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index c0bd64814ca..3fc29f9a661 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,9 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ -/* global Flash */ - import 'vendor/jquery.waitforimages'; import '~/lib/utils/text_utility'; -import './flash'; +import Flash from './flash'; import TaskList from './task_list'; import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import IssuablesHelper from './helpers/issuables_helper'; diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 06f6ec241f4..eecb56cb185 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,5 +1,4 @@ <script> -/* global Flash */ import Visibility from 'visibilityjs'; import Poll from '../../lib/utils/poll'; import eventHub from '../event_hub'; @@ -153,7 +152,7 @@ export default { }) .catch(() => { eventHub.$emit('close.form'); - return new Flash('Error updating issue'); + window.Flash('Error updating issue'); }); }, deleteIssuable() { @@ -167,7 +166,7 @@ export default { }) .catch(() => { eventHub.$emit('close.form'); - return new Flash('Error deleting issue'); + window.Flash('Error deleting issue'); }); }, }, diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index dc902eefc5f..0aa1b2c2e31 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -1,5 +1,4 @@ <script> - /* global Flash */ import updateMixin from '../../mixins/update'; import markdownField from '../../../vue_shared/components/markdown/field.vue'; diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/job.js index 3d27a3544eb..c6b5844dff6 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/job.js @@ -1,15 +1,12 @@ -/* eslint-disable func-names, wrap-iife, no-use-before-define, -consistent-return, prefer-rest-params */ import _ from 'underscore'; import bp from './breakpoints'; import { bytesToKiB } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; -window.Build = (function () { - Build.timeout = null; - Build.state = null; - - function Build(options) { +export default class Job { + constructor(options) { + this.timeout = null; + this.state = null; this.options = options || $('.js-build-options').data(); this.pageUrl = this.options.pageUrl; @@ -19,9 +16,7 @@ window.Build = (function () { this.$document = $(document); this.logBytes = 0; this.hasBeenScrolled = false; - this.updateDropdown = this.updateDropdown.bind(this); - this.getBuildTrace = this.getBuildTrace.bind(this); this.$buildTrace = $('#build-trace'); this.$buildRefreshAnimation = $('.js-build-refresh'); @@ -33,7 +28,7 @@ window.Build = (function () { this.$scrollTopBtn = $('.js-scroll-up'); this.$scrollBottomBtn = $('.js-scroll-down'); - clearTimeout(Build.timeout); + clearTimeout(this.timeout); this.initSidebar(); this.populateJobs(this.buildStage); @@ -85,7 +80,7 @@ window.Build = (function () { this.getBuildTrace(); } - Build.prototype.initAffixTopArea = function () { + initAffixTopArea() { /** If the browser does not support position sticky, it returns the position as static. If the browser does support sticky, then we allow the browser to handle it, if not @@ -100,13 +95,14 @@ window.Build = (function () { top: offsetTop, }, }); - }; + } - Build.prototype.canScroll = function () { + // eslint-disable-next-line class-methods-use-this + canScroll() { return $(document).height() > $(window).height(); - }; + } - Build.prototype.toggleScroll = function () { + toggleScroll() { const currentPosition = $(document).scrollTop(); const scrollHeight = $(document).height(); @@ -119,7 +115,7 @@ window.Build = (function () { this.toggleDisableButton(this.$scrollTopBtn, false); this.toggleDisableButton(this.$scrollBottomBtn, false); } else if (currentPosition === 0) { - // User is at Top of Build Log + // User is at Top of Log this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollBottomBtn, false); @@ -133,38 +129,40 @@ window.Build = (function () { this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollBottomBtn, true); } - }; + } - Build.prototype.scrollDown = function () { + // eslint-disable-next-line class-methods-use-this + scrollDown() { $(document).scrollTop($(document).height()); - }; + } - Build.prototype.scrollToBottom = function () { + scrollToBottom() { this.scrollDown(); this.hasBeenScrolled = true; this.toggleScroll(); - }; + } - Build.prototype.scrollToTop = function () { + scrollToTop() { $(document).scrollTop(0); this.hasBeenScrolled = true; this.toggleScroll(); - }; + } - Build.prototype.toggleDisableButton = function ($button, disable) { + // eslint-disable-next-line class-methods-use-this + toggleDisableButton($button, disable) { if (disable && $button.prop('disabled')) return; $button.prop('disabled', disable); - }; + } - Build.prototype.toggleScrollAnimation = function (toggle) { + toggleScrollAnimation(toggle) { this.$scrollBottomBtn.toggleClass('animate', toggle); - }; + } - Build.prototype.initSidebar = function () { + initSidebar() { this.$sidebar = $('.js-build-sidebar'); - }; + } - Build.prototype.getBuildTrace = function () { + getBuildTrace() { return $.ajax({ url: `${this.pageUrl}/trace.json`, data: { state: this.state }, @@ -204,7 +202,7 @@ window.Build = (function () { this.toggleScrollAnimation(false); } - Build.timeout = setTimeout(() => { + this.timeout = setTimeout(() => { this.getBuildTrace(); }, 4000); } else { @@ -225,14 +223,14 @@ window.Build = (function () { } }) .then(() => this.toggleScroll()); - }; - - Build.prototype.shouldHideSidebarForViewport = function () { + } + // eslint-disable-next-line class-methods-use-this + shouldHideSidebarForViewport() { const bootstrapBreakpoint = bp.getBreakpointSize(); return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; - }; + } - Build.prototype.toggleSidebar = function (shouldHide) { + toggleSidebar(shouldHide) { const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; const $toggleButton = $('.js-sidebar-build-toggle-header'); @@ -249,17 +247,17 @@ window.Build = (function () { } else { $toggleButton.removeClass('hidden'); } - }; + } - Build.prototype.sidebarOnResize = function () { + sidebarOnResize() { this.toggleSidebar(this.shouldHideSidebarForViewport()); - }; + } - Build.prototype.sidebarOnClick = function () { + sidebarOnClick() { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); - }; - - Build.prototype.updateArtifactRemoveDate = function () { + } + // eslint-disable-next-line class-methods-use-this, consistent-return + updateArtifactRemoveDate() { const $date = $('.js-artifacts-remove'); if ($date.length) { const date = $date.text(); @@ -267,23 +265,21 @@ window.Build = (function () { gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '), ); } - }; - - Build.prototype.populateJobs = function (stage) { + } + // eslint-disable-next-line class-methods-use-this + populateJobs(stage) { $('.build-job').hide(); $(`.build-job[data-stage="${stage}"]`).show(); - }; - - Build.prototype.updateStageDropdownText = function (stage) { + } + // eslint-disable-next-line class-methods-use-this + updateStageDropdownText(stage) { $('.stage-selection').text(stage); - }; + } - Build.prototype.updateDropdown = function (e) { + updateDropdown(e) { e.preventDefault(); const stage = e.currentTarget.text; this.updateStageDropdownText(stage); this.populateJobs(stage); - }; - - return Build; -})(); + } +} diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index f92e669414a..baaf5641200 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -1,5 +1,3 @@ -/* global Flash */ - import Vue from 'vue'; import JobMediator from './job_details_mediator'; import jobHeader from './components/header.vue'; diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js index cc014b815c4..3e2658f9fc1 100644 --- a/app/assets/javascripts/jobs/job_details_mediator.js +++ b/app/assets/javascripts/jobs/job_details_mediator.js @@ -1,11 +1,12 @@ -/* global Flash */ /* global Build */ import Visibility from 'visibilityjs'; +import Flash from '../flash'; import Poll from '../lib/utils/poll'; import JobStore from './stores/job_store'; import JobService from './services/job_service'; -import '../build'; +import Job from '../job'; +import handleRevealVariables from '../build_variables'; export default class JobMediator { constructor(options = {}) { @@ -20,7 +21,8 @@ export default class JobMediator { } initBuildClass() { - this.build = new Build(); + this.build = new Job(); + handleRevealVariables(); } fetchJob() { diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index d8814802d9e..c929dc98c10 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -1,124 +1,121 @@ /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ -/* global Flash */ /* global Sortable */ -((global) => { - class LabelManager { - constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { - this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); - this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); - this.otherLabels = otherLabels || $('.js-other-labels'); - this.errorMessage = 'Unable to update label prioritization at this time'; - this.emptyState = document.querySelector('#js-priority-labels-empty-state'); - this.sortable = Sortable.create(this.prioritizedLabels.get(0), { - filter: '.empty-message', - forceFallback: true, - fallbackClass: 'is-dragging', - dataIdAttr: 'data-id', - onUpdate: this.onPrioritySortUpdate.bind(this), - }); - this.bindEvents(); - } +import Flash from './flash'; - bindEvents() { - this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick); - return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); - } +export default class LabelManager { + constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { + this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); + this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels'); + this.otherLabels = otherLabels || $('.js-other-labels'); + this.errorMessage = 'Unable to update label prioritization at this time'; + this.emptyState = document.querySelector('#js-priority-labels-empty-state'); + this.sortable = Sortable.create(this.prioritizedLabels.get(0), { + filter: '.empty-message', + forceFallback: true, + fallbackClass: 'is-dragging', + dataIdAttr: 'data-id', + onUpdate: this.onPrioritySortUpdate.bind(this), + }); + this.bindEvents(); + } - onTogglePriorityClick(e) { - e.preventDefault(); - const _this = e.data; - const $btn = $(e.currentTarget); - const $label = $(`#${$btn.data('domId')}`); - const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; - const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); - $tooltip.tooltip('destroy'); - _this.toggleLabelPriority($label, action); - _this.toggleEmptyState($label, $btn, action); - } + bindEvents() { + this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick); + return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); + } - onButtonActionClick(e) { - e.stopPropagation(); - $(e.currentTarget).tooltip('hide'); - } + onTogglePriorityClick(e) { + e.preventDefault(); + const _this = e.data; + const $btn = $(e.currentTarget); + const $label = $(`#${$btn.data('domId')}`); + const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add'; + const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`); + $tooltip.tooltip('destroy'); + _this.toggleLabelPriority($label, action); + _this.toggleEmptyState($label, $btn, action); + } - toggleEmptyState($label, $btn, action) { - this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); - } + onButtonActionClick(e) { + e.stopPropagation(); + $(e.currentTarget).tooltip('hide'); + } - toggleLabelPriority($label, action, persistState) { - if (persistState == null) { - persistState = true; - } - let xhr; - const _this = this; - const url = $label.find('.js-toggle-priority').data('url'); - let $target = this.prioritizedLabels; - let $from = this.otherLabels; - if (action === 'remove') { - $target = this.otherLabels; - $from = this.prioritizedLabels; - } - $label.detach().appendTo($target); - if ($from.find('li').length) { - $from.find('.empty-message').removeClass('hidden'); - } - if ($target.find('> li:not(.empty-message)').length) { - $target.find('.empty-message').addClass('hidden'); - } - // Return if we are not persisting state - if (!persistState) { - return; - } - if (action === 'remove') { - xhr = $.ajax({ - url, - type: 'DELETE' - }); - // Restore empty message - if (!$from.find('li').length) { - $from.find('.empty-message').removeClass('hidden'); - } - } else { - xhr = this.savePrioritySort($label, action); - } - return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); - } + toggleEmptyState($label, $btn, action) { + this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); + } - onPrioritySortUpdate() { - const xhr = this.savePrioritySort(); - return xhr.fail(function() { - return new Flash(this.errorMessage, 'alert'); - }); + toggleLabelPriority($label, action, persistState) { + if (persistState == null) { + persistState = true; } - - savePrioritySort() { - return $.post({ - url: this.prioritizedLabels.data('url'), - data: { - label_ids: this.getSortedLabelsIds() - } + let xhr; + const _this = this; + const url = $label.find('.js-toggle-priority').data('url'); + let $target = this.prioritizedLabels; + let $from = this.otherLabels; + if (action === 'remove') { + $target = this.otherLabels; + $from = this.prioritizedLabels; + } + $label.detach().appendTo($target); + if ($from.find('li').length) { + $from.find('.empty-message').removeClass('hidden'); + } + if ($target.find('> li:not(.empty-message)').length) { + $target.find('.empty-message').addClass('hidden'); + } + // Return if we are not persisting state + if (!persistState) { + return; + } + if (action === 'remove') { + xhr = $.ajax({ + url, + type: 'DELETE' }); + // Restore empty message + if (!$from.find('li').length) { + $from.find('.empty-message').removeClass('hidden'); + } + } else { + xhr = this.savePrioritySort($label, action); } + return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action)); + } - rollbackLabelPosition($label, originalAction) { - const action = originalAction === 'remove' ? 'add' : 'remove'; - this.toggleLabelPriority($label, action, false); + onPrioritySortUpdate() { + const xhr = this.savePrioritySort(); + return xhr.fail(function() { return new Flash(this.errorMessage, 'alert'); - } + }); + } - getSortedLabelsIds() { - const sortedIds = []; - this.prioritizedLabels.find('> li').each(function() { - const id = $(this).data('id'); + savePrioritySort() { + return $.post({ + url: this.prioritizedLabels.data('url'), + data: { + label_ids: this.getSortedLabelsIds() + } + }); + } - if (id) { - sortedIds.push(id); - } - }); - return sortedIds; - } + rollbackLabelPosition($label, originalAction) { + const action = originalAction === 'remove' ? 'add' : 'remove'; + this.toggleLabelPriority($label, action, false); + return new Flash(this.errorMessage, 'alert'); } - gl.LabelManager = LabelManager; -})(window.gl || (window.gl = {})); + getSortedLabelsIds() { + const sortedIds = []; + this.prioritizedLabels.find('> li').each(function() { + const id = $(this).data('id'); + + if (id) { + sortedIds.push(id); + } + }); + return sortedIds; + } +} diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index 03dd61b4263..7aab13ed9c6 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -1,44 +1,35 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, max-len */ -(function() { - this.Labels = (function() { - function Labels() { - this.setSuggestedColor = this.setSuggestedColor.bind(this); - this.updateColorPreview = this.updateColorPreview.bind(this); - var form; - form = $('.label-form'); - this.cleanBinding(); - this.addBinding(); - this.updateColorPreview(); - } +export default class Labels { + constructor() { + this.setSuggestedColor = this.setSuggestedColor.bind(this); + this.updateColorPreview = this.updateColorPreview.bind(this); + this.cleanBinding(); + this.addBinding(); + this.updateColorPreview(); + } - Labels.prototype.addBinding = function() { - $(document).on('click', '.suggest-colors a', this.setSuggestedColor); - return $(document).on('input', 'input#label_color', this.updateColorPreview); - }; + addBinding() { + $(document).on('click', '.suggest-colors a', this.setSuggestedColor); + return $(document).on('input', 'input#label_color', this.updateColorPreview); + } + // eslint-disable-next-line class-methods-use-this + cleanBinding() { + $(document).off('click', '.suggest-colors a'); + return $(document).off('input', 'input#label_color'); + } + // eslint-disable-next-line class-methods-use-this + updateColorPreview() { + const previewColor = $('input#label_color').val(); + return $('div.label-color-preview').css('background-color', previewColor); + // Updates the the preview color with the hex-color input + } - Labels.prototype.cleanBinding = function() { - $(document).off('click', '.suggest-colors a'); - return $(document).off('input', 'input#label_color'); - }; - - Labels.prototype.updateColorPreview = function() { - var previewColor; - previewColor = $('input#label_color').val(); - return $('div.label-color-preview').css('background-color', previewColor); - // Updates the the preview color with the hex-color input - }; - - // Updates the preview color with a click on a suggested color - Labels.prototype.setSuggestedColor = function(e) { - var color; - color = $(e.currentTarget).data('color'); - $('input#label_color').val(color); - this.updateColorPreview(); - // Notify the form, that color has changed - $('.label-form').trigger('keyup'); - return e.preventDefault(); - }; - - return Labels; - })(); -}).call(window); + // Updates the preview color with a click on a suggested color + setSuggestedColor(e) { + const color = $(e.currentTarget).data('color'); + $('input#label_color').val(color); + this.updateColorPreview(); + // Notify the form, that color has changed + $('.label-form').trigger('keyup'); + return e.preventDefault(); + } +} diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index d479f7ed682..84602cf9207 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -285,7 +285,7 @@ import CreateLabelDropdown from './create_label'; }, hidden: function() { var isIssueIndex, isMRIndex, page, selectedLabels; - page = $('body').data('page'); + page = $('body').attr('data-page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; $selectbox.hide(); @@ -325,7 +325,7 @@ import CreateLabelDropdown from './create_label'; $loading.fadeOut(); }; - page = $('body').data('page'); + page = $('body').attr('data-page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 423a25fbdfa..9f05cf16967 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,5 +1,5 @@ -export const getPagePath = (index = 0) => $('body').data('page').split(':')[index]; +export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; export const isInGroupsPage = () => getPagePath() === 'groups'; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 021f936a4fa..f776829f69c 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ +/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ import 'vendor/latinise'; @@ -13,9 +13,17 @@ if ((base = w.gl).text == null) { gl.text.addDelimiter = function(text) { return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text; }; -gl.text.highCountTrim = function(count) { + +/** + * Returns '99+' for numbers bigger than 99. + * + * @param {Number} count + * @return {Number|String} + */ +export function highCountTrim(count) { return count > 99 ? '99+' : count; -}; +} + gl.text.randomString = function() { return Math.random().toString(36).substring(7); }; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 853546b617b..8d7608ce0f4 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ -/* global Flash */ /* global ConfirmDangerModal */ /* global Aside */ @@ -47,25 +46,14 @@ import './lib/utils/url_utility'; // behaviors import './behaviors/'; -// u2f -import './u2f/authenticate'; -import './u2f/error'; -import './u2f/register'; -import './u2f/util'; - // everything else import './activities'; import './admin'; -import './api'; import './aside'; import './autosave'; import loadAwardsHandler from './awards_handler'; import bp from './breakpoints'; import './broadcast_message'; -import './build'; -import './build_artifacts'; -import './build_variables'; -import './ci_lint_editor'; import './commits'; import './compare'; import './compare_autocomplete'; @@ -76,7 +64,7 @@ import './diff'; import './dropzone_input'; import './due_date_select'; import './files_comment_button'; -import './flash'; +import Flash from './flash'; import './gl_dropdown'; import './gl_field_error'; import './gl_field_errors'; @@ -91,8 +79,6 @@ import './issuable_context'; import './issuable_form'; import './issue'; import './issue_status_select'; -import './label_manager'; -import './labels'; import './labels_select'; import './layout_nav'; import LazyLoader from './lazy_loader'; @@ -130,7 +116,6 @@ import './right_sidebar'; import './search'; import './search_autocomplete'; import './smart_interval'; -import './star'; import './subscription'; import './subscription_select'; import initBreadcrumbs from './breadcrumb'; @@ -169,7 +154,6 @@ $(function () { var $document = $(document); var $window = $(window); var $sidebarGutterToggle = $('.js-sidebar-toggle'); - var $flash = $('.flash-container'); var bootstrapBreakpoint = bp.getBreakpointSize(); var fitSidebarForSize; @@ -254,13 +238,6 @@ $(function () { // Form submitter }); gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); - // Flash - if ($flash.length > 0) { - $flash.click(function () { - return $(this).fadeOut(); - }); - $flash.show(); - } // Disable form buttons while a form is submitting $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { var buttons; diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index 645045fea88..93f8f6ee926 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -1,8 +1,8 @@ /* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */ /* global ace */ -/* global Flash */ import Vue from 'vue'; +import Flash from '../../flash'; ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index d74cf5328ad..17591829b76 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -1,7 +1,7 @@ /* eslint-disable new-cap, comma-dangle, no-new */ -/* global Flash */ import Vue from 'vue'; +import Flash from '../flash'; import initIssuableSidebar from '../init_issuable_sidebar'; import './merge_conflict_store'; import './merge_conflict_service'; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index c042b22d1fd..df042c7baff 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,9 +1,8 @@ /* eslint-disable no-new, class-methods-use-this */ -/* global Flash */ /* global notes */ import Cookies from 'js-cookie'; -import './flash'; +import Flash from './flash'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import initChangesDropdown from './init_changes_dropdown'; import bp from './breakpoints'; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 3e07ec4d0aa..8f3f1986763 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,7 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */ -/* global Flash */ /* global Sortable */ +import Flash from './flash'; + (function() { this.Milestone = (function() { function Milestone() { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 4675b1fcb8f..951d5e559b4 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -147,7 +147,7 @@ import _ from 'underscore'; const { $el, e } = options; let selected = options.selectedObj; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; - page = $('body').data('page'); + page = $('body').attr('data-page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); isSelecting = (selected.name !== selectedMilestone); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index 64c1447f427..ca3d271663b 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -1,5 +1,5 @@ /* eslint-disable no-new */ -/* global Flash */ +import Flash from './flash'; /** * In each pipelines table we have a mini pipeline graph for each pipeline. diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 442ed86d50c..cbe24c0915b 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,6 +1,6 @@ <script> - /* global Flash */ import _ from 'underscore'; + import Flash from '../../flash'; import MonitoringService from '../services/monitoring_service'; import GraphGroup from './graph_group.vue'; import Graph from './graph.vue'; diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 24de21f2ce2..790f78d2e11 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -5,7 +5,6 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, class-methods-use-this */ -/* global Flash */ /* global Autosave */ /* global ResolveService */ /* global mrRefreshWidgetUrl */ @@ -18,7 +17,9 @@ import Dropzone from 'dropzone'; import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; +import Flash from './flash'; import CommentTypeToggle from './comment_type_toggle'; +import GLForm from './gl_form'; import loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; @@ -354,7 +355,7 @@ export default class Notes { Object.keys(noteEntity.commands_changes).length > 0) { $notesList.find('.system-note.being-posted').remove(); } - this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); + this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0)); this.refresh(); } return; @@ -557,7 +558,7 @@ export default class Notes { */ setupNoteForm(form) { var textarea, key; - new gl.GLForm(form, this.enableGFM); + this.glForm = new GLForm(form, this.enableGFM); textarea = form.find('.js-note-text'); key = [ 'Note', @@ -593,7 +594,7 @@ export default class Notes { } else if ($form.hasClass('js-discussion-note-form')) { formParentTimeline = $form.closest('.discussion-notes').find('.notes'); } - return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline); + return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0)); } updateNoteError($parentTimeline) { @@ -1152,7 +1153,7 @@ export default class Notes { var targetId = $originalContentEl.data('target-id'); var targetType = $originalContentEl.data('target-type'); - new gl.GLForm($editForm.find('form'), this.enableGFM); + this.glForm = new GLForm($editForm.find('form'), this.enableGFM); $editForm.find('form') .attr('action', postUrl) @@ -1213,13 +1214,13 @@ export default class Notes { } addFlash(...flashParams) { - this.flashInstance = new Flash(...flashParams); + this.flashContainer = new Flash(...flashParams); } clearFlash() { - if (this.flashInstance && this.flashInstance.flashContainer) { - this.flashInstance.flashContainer.hide(); - this.flashInstance = null; + if (this.flashContainer) { + this.flashContainer.style.display = 'none'; + this.flashContainer = null; } } @@ -1257,7 +1258,7 @@ export default class Notes { } static checkMergeRequestStatus() { - if (getPagePath(1) === 'merge_requests') { + if (getPagePath(1) === 'merge_requests' && gl.mrWidget) { gl.mrWidget.checkStatus(); } } diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index ab8516296a8..2ce52e4538a 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -1,8 +1,9 @@ <script> - /* global Flash, Autosave */ + /* global Autosave */ import { mapActions, mapGetters } from 'vuex'; import _ from 'underscore'; import autosize from 'vendor/autosize'; + import Flash from '../../flash'; import '../../autosave'; import TaskList from '../../task_list'; import * as constants from '../constants'; @@ -145,7 +146,7 @@ Flash( 'Something went wrong while adding your comment. Please try again.', 'alert', - $(this.$refs.commentForm), + this.$refs.commentForm, ); } } else { @@ -160,7 +161,7 @@ this.isSubmitting = false; this.discard(false); const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; - Flash(msg, 'alert', $(this.$el)); + Flash(msg, 'alert', this.$el); this.note = noteData.data.note.note; // Restore textarea content. this.removePlaceholderNotes(); }); diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue index b131ef4b182..baf43190d9e 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/issue_discussion.vue @@ -1,6 +1,6 @@ <script> - /* global Flash */ import { mapActions, mapGetters } from 'vuex'; + import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; import issueNote from './issue_note.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; @@ -133,7 +133,7 @@ this.isReplying = true; this.$nextTick(() => { const msg = 'Your comment could not be submitted! Please check your network connection and try again.'; - Flash(msg, 'alert', $(this.$el)); + Flash(msg, 'alert', this.$el); this.$refs.noteForm.note = noteText; callback(err); }); diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 1f43b8a16ad..0ddbd672bed 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -1,7 +1,6 @@ <script> - /* global Flash */ - import { mapGetters, mapActions } from 'vuex'; + import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import issueNoteHeader from './issue_note_header.vue'; import issueNoteActions from './issue_note_actions.vue'; @@ -101,7 +100,7 @@ this.isEditing = true; this.$nextTick(() => { const msg = 'Something went wrong while editing your comment. Please try again.'; - Flash(msg, 'alert', $(this.$el)); + Flash(msg, 'alert', this.$el); this.recoverNoteContent(noteText); callback(); }); diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue index d42e61e3899..c3a340139e7 100644 --- a/app/assets/javascripts/notes/components/issue_note_awards_list.vue +++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue @@ -1,10 +1,9 @@ <script> - /* global Flash */ - import { mapActions, mapGetters } from 'vuex'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg'; + import Flash from '../../flash'; import { glEmojiTag } from '../../emoji'; import tooltip from '../../vue_shared/directives/tooltip'; diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index b6fc5e5036f..aecd1f957e5 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -1,6 +1,6 @@ <script> - /* global Flash */ import { mapGetters, mapActions } from 'vuex'; + import Flash from '../../flash'; import store from '../stores/'; import * as constants from '../constants'; import issueNote from './issue_note.vue'; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 1a791039909..6f04aecc9b7 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -1,5 +1,5 @@ -/* global Flash */ import Visibility from 'visibilityjs'; +import Flash from '../../flash'; import Poll from '../../lib/utils/poll'; import * as types from './mutation_types'; import * as utils from './utils'; @@ -99,7 +99,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { eTagPoll.makeRequest(); $('.js-gfm-input').trigger('clear-commands-cache.atwho'); - Flash('Commands applied', 'notice', $(noteData.flashContainer)); + Flash('Commands applied', 'notice', noteData.flashContainer); } if (commandsChanges) { @@ -114,8 +114,8 @@ export const saveNote = ({ commit, dispatch }, noteData) => { .catch(() => { Flash( 'Something went wrong while adding your award. Please try again.', - null, - $(noteData.flashContainer), + 'alert', + noteData.flashContainer, ); }); } @@ -126,7 +126,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { } if (errors && errors.commands_only) { - Flash(errors.commands_only, 'notice', $(noteData.flashContainer)); + Flash(errors.commands_only, 'notice', noteData.flashContainer); } commit(types.REMOVE_PLACEHOLDER_NOTES); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index 838356133cd..f90ac2d9f71 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */ -/* global Flash */ +import Flash from './flash'; (function() { this.NotificationsDropdown = (function() { diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index 50c725aa3d5..f1cf6e92ef5 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import Translate from '../vue_shared/translate'; +import GlFieldErrors from '../gl_field_errors'; import intervalPatternInput from './components/interval_pattern_input.vue'; import TimezoneDropdown from './components/timezone_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown'; @@ -39,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => { gl.timezoneDropdown = new TimezoneDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown(); - gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); + gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement); setupPipelineVariableList($('.js-pipeline-variable-list')); }); diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index c4c63a52358..f3c0aca17ba 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,6 +1,4 @@ <script> - /* global Flash */ - import '~/flash'; import playIconSvg from 'icons/_icon_play.svg'; import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index a4a27247406..1a7a5c2a415 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -13,7 +13,7 @@ * 4. Commit widget */ -/* global Flash */ +import Flash from '../../flash'; import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index e97f5632dc8..50bdf80c3e3 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -1,6 +1,5 @@ -/* global Flash */ -import '~/flash'; import Visibility from 'visibilityjs'; +import Flash from '../../flash'; import Poll from '../../lib/utils/poll'; import emptyState from '../components/empty_state.vue'; import errorState from '../components/error_state.vue'; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index bfc416da50b..206023d4ddb 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -1,6 +1,5 @@ -/* global Flash */ - import Vue from 'vue'; +import Flash from '../flash'; import PipelinesMediator from './pipeline_details_mediatior'; import pipelineGraph from './components/graph/graph_component.vue'; import pipelineHeader from './components/header_component.vue'; diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js index 385e7430a7d..823ccd849f4 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js @@ -1,6 +1,5 @@ -/* global Flash */ - import Visibility from 'visibilityjs'; +import Flash from '../flash'; import Poll from '../lib/utils/poll'; import PipelineStore from './stores/pipeline_store'; import PipelineService from './services/pipeline_service'; diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index 3deb242bc1f..0dc02f012e4 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -1,5 +1,5 @@ /* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ -/* global Flash */ +import Flash from '../flash'; import { getPagePath } from '../lib/utils/common_utils'; ((global) => { diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 7f972b6f6ee..3ecc0c2a6e5 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -29,6 +29,12 @@ const bindEvents = () => { const $newProjectForm = $('#new_project'); const $projectImportUrl = $('#project_import_url'); const $projectPath = $('#project_path'); + const $useTemplateBtn = $('.template-button > input'); + const $projectFieldsForm = $('.project-fields-form'); + const $selectedTemplateText = $('.selected-template'); + const $changeTemplateBtn = $('.change-template'); + const $selectedIcon = $('.selected-icon svg'); + const $templateProjectNameInput = $('#template-project-name #project_path'); if ($newProjectForm.length !== 1) { return; @@ -48,6 +54,40 @@ const bindEvents = () => { $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`); }); + function chooseTemplate() { + $('.template-option').hide(); + $projectFieldsForm.addClass('selected'); + $selectedIcon.removeClass('active'); + const value = $(this).val(); + const templates = { + rails: { + text: 'Ruby on Rails', + icon: '.selected-icon .icon-rails', + }, + express: { + text: 'NodeJS Express', + icon: '.selected-icon .icon-node-express', + }, + spring: { + text: 'Spring', + icon: '.selected-icon .icon-java-spring', + }, + }; + + const selectedTemplate = templates[value]; + $selectedTemplateText.text(selectedTemplate.text); + $(selectedTemplate.icon).addClass('active'); + $templateProjectNameInput.focus(); + } + + $useTemplateBtn.on('change', chooseTemplate); + + $changeTemplateBtn.on('click', () => { + $('.template-option').show(); + $projectFieldsForm.removeClass('selected'); + $useTemplateBtn.prop('checked', false); + }); + $newProjectForm.on('submit', () => { $projectPath.val($projectPath.val().trim()); }); diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index a4d50a52315..55c93923cc8 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -81,7 +81,11 @@ export default class PrometheusMetrics { loadActiveMetrics() { this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); backOff((next, stop) => { - $.getJSON(this.activeMetricsEndpoint) + $.ajax({ + url: this.activeMetricsEndpoint, + dataType: 'json', + global: false, + }) .done((res) => { if (res && res.success) { stop(res); diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 3b920942a3f..632625da8e7 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -1,6 +1,5 @@ /* eslint-disable no-new */ -/* global Flash */ - +import Flash from '../flash'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; export default class ProtectedBranchEdit { @@ -57,7 +56,7 @@ export default class ProtectedBranchEdit { }, }, error() { - new Flash('Failed to update branch!', null, $('.js-protected-branches-list')); + new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list')); }, }).always(() => { this.$allowedToMergeDropdown.enable(); diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index 09a387c0f9e..dad0ad25b65 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -1,6 +1,5 @@ /* eslint-disable no-new */ -/* global Flash */ - +import Flash from '../flash'; import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; export default class ProtectedTagEdit { @@ -43,7 +42,7 @@ export default class ProtectedTagEdit { }, }, error() { - new Flash('Failed to update tag!', null, $('.js-protected-tags-list')); + new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); }, }).always(() => { this.$allowedToCreateDropdownButton.enable(); diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index 119e38c583d..6d8cc964eb2 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -1,5 +1,5 @@ <script> -/* global Flash */ +import Flash from '../../flash'; import Store from '../stores/repo_store'; import RepoMixin from '../mixins/repo_mixin'; import Service from '../services/repo_service'; diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index 7483f8bc305..46204598e1d 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -1,7 +1,6 @@ -/* global Flash */ import Service from '../services/repo_service'; import Store from '../stores/repo_store'; -import '../../flash'; +import Flash from '../../flash'; const RepoHelper = { monacoInstance: null, diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index af83497fa39..830685f7e6e 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -1,4 +1,3 @@ -/* global Flash */ import axios from 'axios'; import Store from '../stores/repo_store'; import Api from '../../api'; diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index 93b39cff27e..c633f538c1b 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -1,4 +1,3 @@ -/* global Flash */ import Helper from '../helpers/repo_helper'; import Service from '../services/repo_service'; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 05caf177aec..07fee53d814 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */ -/* global Flash */ +import Flash from './flash'; import Api from './api'; (function() { diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js index f83c3b037ed..74c17bc14a2 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js @@ -1,5 +1,4 @@ -/* global Flash */ - +import Flash from '../../../flash'; import AssigneeTitle from './assignee_title'; import Assignees from './assignees'; diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index f2b1099a678..22a9a34dda3 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -1,5 +1,5 @@ <script> -/* global Flash */ +import Flash from '../../../flash'; import editForm from './edit_form.vue'; export default { diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js index 3c9de02407e..977dd83a7ea 100644 --- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js +++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js @@ -1,5 +1,3 @@ -/* global Flash */ - function isValidProjectId(id) { return id > 0; } @@ -38,7 +36,7 @@ class SidebarMoveIssue { data: (searchTerm, callback) => { this.mediator.fetchAutocompleteProjects(searchTerm) .then(callback) - .catch(() => new Flash('An error occurred while fetching projects autocomplete.')); + .catch(() => new window.Flash('An error occurred while fetching projects autocomplete.')); }, renderRow: project => ` <li> @@ -73,7 +71,7 @@ class SidebarMoveIssue { this.mediator.moveIssue() .catch(() => { - Flash('An error occurred while moving the issue.'); + window.Flash('An error occurred while moving the issue.'); this.$confirmButton .enable() .removeClass('is-loading'); diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 2fe6e5b31f0..ede3a0de144 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -1,5 +1,4 @@ -/* global Flash */ - +import Flash from '../flash'; import Service from './services/sidebar_service'; import Store from './stores/sidebar_store'; diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index 3a06b477d7c..1a8dc085772 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,28 +1,29 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ -/* global Flash */ - +import Flash from './flash'; import { __, s__ } from './locale'; export default class Star { constructor() { - $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { - var $starIcon, $starSpan, $this, toggleStar; - $this = $(this); - $starSpan = $this.find('span'); - $starIcon = $this.find('i'); - toggleStar = function(isStarred) { - $this.parent().find('.star-count').text(data.star_count); - if (isStarred) { - $starSpan.removeClass('starred').text(s__('StarProject|Star')); - $starIcon.removeClass('fa-star').addClass('fa-star-o'); - } else { - $starSpan.addClass('starred').text(__('Unstar')); - $starIcon.removeClass('fa-star-o').addClass('fa-star'); + $('.project-home-panel .toggle-star') + .on('ajax:success', function handleSuccess(e, data) { + const $this = $(this); + const $starSpan = $this.find('span'); + const $starIcon = $this.find('i'); + + function toggleStar(isStarred) { + $this.parent().find('.star-count').text(data.star_count); + if (isStarred) { + $starSpan.removeClass('starred').text(s__('StarProject|Star')); + $starIcon.removeClass('fa-star').addClass('fa-star-o'); + } else { + $starSpan.addClass('starred').text(__('Unstar')); + $starIcon.removeClass('fa-star-o').addClass('fa-star'); + } } - }; - toggleStar($starSpan.hasClass('starred')); - }).on('ajax:error', function(e, xhr, status, error) { - new Flash('Star toggle failed. Try again later.', 'alert'); - }); + + toggleStar($starSpan.hasClass('starred')); + }) + .on('ajax:error', () => { + Flash('Star toggle failed. Try again later.', 'alert'); + }); } } diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index c39f569da5e..dcbec40c79e 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -1,6 +1,5 @@ -/* global Flash */ - import 'deckar01-task_list'; +import Flash from './flash'; export default class TaskList { constructor(options = {}) { diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/two_factor_auth.js index d26f61562a5..e3414d9afff 100644 --- a/app/assets/javascripts/two_factor_auth.js +++ b/app/assets/javascripts/two_factor_auth.js @@ -1,4 +1,5 @@ -/* global U2FRegister */ +import U2FRegister from './u2f/register'; + document.addEventListener('DOMContentLoaded', () => { const twoFactorNode = document.querySelector('.js-two-factor-auth'); const skippable = twoFactorNode.dataset.twoFactorSkippable === 'true'; diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index 8821b22477f..a3cc04e35fe 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -1,118 +1,108 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */ +/* eslint-disable func-names, wrap-iife */ /* global u2f */ -/* global U2FError */ -/* global U2FUtil */ - import _ from 'underscore'; +import isU2FSupported from './util'; +import U2FError from './error'; // Authenticate U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> authenticated -> POST to server // State Flow #2: setup -> in_progress -> error -> setup -(function() { - const global = window.gl || (window.gl = {}); - - global.U2FAuthenticate = (function() { - function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) { - this.container = container; - this.renderNotSupported = this.renderNotSupported.bind(this); - this.renderAuthenticated = this.renderAuthenticated.bind(this); - this.renderError = this.renderError.bind(this); - this.renderInProgress = this.renderInProgress.bind(this); - this.renderTemplate = this.renderTemplate.bind(this); - this.authenticate = this.authenticate.bind(this); - this.start = this.start.bind(this); - this.appId = u2fParams.app_id; - this.challenge = u2fParams.challenge; - this.form = form; - this.fallbackButton = fallbackButton; - this.fallbackUI = fallbackUI; - if (this.fallbackButton) this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this)); - this.signRequests = u2fParams.sign_requests.map(function(request) { - // The U2F Javascript API v1.1 requires a single challenge, with - // _no challenges per-request_. The U2F Javascript API v1.0 requires a - // challenge per-request, which is done by copying the single challenge - // into every request. - // - // In either case, we don't need the per-request challenges that the server - // has generated, so we can remove them. - // - // Note: The server library fixes this behaviour in (unreleased) version 1.0.0. - // This can be removed once we upgrade. - // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 - return _(request).omit('challenge'); - }); +export default class U2FAuthenticate { + constructor(container, form, u2fParams, fallbackButton, fallbackUI) { + this.container = container; + this.renderNotSupported = this.renderNotSupported.bind(this); + this.renderAuthenticated = this.renderAuthenticated.bind(this); + this.renderError = this.renderError.bind(this); + this.renderInProgress = this.renderInProgress.bind(this); + this.renderTemplate = this.renderTemplate.bind(this); + this.authenticate = this.authenticate.bind(this); + this.start = this.start.bind(this); + this.appId = u2fParams.app_id; + this.challenge = u2fParams.challenge; + this.form = form; + this.fallbackButton = fallbackButton; + this.fallbackUI = fallbackUI; + if (this.fallbackButton) { + this.fallbackButton.addEventListener('click', this.switchToFallbackUI.bind(this)); } - U2FAuthenticate.prototype.start = function() { - if (U2FUtil.isU2FSupported()) { - return this.renderInProgress(); - } else { - return this.renderNotSupported(); - } - }; + // The U2F Javascript API v1.1 requires a single challenge, with + // _no challenges per-request_. The U2F Javascript API v1.0 requires a + // challenge per-request, which is done by copying the single challenge + // into every request. + // + // In either case, we don't need the per-request challenges that the server + // has generated, so we can remove them. + // + // Note: The server library fixes this behaviour in (unreleased) version 1.0.0. + // This can be removed once we upgrade. + // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4 + this.signRequests = u2fParams.sign_requests.map(request => _(request).omit('challenge')); - U2FAuthenticate.prototype.authenticate = function() { - return u2f.sign(this.appId, this.challenge, this.signRequests, (function(_this) { - return function(response) { - var error; - if (response.errorCode) { - error = new U2FError(response.errorCode, 'authenticate'); - return _this.renderError(error); - } else { - return _this.renderAuthenticated(JSON.stringify(response)); - } - }; - })(this), 10); + this.templates = { + notSupported: '#js-authenticate-u2f-not-supported', + setup: '#js-authenticate-u2f-setup', + inProgress: '#js-authenticate-u2f-in-progress', + error: '#js-authenticate-u2f-error', + authenticated: '#js-authenticate-u2f-authenticated', }; + } - // Rendering # - U2FAuthenticate.prototype.templates = { - "notSupported": "#js-authenticate-u2f-not-supported", - "setup": '#js-authenticate-u2f-setup', - "inProgress": '#js-authenticate-u2f-in-progress', - "error": '#js-authenticate-u2f-error', - "authenticated": '#js-authenticate-u2f-authenticated' - }; + start() { + if (isU2FSupported()) { + return this.renderInProgress(); + } + return this.renderNotSupported(); + } - U2FAuthenticate.prototype.renderTemplate = function(name, params) { - var template, templateString; - templateString = $(this.templates[name]).html(); - template = _.template(templateString); - return this.container.html(template(params)); - }; + authenticate() { + return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) { + return function (response) { + if (response.errorCode) { + const error = new U2FError(response.errorCode, 'authenticate'); + return _this.renderError(error); + } + return _this.renderAuthenticated(JSON.stringify(response)); + }; + })(this), 10); + } - U2FAuthenticate.prototype.renderInProgress = function() { - this.renderTemplate('inProgress'); - return this.authenticate(); - }; + renderTemplate(name, params) { + const templateString = $(this.templates[name]).html(); + const template = _.template(templateString); + return this.container.html(template(params)); + } - U2FAuthenticate.prototype.renderError = function(error) { - this.renderTemplate('error', { - error_message: error.message(), - error_code: error.errorCode - }); - return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress); - }; + renderInProgress() { + this.renderTemplate('inProgress'); + return this.authenticate(); + } - U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) { - this.renderTemplate('authenticated'); - const container = this.container[0]; - container.querySelector('#js-device-response').value = deviceResponse; - container.querySelector(this.form).submit(); - this.fallbackButton.classList.add('hidden'); - }; + renderError(error) { + this.renderTemplate('error', { + error_message: error.message(), + error_code: error.errorCode, + }); + return this.container.find('#js-u2f-try-again').on('click', this.renderInProgress); + } - U2FAuthenticate.prototype.renderNotSupported = function() { - return this.renderTemplate('notSupported'); - }; + renderAuthenticated(deviceResponse) { + this.renderTemplate('authenticated'); + const container = this.container[0]; + container.querySelector('#js-device-response').value = deviceResponse; + container.querySelector(this.form).submit(); + this.fallbackButton.classList.add('hidden'); + } - U2FAuthenticate.prototype.switchToFallbackUI = function() { - this.fallbackButton.classList.add('hidden'); - this.container[0].classList.add('hidden'); - this.fallbackUI.classList.remove('hidden'); - }; + renderNotSupported() { + return this.renderTemplate('notSupported'); + } + + switchToFallbackUI() { + this.fallbackButton.classList.add('hidden'); + this.container[0].classList.add('hidden'); + this.fallbackUI.classList.remove('hidden'); + } - return U2FAuthenticate; - })(); -})(); +} diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 3119b3480c3..1a98564ff55 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,25 +1,22 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */ -/* global u2f */ +export default class U2FError { + constructor(errorCode, u2fFlowType) { + this.errorCode = errorCode; + this.message = this.message.bind(this); + this.httpsDisabled = window.location.protocol !== 'https:'; + this.u2fFlowType = u2fFlowType; + } -(function() { - this.U2FError = (function() { - function U2FError(errorCode, u2fFlowType) { - this.errorCode = errorCode; - this.message = this.message.bind(this); - this.httpsDisabled = window.location.protocol !== 'https:'; - this.u2fFlowType = u2fFlowType; - } - - U2FError.prototype.message = function() { - if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) { - return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.'; - } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) { - if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.'; - if (this.u2fFlowType === 'register') return 'This device has already been registered with us.'; + message() { + if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) { + return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.'; + } else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) { + if (this.u2fFlowType === 'authenticate') { + return 'This device has not been registered with us.'; } - return "There was a problem communicating with your device."; - }; - - return U2FError; - })(); -}).call(window); + if (this.u2fFlowType === 'register') { + return 'This device has already been registered with us.'; + } + } + return 'There was a problem communicating with your device.'; + } +} diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 3a2534d553b..cc3f02e75f6 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,98 +1,89 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */ +/* eslint-disable func-names, wrap-iife */ /* global u2f */ -/* global U2FError */ -/* global U2FUtil */ import _ from 'underscore'; +import isU2FSupported from './util'; +import U2FError from './error'; // Register U2F (universal 2nd factor) devices for users to authenticate with. // // State Flow #1: setup -> in_progress -> registered -> POST to server // State Flow #2: setup -> in_progress -> error -> setup -(function() { - this.U2FRegister = (function() { - function U2FRegister(container, u2fParams) { - this.container = container; - this.renderNotSupported = this.renderNotSupported.bind(this); - this.renderRegistered = this.renderRegistered.bind(this); - this.renderError = this.renderError.bind(this); - this.renderInProgress = this.renderInProgress.bind(this); - this.renderSetup = this.renderSetup.bind(this); - this.renderTemplate = this.renderTemplate.bind(this); - this.register = this.register.bind(this); - this.start = this.start.bind(this); - this.appId = u2fParams.app_id; - this.registerRequests = u2fParams.register_requests; - this.signRequests = u2fParams.sign_requests; - } +export default class U2FRegister { + constructor(container, u2fParams) { + this.container = container; + this.renderNotSupported = this.renderNotSupported.bind(this); + this.renderRegistered = this.renderRegistered.bind(this); + this.renderError = this.renderError.bind(this); + this.renderInProgress = this.renderInProgress.bind(this); + this.renderSetup = this.renderSetup.bind(this); + this.renderTemplate = this.renderTemplate.bind(this); + this.register = this.register.bind(this); + this.start = this.start.bind(this); + this.appId = u2fParams.app_id; + this.registerRequests = u2fParams.register_requests; + this.signRequests = u2fParams.sign_requests; - U2FRegister.prototype.start = function() { - if (U2FUtil.isU2FSupported()) { - return this.renderSetup(); - } else { - return this.renderNotSupported(); - } + this.templates = { + notSupported: '#js-register-u2f-not-supported', + setup: '#js-register-u2f-setup', + inProgress: '#js-register-u2f-in-progress', + error: '#js-register-u2f-error', + registered: '#js-register-u2f-registered', }; + } - U2FRegister.prototype.register = function() { - return u2f.register(this.appId, this.registerRequests, this.signRequests, (function(_this) { - return function(response) { - var error; - if (response.errorCode) { - error = new U2FError(response.errorCode, 'register'); - return _this.renderError(error); - } else { - return _this.renderRegistered(JSON.stringify(response)); - } - }; - })(this), 10); - }; + start() { + if (isU2FSupported()) { + return this.renderSetup(); + } + return this.renderNotSupported(); + } - // Rendering # - U2FRegister.prototype.templates = { - "notSupported": "#js-register-u2f-not-supported", - "setup": '#js-register-u2f-setup', - "inProgress": '#js-register-u2f-in-progress', - "error": '#js-register-u2f-error', - "registered": '#js-register-u2f-registered' - }; + register() { + return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) { + return function (response) { + if (response.errorCode) { + const error = new U2FError(response.errorCode, 'register'); + return _this.renderError(error); + } + return _this.renderRegistered(JSON.stringify(response)); + }; + })(this), 10); + } - U2FRegister.prototype.renderTemplate = function(name, params) { - var template, templateString; - templateString = $(this.templates[name]).html(); - template = _.template(templateString); - return this.container.html(template(params)); - }; + renderTemplate(name, params) { + const templateString = $(this.templates[name]).html(); + const template = _.template(templateString); + return this.container.html(template(params)); + } - U2FRegister.prototype.renderSetup = function() { - this.renderTemplate('setup'); - return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress); - }; + renderSetup() { + this.renderTemplate('setup'); + return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress); + } - U2FRegister.prototype.renderInProgress = function() { - this.renderTemplate('inProgress'); - return this.register(); - }; + renderInProgress() { + this.renderTemplate('inProgress'); + return this.register(); + } - U2FRegister.prototype.renderError = function(error) { - this.renderTemplate('error', { - error_message: error.message(), - error_code: error.errorCode - }); - return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); - }; + renderError(error) { + this.renderTemplate('error', { + error_message: error.message(), + error_code: error.errorCode, + }); + return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); + } - U2FRegister.prototype.renderRegistered = function(deviceResponse) { - this.renderTemplate('registered'); - // Prefer to do this instead of interpolating using Underscore templates - // because of JSON escaping issues. - return this.container.find("#js-device-response").val(deviceResponse); - }; - - U2FRegister.prototype.renderNotSupported = function() { - return this.renderTemplate('notSupported'); - }; + renderRegistered(deviceResponse) { + this.renderTemplate('registered'); + // Prefer to do this instead of interpolating using Underscore templates + // because of JSON escaping issues. + return this.container.find('#js-device-response').val(deviceResponse); + } - return U2FRegister; - })(); -}).call(window); + renderNotSupported() { + return this.renderTemplate('notSupported'); + } +} diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js index 813d363db00..9771ff935c2 100644 --- a/app/assets/javascripts/u2f/util.js +++ b/app/assets/javascripts/u2f/util.js @@ -1,12 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife */ -(function() { - this.U2FUtil = (function() { - function U2FUtil() {} - - U2FUtil.isU2FSupported = function() { - return window.u2f; - }; - - return U2FUtil; - })(); -}).call(window); +export default function isU2FSupported() { + return window.u2f; +} diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/users/index.js index 33a83f8dae5..9fd8452a2b6 100644 --- a/app/assets/javascripts/users/index.js +++ b/app/assets/javascripts/users/index.js @@ -1,7 +1,7 @@ import Cookies from 'js-cookie'; import UserTabs from './user_tabs'; -export default function initUserProfile(action) { +function initUserProfile(action) { // place profile avatars to top $('.profile-groups-avatars').tooltip({ placement: 'top', @@ -17,3 +17,9 @@ export default function initUserProfile(action) { $(this).parents('.project-limit-message').remove(); }); } + +document.addEventListener('DOMContentLoaded', () => { + const page = $('body').attr('data-page'); + const action = page.split(':')[1]; + initUserProfile(action); +}); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 73676bd6de7..a0883b32593 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -424,7 +424,7 @@ function UsersSelect(currentUser, els) { } var isIssueIndex, isMRIndex, page, selected; - page = $('body').data('page'); + page = $('body').attr('data-page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index e98d147733c..e86a0f7e749 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -1,6 +1,5 @@ -/* global Flash */ - import '~/lib/utils/datetime_utility'; +import Flash from '../../flash'; import MemoryUsage from './mr_widget_memory_usage'; import StatusIcon from './mr_widget_status_icon'; import MRWidgetService from '../services/mr_widget_service'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js index bdfd4d9667c..05c4a28be88 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js @@ -1,4 +1,4 @@ -/* global Flash */ +import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon'; import MRWidgetAuthor from '../../components/mr_widget_author'; import eventHub from '../../event_hub'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js index 74fc52796a0..2dfd87ed904 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js @@ -1,5 +1,4 @@ -/* global Flash */ - +import Flash from '../../../flash'; import mrWidgetAuthorTime from '../../components/mr_widget_author_time'; import tooltip from '../../../vue_shared/directives/tooltip'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index 61734163b6e..b8a96b23012 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -1,7 +1,7 @@ -/* global Flash */ import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import simplePoll from '~/lib/utils/simple_poll'; +import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon'; import eventHub from '../../event_hub'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js index 54be1fbe675..4f83350e07c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js @@ -1,4 +1,3 @@ -/* global Flash */ import statusIcon from '../mr_widget_status_icon'; import tooltip from '../../../vue_shared/directives/tooltip'; import eventHub from '../../event_hub'; @@ -27,12 +26,12 @@ export default { .then(res => res.json()) .then((res) => { eventHub.$emit('UpdateWidgetData', res); - new Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line + new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line $('.merge-request .detail-page-description .title').text(this.mr.title); }) .catch(() => { this.isMakingRequest = false; - new Flash('Something went wrong. Please try again.'); // eslint-disable-line + new window.Flash('Something went wrong. Please try again.'); // eslint-disable-line }); }, }, diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 044b664484b..4f497b204a3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -1,5 +1,4 @@ -/* global Flash */ - +import Flash from '../flash'; import { WidgetHeader, WidgetMergeHelp, diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 759d30c9c7c..8c0d9b9cda8 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,5 +1,6 @@ <script> - /* global Flash */ + import Flash from '../../../flash'; + import GLForm from '../../../gl_form'; import markdownHeader from './header.vue'; import markdownToolbar from './toolbar.vue'; @@ -85,7 +86,7 @@ /* GLForm class handles all the toolbar buttons */ - return new gl.GLForm($(this.$refs['gl-form']), true); + return new GLForm($(this.$refs['gl-form']), true); }, beforeDestroy() { const glForm = $(this.$refs['gl-form']).data('gl-form'); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c7be94e2c8e..0d7a5cba928 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -38,6 +38,7 @@ @import "framework/new-sidebar"; @import "framework/tables"; @import "framework/notes"; +@import "framework/tabs"; @import "framework/timeline"; @import "framework/tooltips"; @import "framework/typography"; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 5b950ae0ba0..9dcf332eee2 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -749,7 +749,7 @@ margin-bottom: $dropdown-vertical-offset; } - li:not(.dropdown-bold-header) { + li { display: block; padding: 0 1px; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 22945e935ef..d79444fad79 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -161,9 +161,10 @@ } } - .dropdown-bold-header { + li.dropdown-bold-header { color: $gl-text-color-secondary; font-size: 12px; + padding: 0 16px; } .navbar-collapse { diff --git a/app/assets/stylesheets/framework/new-sidebar.scss b/app/assets/stylesheets/framework/new-sidebar.scss index caf4c7a40b1..78972717932 100644 --- a/app/assets/stylesheets/framework/new-sidebar.scss +++ b/app/assets/stylesheets/framework/new-sidebar.scss @@ -90,7 +90,7 @@ $new-sidebar-collapsed-width: 50px; top: $header-height; bottom: 0; left: 0; - background-color: $gray-normal; + background-color: $gray-light; box-shadow: inset -2px 0 0 $border-color; transform: translate3d(0, 0, 0); diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss index 5c96b3b78e7..3fd2549b143 100644 --- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss +++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss @@ -6,6 +6,7 @@ margin: 0; list-style: none; height: auto; + border-bottom: 1px solid $border-color; li { display: flex; @@ -24,6 +25,7 @@ &:focus { text-decoration: none; color: $black; + border-bottom: 2px solid $gray-darkest; .badge { color: $black; diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 50f1445bc2e..58f6e62b06a 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -61,6 +61,7 @@ border: 1px solid $dropdown-border-color; min-width: 175px; color: $gl-text-color; + z-index: 999; } .select2-drop.select2-drop-above.select2-drop-active { diff --git a/app/assets/stylesheets/framework/tabs.scss b/app/assets/stylesheets/framework/tabs.scss new file mode 100644 index 00000000000..c8ba14b7066 --- /dev/null +++ b/app/assets/stylesheets/framework/tabs.scss @@ -0,0 +1,35 @@ +.gitlab-tabs { + background: $gray-light; + border: 1px solid $border-color; + + li { + width: 50%; + + &:not(:last-child) { + border-right: 1px solid $border-color; + } + + &.active { + background: $white-light; + } + + a { + width: 100%; + text-align: center; + } + } +} + +.gitlab-tab-content { + border: 1px solid $border-color; + border-top: 0; + margin-bottom: $gl-padding; + + .tab-pane { + padding: $gl-padding; + + &.no-padding { + padding: 0; + } + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index bbbd16322eb..089a67a7c98 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -699,14 +699,6 @@ $perf-bar-bucket-color: #ccc; $perf-bar-bucket-box-shadow-from: rgba($white-light, .2); $perf-bar-bucket-box-shadow-to: rgba($black, .25); - -/* -Project Templates Icons -*/ -$rails: #c00; -$node: #353535; -$java: #70ad51; - /* Issuable warning */ diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index b3bab082a35..692acf74a58 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -3,41 +3,12 @@ border-bottom: 1px solid $border-color; } -.project-member-tabs { - background: $gray-light; - border: 1px solid $border-color; - - li { - width: 50%; - - &.active { - background: $white-light; - } - - &:first-child { - border-right: 1px solid $border-color; - } - - a { - width: 100%; - text-align: center; - } - } -} - .users-project-form { .btn-create { margin-right: 10px; } } -.project-member-tab-content { - padding: $gl-padding; - border: 1px solid $border-color; - border-top: 0; - margin-bottom: $gl-padding; -} - .member { .list-item-name { @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a086c11324d..bd385db9692 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -48,7 +48,8 @@ border: 1px solid $border-color; } - + .select2 a { + + .select2 a, + + .btn-default { border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -549,10 +550,96 @@ a.deploy-project-label { } } -.project-template, +.project-template { + > .form-group { + margin-bottom: 0; + } + + .template-option { + padding: $gl-padding $gl-padding $gl-padding ($gl-padding * 4); + position: relative; + + &:not(:first-child) { + border-top: 1px solid $border-color; + } + } + + .template-title { + font-size: 16px; + } + + .template-description { + margin: 6px 0 12px; + } + + .template-button { + input { + position: absolute; + clip: rect(0, 0, 0, 0); + } + } + + svg { + position: absolute; + left: $gl-padding; + top: $gl-padding; + } + + .project-fields-form { + display: none; + + &.selected { + display: block; + padding: $gl-padding; + } + } + + .template-input-group { + position: relative; + + @media (min-width: $screen-sm-min) { + display: flex; + } + + .input-group-addon { + flex: 1; + text-align: left; + padding-left: ($gl-padding * 3); + background-color: $white-light; + } + + .selected-template { + line-height: 20px; + } + + .selected-icon { + svg { + display: none; + top: 7px; + height: 20px; + width: 20px; + + &.active { + display: block; + } + } + } + } +} + +.gitlab-tab-content { + .import-project-pane { + padding-bottom: 6px; + } +} + .project-import { - .form-group { - margin-bottom: 5px; + .import-btn-container { + margin-bottom: 0; + } + + .toggle-import-form { + padding-bottom: 10px; } .import-buttons { @@ -567,10 +654,6 @@ a.deploy-project-label { margin-right: 10px; } - .blank-option { - min-width: 70px; - } - .btn-template-icon { height: 24px; width: inherit; @@ -592,18 +675,6 @@ a.deploy-project-label { } } - .icon-rails path { - fill: $rails; - } - - .icon-node-express path { - fill: $node; - } - - .icon-java-spring path { - fill: $java; - } - > div { margin-bottom: 10px; padding-left: 0; @@ -611,10 +682,6 @@ a.deploy-project-label { } } -.project-templates-buttons .btn:last-child { - margin-right: 0; -} - .create-project-options { display: flex; @@ -1053,6 +1120,12 @@ pre.light-well { min-width: 100px; } + &.form-group { + @media (min-width: $screen-sm-min) { + margin-bottom: 0; + } + } + .select2-choice { border-top-right-radius: 0; border-bottom-right-radius: 0; diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 719893c0bc8..38b808cdc31 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -2,7 +2,8 @@ class Admin::RunnersController < Admin::ApplicationController before_action :runner, except: :index def index - @runners = Ci::Runner.order('id DESC') + sort = params[:sort] == 'contacted_asc' ? { contacted_at: :asc } : { id: :desc } + @runners = Ci::Runner.order(sort) @runners = @runners.search(params[:search]) if params[:search].present? @runners = @runners.page(params[:page]).per(30) @active_runners_cnt = Ci::Runner.online.count diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb new file mode 100644 index 00000000000..5ce602b55a8 --- /dev/null +++ b/app/controllers/concerns/preview_markdown.rb @@ -0,0 +1,22 @@ +module PreviewMarkdown + extend ActiveSupport::Concern + + def preview_markdown + result = PreviewMarkdownService.new(@project, current_user, params).execute + + markdown_params = + case controller_name + when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] } + when 'snippets' then { skip_project_check: true } + else {} + end + + render json: { + body: view_context.markdown(result[:text], markdown_params), + references: { + users: result[:users], + commands: view_context.markdown(result[:commands]) + } + } + end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 3769a2cde33..a962d82e3b5 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -2,6 +2,7 @@ class GroupsController < Groups::ApplicationController include IssuesAction include MergeRequestsAction include ParamsBackwardCompatibility + include PreviewMarkdown respond_to :html diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index a8ebdf5a4a9..f7a9c98629d 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,4 +1,6 @@ class Projects::WikisController < Projects::ApplicationController + include PreviewMarkdown + before_action :authorize_read_wiki! before_action :authorize_create_wiki!, only: [:edit, :create, :history] before_action :authorize_admin_wiki!, only: :destroy @@ -92,17 +94,6 @@ class Projects::WikisController < Projects::ApplicationController def git_access end - def preview_markdown - result = PreviewMarkdownService.new(@project, current_user, params).execute - - render json: { - body: view_context.markdown(result[:text], pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id]), - references: { - users: result[:users] - } - } - end - private def load_project_wiki diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a738ca9f361..e90b75672ae 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,6 +1,7 @@ class ProjectsController < Projects::ApplicationController include IssuableCollections include ExtractsPath + include PreviewMarkdown before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :project, except: [:index, :new, :create] @@ -258,18 +259,6 @@ class ProjectsController < Projects::ApplicationController render json: options.to_json end - def preview_markdown - result = PreviewMarkdownService.new(@project, current_user, params).execute - - render json: { - body: view_context.markdown(result[:text]), - references: { - users: result[:users], - commands: view_context.markdown(result[:commands]) - } - } - end - private # Render project landing depending of which features are available diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index c1cdc7c9831..be2d3f638ff 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -4,6 +4,7 @@ class SnippetsController < ApplicationController include SpammableActions include SnippetsActions include RendersBlob + include PreviewMarkdown before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] @@ -87,17 +88,6 @@ class SnippetsController < ApplicationController redirect_to snippets_path, status: 302 end - def preview_markdown - result = PreviewMarkdownService.new(@project, current_user, params).execute - - render json: { - body: view_context.markdown(result[:text], skip_project_check: true), - references: { - users: result[:users] - } - } - end - protected def snippet diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 2c28dd81c87..8bf96c0905f 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -4,8 +4,8 @@ module CompareHelper to.present? && from != to && can?(current_user, :create_merge_request, project) && - project.repository.branch_names.include?(from) && - project.repository.branch_names.include?(to) + project.repository.branch_exists?(from) && + project.repository.branch_exists?(to) end def create_mr_path(from = params[:from], to = params[:to], project = @project) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 82bceddf1f0..676c1d1988b 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -7,7 +7,12 @@ module GroupsHelper can?(current_user, :change_share_with_group_lock, group) end - def group_icon(group) + def group_icon(group, options = {}) + img_path = group_icon_url(group, options) + image_tag img_path, options + end + + def group_icon_url(group, options = {}) if group.is_a?(String) group = Group.find_by_full_path(group) end @@ -89,7 +94,7 @@ module GroupsHelper link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do output = if (group.try(:avatar_url) || show_avatar) && !Rails.env.test? - image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15) + group_icon(group, class: "avatar-tile", width: 15, height: 15) else "" end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 7713fb0b9f8..baa2d6e375e 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -314,20 +314,12 @@ module IssuablesHelper @issuable_templates ||= case issuable when Issue - issue_template_names + ref_project.repository.issue_template_names when MergeRequest - merge_request_template_names + ref_project.repository.merge_request_template_names end end - def merge_request_template_names - @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project) - end - - def issue_template_names - @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project) - end - def selected_template(issuable) params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] } end diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb index 2c5619ac41b..603b9438e35 100644 --- a/app/helpers/lazy_image_tag_helper.rb +++ b/app/helpers/lazy_image_tag_helper.rb @@ -10,6 +10,7 @@ module LazyImageTagHelper unless options.delete(:lazy) == false options[:data] ||= {} options[:data][:src] = path_to_image(source) + options[:class] ||= "" options[:class] << " lazy" diff --git a/app/models/blob.rb b/app/models/blob.rb index 954d4e4d779..ad0bc2e2ead 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -156,7 +156,9 @@ class Blob < SimpleDelegator end def file_type - Gitlab::FileDetector.type_of(path) + name = File.basename(path) + + Gitlab::FileDetector.type_of(path) || Gitlab::FileDetector.type_of(name) end def video? diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 8fbfed11bdf..2ec70203710 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -11,7 +11,7 @@ module Avatarable # If asset_host is set then it is expected that assets are handled by a standalone host. # That means we do not want to get GitLab's relative_url_root option anymore. - host = asset_host.present? ? asset_host : gitlab_host + host = (asset_host.present? && (!respond_to?(:public?) || public?)) ? asset_host : gitlab_host [host, avatar.url].join end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 75e9bdaaa45..972a35dde4d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -675,13 +675,13 @@ class MergeRequest < ActiveRecord::Base def source_branch_exists? return false unless self.source_project - self.source_project.repository.branch_names.include?(self.source_branch) + self.source_project.repository.branch_exists?(self.source_branch) end def target_branch_exists? return false unless self.target_project - self.target_project.repository.branch_names.include?(self.target_branch) + self.target_project.repository.branch_exists?(self.target_branch) end def merge_commit_message(include_description: false) diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index b85f5dbaf2e..f89e60ad9f4 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -1,4 +1,6 @@ class OauthAccessToken < Doorkeeper::AccessToken belongs_to :resource_owner, class_name: 'User' belongs_to :application, class_name: 'Doorkeeper::Application' + + alias_method :user, :resource_owner end diff --git a/app/models/repository.rb b/app/models/repository.rb index d725c65081d..bf526ca1762 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -34,7 +34,8 @@ class Repository CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide changelog license_blob license_key gitignore koding_yml gitlab_ci_yml branch_names tag_names branch_count - tag_count avatar exists? empty? root_ref has_visible_content?).freeze + tag_count avatar exists? empty? root_ref has_visible_content? + issue_template_names merge_request_template_names).freeze # Methods that use cache_method but only memoize the value MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze @@ -50,7 +51,9 @@ class Repository gitignore: :gitignore, koding: :koding_yml, gitlab_ci: :gitlab_ci_yml, - avatar: :avatar + avatar: :avatar, + issue_template: :issue_template_names, + merge_request_template: :merge_request_template_names }.freeze # Wraps around the given method and caches its output in Redis and an instance @@ -535,6 +538,16 @@ class Repository end cache_method :avatar + def issue_template_names + Gitlab::Template::IssueTemplate.dropdown_names(project) + end + cache_method :issue_template_names, fallback: [] + + def merge_request_template_names + Gitlab::Template::MergeRequestTemplate.dropdown_names(project) + end + cache_method :merge_request_template_names, fallback: [] + def readme if readme = tree(:head)&.readme ReadmeBlob.new(readme, self) diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb index 7c872a3e986..6d8466da902 100644 --- a/app/serializers/group_entity.rb +++ b/app/serializers/group_entity.rb @@ -45,6 +45,6 @@ class GroupEntity < Grape::Entity end expose :avatar_url do |group| - group_icon(group) + group_icon_url(group) end end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index a110abf8256..8c5821aa870 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -60,13 +60,9 @@ module MergeRequests def after_merge MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) - if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? - # Verify again that the source branch can be removed, since branch may be protected, - # or the source branch may have been updated. - if @merge_request.can_remove_source_branch?(branch_deletion_user) - DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) - .execute(merge_request.source_branch) - end + if delete_source_branch? + DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) + .execute(merge_request.source_branch) end end @@ -78,6 +74,14 @@ module MergeRequests @merge_request.force_remove_source_branch? ? @merge_request.author : current_user end + # Verify again that the source branch can be removed, since branch may be protected, + # or the source branch may have been updated, or the user may not have permission + # + def delete_source_branch? + params.fetch('should_remove_source_branch', @merge_request.force_remove_source_branch?) && + @merge_request.can_remove_source_branch?(branch_deletion_user) + end + # Logs merge error message and cleans `MergeRequest#merge_jid`. # def handle_merge_error(log_message:, save_message_on_model: false) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 8d5da459882..be3b4b2ba07 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -390,7 +390,7 @@ class NotificationService end def relabeled_resource_email(target, labels, current_user, method) - recipients = labels.flat_map { |l| l.subscribers(target.project) } + recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq recipients = notifiable_users( recipients, :subscription, target: target, diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index a077b3584b0..955d934838b 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -458,7 +458,7 @@ module QuickActions target_branch_param.strip end command :target_branch do |branch_name| - @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) + @updates[:target_branch] = branch_name if project.repository.branch_exists?(branch_name) end desc 'Move issue from one column of the board to another' diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 7b32e215c7f..a52dce6cb4b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -593,7 +593,7 @@ module SystemNoteService def discussion_lock(issuable, author) action = issuable.discussion_locked? ? 'locked' : 'unlocked' - body = "#{action} this issue" + body = "#{action} this #{issuable.class.to_s.titleize.downcase}" create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action)) end diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index e3a77dfdf10..47cc2d4d27e 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -20,7 +20,7 @@ = visibility_level_icon(group.visibility_level, fw: false) .avatar-container.s40 - = image_tag group_icon(group), class: "avatar s40 hidden-xs" + = group_icon(group, class: "avatar s40 hidden-xs") .title = link_to [:admin, group], class: 'group-name' do = group.full_name diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 3e02f7b1e16..2545cecc721 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -16,7 +16,7 @@ %ul.well-list %li .avatar-container.s60 - = image_tag group_icon(@group), class: "avatar s60" + = group_icon(@group, class: "avatar s60") %li %span.light Name: %strong= @group.name diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 43cea1358cc..76f4a817744 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -63,7 +63,7 @@ %th Projects %th Jobs %th Tags - %th Last contact + %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc')) %th - @runners.each do |runner| diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 52279d0a870..4b6c4581eb3 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -7,4 +7,4 @@ %td.notes_line{ colspan: 2 } %td.notes_content .content{ class: ('hide' unless expanded) } - = render partial: "discussions/notes", collection: discussions, as: :discussion + = render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true } diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 636d06cab53..f9bfc01f213 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -24,4 +24,4 @@ = render partial: "projects/diffs/#{partial}", locals: { diff_file: diff_file, position: discussion.position.to_json, click_to_comment: false } .note-container - = render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse: true } + = render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse_class: true } diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index 9efcfef690f..1cc227428e9 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -1,5 +1,5 @@ -- disable_collapse = local_assigns.fetch(:disable_collapse, false) -- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse +- disable_collapse_class = local_assigns.fetch(:disable_collapse_class, false) +- collapsed_class = 'collapsed' if discussion.resolved? && !disable_collapse_class - badge_counter = discussion_counter + 1 if local_assigns[:discussion_counter] - show_toggle = local_assigns.fetch(:show_toggle, true) - show_image_comment_badge = local_assigns.fetch(:show_image_comment_badge, false) diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index 181c7bee702..a0760c2073b 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -1,7 +1,7 @@ .group-home-panel.text-center %div{ class: container_class } .avatar-container.s70.group-avatar - = image_tag group_icon(@group), class: "avatar s70 avatar-tile" + = group_icon(@group, class: "avatar s70 avatar-tile") %h1.group-title = @group.name %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 15606dd30fd..16038ef2f79 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -10,7 +10,7 @@ .form-group .col-sm-offset-2.col-sm-10 .avatar-container.s160 - = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160' + = group_icon(@group, alt: '', class: 'avatar group-avatar s160') %p.light - if @group.avatar? You can change your group avatar here diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index 7f450cd9a93..cc879e5a308 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -10,7 +10,7 @@ .form-group.milestone-description = f.label :description, "Description", class: "control-label" .col-sm-10 - = render layout: 'projects/md_preview', locals: { url: '' } do + = render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' .clearfix .error-alert diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 8cba495f7e4..0bf318b0b66 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -6,7 +6,7 @@ .context-header = link_to group_path(@group), title: @group.name do .avatar-container.s40.group-avatar - = image_tag group_icon(@group), class: "avatar s40 avatar-tile" + = group_icon(@group, class: "avatar s40 avatar-tile") .sidebar-context-title = @group.name %ul.sidebar-top-level-items diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index b7a60938132..8eb3f2d5192 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -31,7 +31,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } = @pipeline.ref @@ -42,7 +42,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = @pipeline.short_sha @@ -60,7 +60,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.committer %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@ triggered by - if @pipeline.user %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } - %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } = @pipeline.user.name diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 3f16885b8e3..574a8f2fa50 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -31,7 +31,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/ + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" } = @pipeline.ref @@ -42,7 +42,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/ + %img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = @pipeline.short_sha @@ -60,7 +60,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.committer %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@ triggered by - if @pipeline.user %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } - %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } = @pipeline.user.name diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml new file mode 100644 index 00000000000..a78a8e5d628 --- /dev/null +++ b/app/views/projects/_new_project_fields.html.haml @@ -0,0 +1,41 @@ +- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility + +.row{ id: project_name_id } + .form-group.project-path.col-sm-6 + = f.label :namespace_id, class: 'label-light' do + %span + Project path + .input-group + - if current_user.can_select_namespace? + .input-group-addon + = root_url + = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1} + + - else + .input-group-addon.static-namespace + #{user_url(current_user.username)}/ + = f.hidden_field :namespace_id, value: current_user.namespace_id + .form-group.project-path.col-sm-6 + = f.label :path, class: 'label-light' do + %span + Project name + = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true +- if current_user.can_create_group? + .help-block + Want to house several dependent projects under the same namespace? + = link_to "Create a group", new_group_path + +.form-group + = f.label :description, class: 'label-light' do + Project description + %span (optional) + = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250 + +.form-group.visibility-level-setting + = f.label :visibility_level, class: 'label-light' do + Visibility Level + = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' } + = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false + += f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 += link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index 5638b7da1b0..d50175727be 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -1,10 +1,24 @@ -.project-templates-buttons.import-buttons{ data: { toggle: "buttons" } } - .btn.blank-option.active - %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: "blank", checked: "true", value: "" } - = icon('file-o', class: 'btn-template-icon') - Blank +.project-templates-buttons.import-buttons - Gitlab::ProjectTemplate.all.each do |template| - .btn - %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name } + .template-option = custom_icon(template.logo) - = template.title + .template-title= template.title + .template-description= template.description + %label.btn.btn-success.template-button.choose-template.append-right-10{ for: template.name } + %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name } + %span Use template + %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview + + .project-fields-form + .form-group + %label.label-light + Template + .input-group.template-input-group + .input-group-addon + .selected-icon + - Gitlab::ProjectTemplate.all.each do |template| + = custom_icon(template.logo) + .selected-template + %button.btn.btn-default.change-template{ type: "button" } Change template + + = render 'new_project_fields', f: f, project_name_id: "template-project-name" diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 8ae4fd94146..893e536e289 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -97,7 +97,7 @@ %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' %p - Perform advanced options such as housekeeping, exporting, archiving, renaming, transferring, or removing your project. + Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project. .settings-content.no-animate{ class: ('expanded' if expanded) } .sub-section %h4 Housekeeping diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index 43e23bb2200..d5c6d329ce4 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -48,7 +48,7 @@ - if @build.trigger_variables.any? %p - %button.btn.group.btn-group-justified.reveal-variables Reveal Variables + %button.btn.group.btn-group-justified.js-reveal-variables Reveal Variables %dl.js-build-variables.trigger-build-variables.hide - @build.trigger_variables.each do |trigger_variable| diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cc41b908946..0a835dcdeb0 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -14,114 +14,88 @@ .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 New project - - if import_sources_enabled? - %p - A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}. - %p - All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings. + %p + A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}. + %p + All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings. .col-lg-9.js-toggle-container - = form_for @project, html: { class: 'new_project' } do |f| - .create-project-options - .first-column + %ul.nav-links.gitlab-tabs{ role: 'tablist' } + %li.active{ role: 'presentation' } + %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %span.hidden-xs Blank project + %span.visible-xs Blank + %li{ role: 'presentation' } + %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } + %span.hidden-xs Create from template + %span.visible-xs Template + %li{ role: 'presentation' } + %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %span.hidden-xs Import project + %span.visible-xs Import + + .tab-content.gitlab-tab-content + .tab-pane.active{ id: 'blank-project-pane', role: 'tabpanel' } + = form_for @project, html: { class: 'new_project' } do |f| + = render 'new_project_fields', f: f, project_name_id: "blank-project-name" + + .tab-pane.no-padding{ id: 'create-from-template-pane', role: 'tabpanel' } + = form_for @project, html: { class: 'new_project' } do |f| .project-template .form-group - = f.label :template_project, class: 'label-light' do - Create from template - = link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'} %div = render 'project_templates', f: f - - if import_sources_enabled? - .second-column - .project-import - .form-group.clearfix - = f.label :visibility_level, class: 'label-light' do #the label here seems wrong - Import project from - .col-sm-12.import-buttons - %div - - if github_import_enabled? - = link_to new_import_github_path, class: 'btn import_github' do - = icon('github', text: 'GitHub') - %div - - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do - = icon('bitbucket', text: 'Bitbucket') - - unless bitbucket_import_configured? - = render 'bitbucket_import_modal' - %div - - if gitlab_import_enabled? - = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do - = icon('gitlab', text: 'GitLab.com') - - unless gitlab_import_configured? - = render 'gitlab_import_modal' - %div - - if google_code_import_enabled? - = link_to new_import_google_code_path, class: 'btn import_google_code' do - = icon('google', text: 'Google Code') - %div - - if fogbugz_import_enabled? - = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - = icon('bug', text: 'Fogbugz') - %div - - if gitea_import_enabled? - = link_to new_import_gitea_url, class: 'btn import_gitea' do - = custom_icon('go_logo') - Gitea - %div - - if git_import_enabled? - %button.btn.js-toggle-button.import_git{ type: "button" } - = icon('git', text: 'Repo by URL') - - if gitlab_project_import_enabled? - .import_gitlab_project.has-tooltip{ data: { container: 'body' } } - = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do - = icon('gitlab', text: 'GitLab export') - - .row - .col-lg-12 - .js-toggle-content.hide - %hr - = render "shared/import_form", f: f - %hr - - .row - .form-group.col-xs-12.col-sm-6 - = f.label :namespace_id, class: 'label-light' do - %span - Project path - .form-group - .input-group - - if current_user.can_select_namespace? - .input-group-addon - = root_url - = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1} - - - else - .input-group-addon.static-namespace - #{root_url}#{current_user.username}/ - = f.hidden_field :namespace_id, value: current_user.namespace_id - .form-group.col-xs-12.col-sm-6.project-path - = f.label :path, class: 'label-light' do - %span - Project name - = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true - - if current_user.can_create_group? - .help-block - Want to house several dependent projects under the same namespace? - = link_to "Create a group", new_group_path - - .form-group - = f.label :description, class: 'label-light' do - Project description - %span.light (optional) - = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250 - - .form-group.visibility-level-setting - = f.label :visibility_level, class: 'label-light' do - Visibility Level - = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' } - = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false - = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 - = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' + .tab-pane.import-project-pane{ id: 'import-project-pane', role: 'tabpanel' } + = form_for @project, html: { class: 'new_project' } do |f| + - if import_sources_enabled? + .project-import.row + .col-sm-12 + .form-group.import-btn-container.clearfix + = f.label :visibility_level, class: 'label-light' do #the label here seems wrong + Import project from + .import-buttons + %div + - if github_import_enabled? + = link_to new_import_github_path, class: 'btn import_github' do + = icon('github', text: 'GitHub') + %div + - if bitbucket_import_enabled? + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do + = icon('bitbucket', text: 'Bitbucket') + - unless bitbucket_import_configured? + = render 'bitbucket_import_modal' + %div + - if gitlab_import_enabled? + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do + = icon('gitlab', text: 'GitLab.com') + - unless gitlab_import_configured? + = render 'gitlab_import_modal' + %div + - if google_code_import_enabled? + = link_to new_import_google_code_path, class: 'btn import_google_code' do + = icon('google', text: 'Google Code') + %div + - if fogbugz_import_enabled? + = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do + = icon('bug', text: 'Fogbugz') + %div + - if gitea_import_enabled? + = link_to new_import_gitea_url, class: 'btn import_gitea' do + = custom_icon('go_logo') + Gitea + %div + - if git_import_enabled? + %button.btn.js-toggle-button.import_git{ type: "button" } + = icon('git', text: 'Repo by URL') + - if gitlab_project_import_enabled? + .import_gitlab_project.has-tooltip{ data: { container: 'body' } } + = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do + = icon('gitlab', text: 'GitLab export') + .col-lg-12 + .js-toggle-content.hide.toggle-import-form + %hr + = render "shared/import_form", f: f + = render 'new_project_fields', f: f, project_name_id: "import-url-name" .save-project-loader.hide .center diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 25153fd0b6f..fd5d3ec56da 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -17,14 +17,14 @@ %i Owners .light - if can?(current_user, :admin_project_member, @project) - %ul.nav-links.project-member-tabs{ role: 'tablist' } + %ul.nav-links.gitlab-tabs{ role: 'tablist' } %li.active{ role: 'presentation' } %a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member - if @project.allowed_to_share_with_group? %li{ role: 'presentation' } %a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group - .tab-content.project-member-tab-content + .tab-content.gitlab-tab-content .tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' } = render 'projects/project_members/new_project_member', tab_title: 'Add member' .tab-pane{ id: 'share-with-group-pane', role: 'tabpanel' } diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index b361ec86ced..63f62eb476e 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -28,7 +28,7 @@ .avatar-container.s40 = link_to group do - = image_tag group_icon(group), class: "avatar s40 hidden-xs" + = group_icon(group, class: "avatar s40 hidden-xs") .title = link_to group_name, group, class: 'group-name' diff --git a/app/views/shared/icons/_express.svg b/app/views/shared/icons/_express.svg index f2c94319f19..a51e81e5568 100644 --- a/app/views/shared/icons/_express.svg +++ b/app/views/shared/icons/_express.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express"> - <g fill="none" fill-rule="evenodd" transform="translate(-3)"> - <rect width="32" height="32"/> - <path fill="#353535" d="M4.19170065,16.2667139 C4.23142421,18.3323387 4.47969269,20.2489714 4.93651356,22.0166696 C5.39333443,23.7843677 6.09841693,25.3236323 7.05178222,26.6345096 C8.00514751,27.9453869 9.23655921,28.9781838 10.7460543,29.7329313 C12.2555493,30.4876788 14.1026668,30.8650469 16.2874623,30.8650469 C19.5050701,30.8650469 22.1764391,30.0209341 24.3016492,28.3326831 C26.4268593,26.644432 27.7476477,24.1120935 28.2640539,20.7355914 L29.4557545,20.7355914 C29.0187954,24.3107112 27.6086304,27.0813875 25.2252172,29.0477034 C22.841804,31.0140194 19.9023051,31.9971626 16.4066324,31.9971626 C14.0232191,32.0368861 11.9874175,31.659518 10.2991665,30.8650469 C8.61091547,30.0705759 7.23054269,28.9484023 6.15800673,27.4984926 C5.08547078,26.0485829 4.29101162,24.3404957 3.77460543,22.3741798 C3.25819923,20.4078639 3,18.2926164 3,16.0283738 C3,13.4860664 3.3773681,11.2218578 4.13211562,9.23568007 C4.88686314,7.24950238 5.87993709,5.57120741 7.11136726,4.20074481 C8.34279742,2.8302822 9.77282391,1.78755456 11.4014896,1.07253059 C13.0301553,0.357506621 14.6985195,0 16.4066324,0 C18.7900456,0 20.8457087,0.456814016 22.5736832,1.37045575 C24.3016578,2.28409749 25.7118228,3.4956477 26.8042206,5.00514275 C27.8966183,6.51463779 28.6910775,8.24258646 29.1876219,10.1890406 C29.6841663,12.1354947 29.8927118,14.1613656 29.8132647,16.2667139 L4.19170065,16.2667139 Z M28.6215641,15.0750133 C28.6215641,13.2080062 28.3633648,11.4304039 27.8469586,9.74215285 C27.3305524,8.05390181 26.5658855,6.57422163 25.5529349,5.30306791 C24.5399843,4.03191419 23.2787803,3.0289095 21.7692853,2.29402376 C20.2597903,1.55913801 18.5119801,1.19170065 16.5258024,1.19170065 C14.8574132,1.19170065 13.2982871,1.50948432 11.8483774,2.14506118 C10.3984676,2.78063804 9.12733299,3.70419681 8.03493526,4.9157652 C6.94253754,6.12733359 6.05870172,7.58715229 5.38340131,9.2952651 C4.70810089,11.0033779 4.31087132,12.9299414 4.19170065,15.0750133 L28.6215641,15.0750133 Z"/> - </g> -</svg> +<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express"><g fill="none" fill-rule="evenodd"><path d="M-3 0h32v32H-3z"/><path fill="#353535" d="M1.192 16.267c.04 2.065.288 3.982.745 5.75.456 1.767 1.16 3.307 2.115 4.618.953 1.31 2.185 2.343 3.694 3.098 1.51.755 3.357 1.132 5.54 1.132 3.22 0 5.89-.844 8.016-2.532 2.125-1.69 3.446-4.22 3.962-7.597h1.192c-.437 3.575-1.847 6.345-4.23 8.312-2.384 1.966-5.324 2.95-8.82 2.95-2.383.04-4.42-.338-6.107-1.133-1.69-.794-3.07-1.917-4.142-3.367-1.073-1.45-1.867-3.158-2.383-5.124C.258 20.408 0 18.294 0 16.028c0-2.542.377-4.806 1.132-6.792C1.887 7.25 2.88 5.57 4.112 4.2 5.34 2.83 6.77 1.79 8.4 1.074 10.03.358 11.698 0 13.406 0c2.383 0 4.44.457 6.167 1.37 1.728.914 3.138 2.126 4.23 3.635 1.093 1.51 1.887 3.238 2.384 5.184.496 1.945.705 3.97.625 6.077H1.193zm24.43-1.192c0-1.867-.26-3.645-.775-5.333-.516-1.688-1.28-3.168-2.294-4.44-1.013-1.27-2.274-2.273-3.784-3.008-1.51-.735-3.258-1.102-5.244-1.102-1.67 0-3.228.317-4.678.953-1.45.636-2.72 1.56-3.813 2.77-1.092 1.212-1.976 2.672-2.652 4.38-.675 1.708-1.072 3.635-1.19 5.78h24.43z"/></g></svg> diff --git a/app/views/shared/icons/_rails.svg b/app/views/shared/icons/_rails.svg index 0bb09a705df..852bd183cc7 100644 --- a/app/views/shared/icons/_rails.svg +++ b/app/views/shared/icons/_rails.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails"> - <g fill="none" fill-rule="evenodd" transform="translate(0 -6)"> - <rect width="32" height="32"/> - <path fill="#C00" fill-rule="nonzero" d="M0.984615385,25.636044 C0.984615385,25.636044 1.40659341,21.4725275 4.36043956,16.5494505 C7.31428571,11.6263736 12.3498901,7.8989011 16.4430769,7.53318681 C24.5872527,6.71736264 31.9015385,14.0175824 31.9015385,14.0175824 C31.9015385,14.0175824 31.6624176,14.1863736 31.4092308,14.3973626 C23.4197802,8.48967033 18.5389011,11.2747253 17.0057143,12.0202198 C9.97274725,15.9446154 12.0967033,25.636044 12.0967033,25.636044 L0.984615385,25.636044 Z M24.1371429,8.32087912 C23.687033,8.13802198 23.2369231,7.96923077 22.7727473,7.81450549 L22.829011,6.88615385 C23.7151648,7.13934066 24.0668132,7.30813187 24.1934066,7.37846154 L24.1371429,8.32087912 Z M22.8008791,11.3028571 C23.250989,11.330989 23.7151648,11.3872527 24.1934066,11.4857143 L24.1371429,12.3578022 C23.672967,12.2593407 23.2087912,12.2030769 22.7446154,12.189011 L22.8008791,11.3028571 Z M17.5964835,6.91428571 C17.1885714,6.91428571 16.7806593,6.92835165 16.3727473,6.97054945 L16.1054945,6.14065934 C16.5696703,6.0843956 17.0197802,6.05626374 17.4558242,6.05626374 L17.7371429,6.91428571 C17.6949451,6.91428571 17.6386813,6.91428571 17.5964835,6.91428571 Z M18.2716484,12.0905495 C18.6232967,11.9358242 19.0312088,11.7810989 19.5094505,11.6404396 L19.8189011,12.5687912 C19.410989,12.6953846 19.0030769,12.8641758 18.5951648,13.0610989 L18.2716484,12.0905495 Z M11.8857143,8.39120879 C11.52,8.57406593 11.1683516,8.78505495 10.8026374,9.01010989 L10.1556044,8.02549451 C10.5353846,7.80043956 10.9010989,7.60351648 11.2527473,7.42065934 L11.8857143,8.39120879 Z M14.7692308,14.7208791 C15.0224176,14.3973626 15.3178022,14.0738462 15.6413187,13.7784615 L16.2742857,14.7349451 C15.9648352,15.0584615 15.6835165,15.381978 15.4443956,15.7336264 L14.7692308,14.7208791 Z M12.7296703,19.2501099 C12.8421978,18.7437363 12.9687912,18.2232967 13.1516484,17.7028571 L14.1643956,18.5046154 C14.0237363,19.0531868 13.9252747,19.6017582 13.869011,20.1503297 L12.7296703,19.2501099 Z M6.56879121,12.5687912 C6.23120879,12.9204396 5.90769231,13.3002198 5.61230769,13.68 L4.52923077,12.7516484 C4.85274725,12.4 5.2043956,12.0483516 5.57010989,11.6967033 L6.56879121,12.5687912 Z M2.32087912,18.8562637 C2.09582418,19.3767033 1.80043956,20.0659341 1.61758242,20.5441758 L0,19.9534066 C0.140659341,19.5736264 0.436043956,18.8703297 0.703296703,18.2654945 L2.32087912,18.8562637 Z M12.5186813,22.8228571 L14.0378022,23.3714286 C14.1221978,24.0325275 14.2487912,24.6514286 14.3753846,25.2 L12.6874725,24.5951648 C12.6171429,24.1731868 12.5468132,23.5683516 12.5186813,22.8228571 Z"/> - </g> -</svg> +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails"><g fill="none" fill-rule="evenodd"><path d="M0-6h32v32H0z"/><path fill="#c00" fill-rule="nonzero" d="M.985 19.636s.422-4.163 3.375-9.087c2.954-4.924 7.99-8.65 12.083-9.017 8.144-.816 15.46 6.485 15.46 6.485s-.24.168-.494.38C23.42 2.49 18.54 5.274 17.005 6.02c-7.033 3.925-4.91 13.616-4.91 13.616H.987zM24.137 2.32c-.45-.182-.9-.35-1.364-.505l.056-.93c.885.254 1.237.423 1.363.493l-.056.943zM22.8 5.304c.45.028.915.084 1.393.183l-.056.872c-.464-.1-.928-.155-1.392-.17l.056-.885zM17.597.913c-.407 0-.815.015-1.223.058l-.268-.83c.465-.056.915-.084 1.35-.084l.282.858h-.14zm.676 5.178c.35-.154.76-.31 1.237-.45l.31.93c-.41.125-.817.294-1.225.49l-.323-.97zm-6.386-3.7c-.366.184-.718.395-1.083.62l-.647-.985c.38-.225.745-.42 1.097-.604l.633.97zm2.883 6.33c.252-.323.548-.646.87-.942l.634.957c-.31.323-.59.647-.83 1L14.77 8.72zm-2.04 4.53c.112-.506.24-1.027.422-1.547l1.012.802c-.14.548-.24 1.097-.295 1.645l-1.14-.9zM6.57 6.57c-.34.35-.662.73-.958 1.11L4.53 6.752c.323-.352.674-.704 1.04-1.055l1 .872zm-4.25 6.286c-.224.52-.52 1.21-.702 1.688L0 13.954c.14-.38.436-1.084.703-1.69l1.618.592zm10.2 3.967l1.518.548c.084.663.21 1.28.337 1.83l-1.688-.605c-.07-.422-.14-1.027-.168-1.772z"/></g></svg> diff --git a/app/views/shared/icons/_spring.svg b/app/views/shared/icons/_spring.svg index 508349aa456..ccf18749029 100644 --- a/app/views/shared/icons/_spring.svg +++ b/app/views/shared/icons/_spring.svg @@ -1,6 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring"> - <g fill="none" fill-rule="evenodd"> - <rect width="32" height="32"/> - <path fill="#70AD51" d="M5.46647617,27.9932117 C6.0517027,28.4658996 6.91159892,28.3777063 7.38425926,27.7914452 C7.85922261,27.2048452 7.76991326,26.3449044 7.18398981,25.8699411 C6.59874295,25.3956543 5.74015536,25.4869934 5.26383884,26.0722403 C4.81393367,26.6267596 4.87238621,27.4284565 5.37913494,27.9159868 L5.11431334,27.6818383 C1.97157151,24.7616933 0,20.5966301 0,15.9782542 C0,7.16842834 7.16775175,0 15.9796074,0 C20.4586065,0 24.5113565,1.8565519 27.4145869,4.8362365 C28.0749348,3.93840692 28.6466499,2.93435335 29.115524,1.82069284 C31.1513712,7.93770658 32.3482517,13.0811131 31.909824,17.1311567 C31.3178113,25.4044499 24.4017495,31.9585382 15.9796074,31.9585382 C12.0682639,31.9585382 8.48438805,30.5444735 5.7042963,28.2034861 L5.46647617,27.9932117 Z M29.0471888,23.0106888 C33.0546075,17.6737787 30.8211972,9.04527781 28.9612624,3.529749 C27.3029502,6.98304378 23.2217836,9.62375882 19.6981239,10.4613722 C16.3950312,11.2482417 13.4715032,10.6021021 10.4153644,11.7780085 C3.44517575,14.457289 3.55613585,22.7698242 7.39373146,24.6365249 C7.39711439,24.6392312 7.62444728,24.7616933 7.62174094,24.7576338 C7.62309411,24.7562806 13.2658211,23.6358542 16.3862356,22.4843049 C20.9450718,20.7996058 25.9524846,16.6494275 27.5986182,11.8273993 C26.723116,16.8415779 22.4179995,21.6669891 18.093262,23.8828081 C15.7908399,25.0648038 14.0005934,25.3279957 10.2123886,26.6385428 C9.74892722,26.798217 9.38492397,26.9538318 9.38492397,26.9538318 C10.3463526,26.7948341 11.301692,26.7420604 11.301692,26.7420604 C16.6954354,26.4869875 25.1087819,28.2582896 29.0471888,23.0106888 Z"/> - </g> -</svg> +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring"><g fill="none" fill-rule="evenodd"><path d="M0 0h32v32H0z"/><path fill="#70AD51" d="M5.466 27.993c.586.473 1.446.385 1.918-.202.475-.585.386-1.445-.2-1.92-.585-.474-1.444-.383-1.92.202-.45.555-.392 1.356.115 1.844l-.266-.234C1.972 24.762 0 20.597 0 15.978 0 7.168 7.168 0 15.98 0c4.48 0 8.53 1.857 11.435 4.836.66-.898 1.232-1.902 1.7-3.015 2.036 6.118 3.233 11.26 2.795 15.31-.592 8.274-7.508 14.83-15.93 14.83-3.912 0-7.496-1.416-10.276-3.757l-.238-.21zm23.58-4.982c4.01-5.336 1.775-13.965-.085-19.48-1.657 3.453-5.738 6.094-9.262 6.93-3.303.788-6.226.142-9.283 1.318-6.97 2.68-6.86 10.992-3.02 12.86.002 0 .23.124.227.12 0-.002 5.644-1.122 8.764-2.274 4.56-1.684 9.566-5.835 11.213-10.657-.877 5.015-5.182 9.84-9.507 12.056-2.302 1.182-4.092 1.445-7.88 2.756-.464.158-.828.314-.828.314.96-.16 1.917-.212 1.917-.212 5.393-.255 13.807 1.516 17.745-3.73z"/></g></svg> diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index bcdad3c153a..5868c52566d 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -4,7 +4,7 @@ - dom_id = "group_member_#{group_link.id}" %li.member.group_member{ id: dom_id } %span.list-item-name - = image_tag group_icon(group), class: "avatar s40", alt: '' + = group_icon(group, class: "avatar s40", alt: '') %strong = link_to group.full_name, group_path(group) .cgray diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml index eff6c80d144..55799e10a46 100644 --- a/app/views/users/_groups.html.haml +++ b/app/views/users/_groups.html.haml @@ -2,4 +2,4 @@ - groups.each do |group| = link_to group, class: 'profile-groups-avatars inline', title: group.name do .avatar-container.s40 - = image_tag group_icon(group), class: 'avatar group-avatar s40' + = group_icon(group, class: 'avatar group-avatar s40') diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 6c3cd6ecefe..cc59f8660fd 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -4,6 +4,9 @@ - page_description @user.bio - header_title @user.name, user_path(@user) - @no_container = true +- content_for :page_specific_javascripts do + = webpack_bundle_tag 'common_d3' + = webpack_bundle_tag 'users' = content_for :meta_tags do = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb new file mode 100644 index 00000000000..0704ebbb0fd --- /dev/null +++ b/app/workers/concerns/project_start_import.rb @@ -0,0 +1,9 @@ +module ProjectStartImport + def start(project) + if project.import_started? && project.import_jid == self.jid + return true + end + + project.import_start + end +end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index cde5b45ad41..264706e3e23 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -4,6 +4,7 @@ class RepositoryForkWorker include Sidekiq::Worker include Gitlab::ShellAdapter include DedicatedSidekiqQueue + include ProjectStartImport sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION @@ -37,7 +38,7 @@ class RepositoryForkWorker private def start_fork(project) - return true if project.import_start + return true if start(project) Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.") false diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 00a021abbdc..d7c0043d3b6 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -4,6 +4,7 @@ class RepositoryImportWorker include Sidekiq::Worker include DedicatedSidekiqQueue include ExceptionBacktrace + include ProjectStartImport sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION @@ -34,7 +35,7 @@ class RepositoryImportWorker private def start_import(project) - return true if project.import_start + return true if start(project) Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.") false diff --git a/changelogs/unreleased/34841-todos.yml b/changelogs/unreleased/34841-todos.yml new file mode 100644 index 00000000000..37180eefbfc --- /dev/null +++ b/changelogs/unreleased/34841-todos.yml @@ -0,0 +1,5 @@ +--- +title: Fix bad type checking to prevent 0 count badge to be shown +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/34897-delete-branch-after-merge.yml b/changelogs/unreleased/34897-delete-branch-after-merge.yml new file mode 100644 index 00000000000..96631aa95c8 --- /dev/null +++ b/changelogs/unreleased/34897-delete-branch-after-merge.yml @@ -0,0 +1,5 @@ +--- +title: Fixed 'Removed source branch' checkbox in merge widget being ignored. +merge_request: 14832 +author: +type: fixed diff --git a/changelogs/unreleased/35580-cannot-import-project-with-milestones.yml b/changelogs/unreleased/35580-cannot-import-project-with-milestones.yml new file mode 100644 index 00000000000..b28105556db --- /dev/null +++ b/changelogs/unreleased/35580-cannot-import-project-with-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Fix the project import with issues and milestones +merge_request: 14657 +author: +type: fixed diff --git a/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml b/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml new file mode 100644 index 00000000000..7e2a7222162 --- /dev/null +++ b/changelogs/unreleased/35652-prometheus-service-page-shows-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix flash errors showing up on a non configured prometheus integration +merge_request: 35652 +author: +type: fixed diff --git a/changelogs/unreleased/36160-select2-dropdown.yml b/changelogs/unreleased/36160-select2-dropdown.yml new file mode 100644 index 00000000000..a836744fb41 --- /dev/null +++ b/changelogs/unreleased/36160-select2-dropdown.yml @@ -0,0 +1,5 @@ +--- +title: Decreases z-index of select2 to a lower number of our navigation bar +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/37660-match-sidebar-colors.yml b/changelogs/unreleased/37660-match-sidebar-colors.yml new file mode 100644 index 00000000000..d5600f453e7 --- /dev/null +++ b/changelogs/unreleased/37660-match-sidebar-colors.yml @@ -0,0 +1,5 @@ +--- +title: Change background color of nav sidebar to match other gl sidebars +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml b/changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml new file mode 100644 index 00000000000..c3c38b35fa7 --- /dev/null +++ b/changelogs/unreleased/37691-subscription-fires-multiple-notifications.yml @@ -0,0 +1,5 @@ +--- +title: Fixed duplicate notifications when added multiple labels on an issue +merge_request: 14798 +author: +type: fixed diff --git a/changelogs/unreleased/38720-sort-admin-runners.yml b/changelogs/unreleased/38720-sort-admin-runners.yml new file mode 100644 index 00000000000..b1047644891 --- /dev/null +++ b/changelogs/unreleased/38720-sort-admin-runners.yml @@ -0,0 +1,5 @@ +--- +title: Add sort runners on admin runners +merge_request: 14661 +author: Takuya Noguchi +type: added diff --git a/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml b/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml new file mode 100644 index 00000000000..5e142a2b4cf --- /dev/null +++ b/changelogs/unreleased/38871-cleanup-data-page-attribute-after-karma-test.yml @@ -0,0 +1,5 @@ +--- +title: Cleanup data-page attribute after each Karma test +merge_request: 14742 +author: +type: fixed diff --git a/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml b/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml new file mode 100644 index 00000000000..d142afa3433 --- /dev/null +++ b/changelogs/unreleased/39033-d3-js-is-being-included-in-the-user_profile-and-graphs_show-bundles.yml @@ -0,0 +1,6 @@ +--- +title: Removed d3.js from the graph and users bundles and used the common_d3 bundle + instead +merge_request: 14826 +author: +type: other diff --git a/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml b/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml new file mode 100644 index 00000000000..19d950b48d6 --- /dev/null +++ b/changelogs/unreleased/an-use-branch-exists-over-branch-names-include.yml @@ -0,0 +1,5 @@ +--- +title: Avoid fetching all branches for branch existence checks +merge_request: 14778 +author: +type: changed diff --git a/changelogs/unreleased/cache-issuable-template-names.yml b/changelogs/unreleased/cache-issuable-template-names.yml new file mode 100644 index 00000000000..858fdff2db2 --- /dev/null +++ b/changelogs/unreleased/cache-issuable-template-names.yml @@ -0,0 +1,5 @@ +--- +title: Cache issue and MR template names in Redis +merge_request: +author: +type: other diff --git a/changelogs/unreleased/issue-36484.yml b/changelogs/unreleased/issue-36484.yml new file mode 100644 index 00000000000..a19126e650f --- /dev/null +++ b/changelogs/unreleased/issue-36484.yml @@ -0,0 +1,5 @@ +--- +title: Remove unnecessary alt-texts from pipeline emails +merge_request: 14602 +author: gernberg +type: fixed diff --git a/changelogs/unreleased/move_markdown_preview_to_concern.yml b/changelogs/unreleased/move_markdown_preview_to_concern.yml new file mode 100644 index 00000000000..036e77610b9 --- /dev/null +++ b/changelogs/unreleased/move_markdown_preview_to_concern.yml @@ -0,0 +1,5 @@ +--- +title: Add support for markdown preview to group milestones +merge_request: 14806 +author: Vitaliy @blackst0ne Klachkov +type: fixed diff --git a/changelogs/unreleased/replace_explore_projects-feature.yml b/changelogs/unreleased/replace_explore_projects-feature.yml new file mode 100644 index 00000000000..85ef045fb4b --- /dev/null +++ b/changelogs/unreleased/replace_explore_projects-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'features/explore/projects.feature' spinach test with an rspec analog +merge_request: 14755 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/config/routes/google_api.rb b/config/routes/google_api.rb index 3fb236d3d51..a119b47c176 100644 --- a/config/routes/google_api.rb +++ b/config/routes/google_api.rb @@ -1,5 +1,7 @@ -namespace :google_api do - resource :auth, only: [], controller: :authorizations do - match :callback, via: [:get, :post] +scope '-' do + namespace :google_api do + resource :auth, only: [], controller: :authorizations do + match :callback, via: [:get, :post] + end end end diff --git a/config/routes/group.rb b/config/routes/group.rb index 23052a6c6dc..8cc30bfcc50 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -1,6 +1,8 @@ require 'constraints/group_url_constrainer' -resources :groups, only: [:index, :new, :create] +resources :groups, only: [:index, :new, :create] do + post :preview_markdown +end scope(path: 'groups/*group_id', module: :groups, diff --git a/config/webpack.config.js b/config/webpack.config.js index 8cded750a66..a71794b379d 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -84,6 +84,7 @@ var config = { vue_merge_request_widget: './vue_merge_request_widget/index.js', test: './test.js', two_factor_auth: './two_factor_auth.js', + users: './users/index.js', performance_bar: './performance_bar.js', webpack_runtime: './webpack.js', }, @@ -215,7 +216,9 @@ var config = { name: 'common_d3', chunks: [ 'graphs', + 'graphs_show', 'monitoring', + 'users', ], }), diff --git a/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb index 915167b038d..8e9ab3f8acc 100644 --- a/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb +++ b/db/migrate/20160518200441_add_artifacts_expire_date_to_ci_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class AddArtifactsExpireDateToCiBuilds < ActiveRecord::Migration def change add_column :ci_builds, :artifacts_expire_at, :timestamp diff --git a/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb index 756910a1fa0..fd7a48d881e 100644 --- a/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb +++ b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime class AddQueuedAtToCiBuilds < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb b/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb index 6ac10723c82..a5d1eca82bb 100644 --- a/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb +++ b/db/migrate/20170503021915_add_last_edited_at_and_last_edited_by_id_to_issues.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb b/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb index 7a1acdcbf69..47ba6bde856 100644 --- a/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb +++ b/db/migrate/20170503022548_add_last_edited_at_and_last_edited_by_id_to_merge_requests.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/Datetime # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/doc/development/README.md b/doc/development/README.md index b648c7ce086..36096842344 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -1,75 +1,93 @@ -# Development +# GitLab development guides -## Outside of docs +## Get started! -- [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) main contributing guide -- [PROCESS.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) contributing process -- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) to install a development version +- Setup GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) +- [GitLab contributing guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) +- [Architecture](architecture.md) of GitLab +- [Rake tasks](rake_tasks.md) for development -## Styleguides +## Processes + +- [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) +- [Generate a changelog entry with `bin/changelog`](changelog.md) +- [Code review guidelines](code_review.md) for reviewing code and having code reviewed. +- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md) + +## UX and frontend guides -- [API styleguide](api_styleguide.md) Use this styleguide if you are - contributing to the API. -- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are - contributing to documentation. -- [Writing documentation](writing_documentation.md) - - [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles) -- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations -- [Testing standards and style guidelines](testing.md) - [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements - [Frontend guidelines](fe_guide/index.md) -- [SQL guidelines](sql.md) for working with SQL queries + +## Backend guides + +- [API styleguide](api_styleguide.md) Use this styleguide if you are + contributing to the API. - [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers +- [Working with Gitaly](gitaly.md) +- [Manage feature flags](feature_flags.md) +- [View sent emails or preview mailers](emails.md) +- [Shell commands](shell_commands.md) in the GitLab codebase - [`Gemfile` guidelines](gemfile.md) +- [Sidekiq debugging](sidekiq_debugging.md) +- [Gotchas](gotchas.md) to avoid +- [Issue and merge requests state models](object_state_models.md) +- [How to dump production data to staging](db_dump.md) -## Process +## Performance guides -- [Generate a changelog entry with `bin/changelog`](changelog.md) -- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md) -- [Code review guidelines](code_review.md) for reviewing code and having code reviewed. +- [Instrumentation](instrumentation.md) +- [Performance guidelines](performance.md) - [Merge request performance guidelines](merge_request_performance_guidelines.md) for ensuring merge requests do not negatively impact GitLab performance -## Backend howtos - -- [Architecture](architecture.md) of GitLab -- [Gotchas](gotchas.md) to avoid -- [How to dump production data to staging](db_dump.md) -- [Instrumentation](instrumentation.md) -- [Performance guidelines](performance.md) -- [Rake tasks](rake_tasks.md) for development -- [Shell commands](shell_commands.md) in the GitLab codebase -- [Sidekiq debugging](sidekiq_debugging.md) -- [Object state models](object_state_models.md) -- [Building a package for testing purposes](build_test_package.md) -- [Manage feature flags](feature_flags.md) -- [View sent emails or preview mailers](emails.md) -- [Working with Gitaly](gitaly.md) +## Databases guides -## Databases +### Migrations -- [Merge Request Checklist](database_merge_request_checklist.md) - [What requires downtime?](what_requires_downtime.md) +- [SQL guidelines](sql.md) for working with SQL queries +- [Migrations style guide](migration_style_guide.md) for creating safe SQL migrations +- [Post deployment migrations](post_deployment_migrations.md) +- [Background migrations](background_migrations.md) +- [Swapping tables](swapping_tables.md) + +### Best practices + +- [Merge Request checklist](database_merge_request_checklist.md) - [Adding database indexes](adding_database_indexes.md) -- [Post Deployment Migrations](post_deployment_migrations.md) -- [Foreign Keys & Associations](foreign_keys.md) -- [Serializing Data](serializing_data.md) -- [Polymorphic Associations](polymorphic_associations.md) -- [Single Table Inheritance](single_table_inheritance.md) -- [Background Migrations](background_migrations.md) -- [Storing SHA1 Hashes As Binary](sha1_as_binary.md) -- [Iterating Tables In Batches](iterating_tables_in_batches.md) -- [Ordering Table Columns](ordering_table_columns.md) -- [Verifying Database Capabilities](verifying_database_capabilities.md) -- [Hash Indexes](hash_indexes.md) -- [Swapping Tables](swapping_tables.md) - -## Internationalization (i18n) +- [Foreign keys & associations](foreign_keys.md) +- [Single table inheritance](single_table_inheritance.md) +- [Polymorphic associations](polymorphic_associations.md) +- [Serializing data](serializing_data.md) +- [Hash indexes](hash_indexes.md) +- [Storing SHA1 hashes as binary](sha1_as_binary.md) +- [Iterating tables in batches](iterating_tables_in_batches.md) +- [Ordering table columns](ordering_table_columns.md) +- [Verifying database capabilities](verifying_database_capabilities.md) + +## Testing guides + +- [Testing standards and style guidelines](testing_guide/index.md) +- [Frontend testing standards and style guidelines](testing_guide/frontend_testing.md) + +## Documentation guides + +- [Documentation styleguide](doc_styleguide.md): Use this styleguide if you are + contributing to the documentation. +- [Writing documentation](writing_documentation.md) + - [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles) + +## Internationalization (i18n) guides - [Introduction](i18n/index.md) - [Externalization](i18n/externalization.md) - [Translation](i18n/translation.md) +## Build guides + +- [Building a package for testing purposes](build_test_package.md) + ## Compliance - [Licensing](licensing.md) for ensuring license compliance diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 031b12a8e91..c0e1bfc12a1 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -54,7 +54,8 @@ or make changes to our frontend development guidelines. --- -## [Testing](testing.md) +## [Testing](../testing_guide/frontend_testing.md) + How we write frontend tests, run the GitLab test suite, and debug test related issues. diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index 77ae6d2a0ea..10f4c5a0902 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -88,16 +88,31 @@ followed by any global declarations, then a blank newline prior to any imports o 1. Use ES module syntax to import modules ```javascript // bad - require('foo'); + const SomeClass = require('some_class'); // good - import Foo from 'foo'; + import SomeClass from 'some_class'; // bad - module.exports = Foo; + module.exports = SomeClass; // good - export default Foo; + export default SomeClass; + ``` + + Import statements are following usual naming guidelines, for example object literals use camel case: + + ```javascript + // some_object file + export default { + key: 'value', + }; + + // bad + import ObjectLiteral from 'some_object'; + + // good + import objectLiteral from 'some_object'; ``` 1. Relative paths: when importing a module in the same directory, a child @@ -285,6 +300,13 @@ A forEach will cause side effects, it will be mutating the array being iterated. 1. **Extensions**: Use `.vue` extension for Vue components. 1. **Reference Naming**: Use camelCase for their instances: ```javascript + // bad + import CardBoard from 'cardBoard' + + components: { + CardBoard: + }; + // good import cardBoard from 'cardBoard' diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md index 867c83f1e72..98e499b8c0f 100644 --- a/doc/development/fe_guide/testing.md +++ b/doc/development/fe_guide/testing.md @@ -1,254 +1 @@ -# Frontend Testing - -There are two types of test suites you'll encounter while developing frontend code -at GitLab. We use Karma and Jasmine for JavaScript unit and integration testing, and RSpec -feature tests with Capybara for e2e (end-to-end) integration testing. - -Unit and feature tests need to be written for all new features. -Most of the time, you should use rspec for your feature tests. -There are cases where the behaviour you are testing is not worth the time spent running the full application, -for example, if you are testing styling, animation, edge cases or small actions that don't involve the backend, -you should write an integration test using Jasmine. - -![Testing priority triangle](img/testing_triangle.png) - -_This diagram demonstrates the relative priority of each test type we use_ - -Regression tests should be written for bug fixes to prevent them from recurring in the future. - -See [the Testing Standards and Style Guidelines](../testing.md) -for more information on general testing practices at GitLab. - -## Karma test suite - -GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test -framework for our JavaScript unit and integration tests. For integration tests, -we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples). -Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`). -Adding these static fixtures should be avoided as they are harder to keep up to date with real views. -The existing static fixtures will be migrated over time. -Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress. -Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin. - -JavaScript tests live in `spec/javascripts/`, matching the folder structure -of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js` -has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file. - -Keep in mind that in a CI environment, these tests are run in a headless -browser and you will not have access to certain APIs, such as -[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), -which will have to be stubbed. - -### Best practice - -#### Naming unit tests - -When writing describe test blocks to test specific functions/methods, -please use the method name as the describe block name. - -```javascript -// Good -describe('methodName', () => { - it('passes', () => { - expect(true).toEqual(true); - }); -}); - -// Bad -describe('#methodName', () => { - it('passes', () => { - expect(true).toEqual(true); - }); -}); - -// Bad -describe('.methodName', () => { - it('passes', () => { - expect(true).toEqual(true); - }); -}); -``` -#### Testing Promises - -When testing Promises you should always make sure that the test is asynchronous and rejections are handled. -Your Promise chain should therefore end with a call of the `done` callback and `done.fail` in case an error occurred. - -```javascript -// Good -it('tests a promise', (done) => { - promise - .then((data) => { - expect(data).toBe(asExpected); - }) - .then(done) - .catch(done.fail); -}); - -// Good -it('tests a promise rejection', (done) => { - promise - .then(done.fail) - .catch((error) => { - expect(error).toBe(expectedError); - }) - .then(done) - .catch(done.fail); -}); - -// Bad (missing done callback) -it('tests a promise', () => { - promise - .then((data) => { - expect(data).toBe(asExpected); - }) -}); - -// Bad (missing catch) -it('tests a promise', (done) => { - promise - .then((data) => { - expect(data).toBe(asExpected); - }) - .then(done) -}); - -// Bad (use done.fail in asynchronous tests) -it('tests a promise', (done) => { - promise - .then((data) => { - expect(data).toBe(asExpected); - }) - .then(done) - .catch(fail) -}); - -// Bad (missing catch) -it('tests a promise rejection', (done) => { - promise - .catch((error) => { - expect(error).toBe(expectedError); - }) - .then(done) -}); -``` - -#### Stubbing - -For unit tests, you should stub methods that are unrelated to the current unit you are testing. -If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely. - -For integration tests, you should stub methods that will effect the stability of the test if they -execute their original behaviour. i.e. Network requests. - -### Vue.js unit tests -See this [section][vue-test]. - -### Running frontend tests - -`rake karma` runs the frontend-only (JavaScript) tests. -It consists of two subtasks: - -- `rake karma:fixtures` (re-)generates fixtures -- `rake karma:tests` actually executes the tests - -As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`) -is sufficient (and saves you some time). - -### Live testing and focused testing - -While developing locally, it may be helpful to keep karma running so that you -can get instant feedback on as you write tests and modify code. To do this -you can start karma with `npm run karma-start`. It will compile the javascript -assets and run a server at `http://localhost:9876/` where it will automatically -run the tests on any browser which connects to it. You can enter that url on -multiple browsers at once to have it run the tests on each in parallel. - -While karma is running, any changes you make will instantly trigger a recompile -and retest of the entire test suite, so you can see instantly if you've broken -a test with your changes. You can use [jasmine focused][jasmine-focus] or -excluded tests (with `fdescribe` or `xdescribe`) to get karma to run only the -tests you want while you're working on a specific feature, but make sure to -remove these directives when you commit your code. - -## RSpec Feature Integration Tests - -Information on setting up and running RSpec integration tests with -[Capybara][capybara] can be found in the -[general testing guide](../testing.md). - -## Gotchas - -### Errors due to use of unsupported JavaScript features - -Similar errors will be thrown if you're using JavaScript features not yet -supported by the PhantomJS test runner which is used for both Karma and RSpec -tests. We polyfill some JavaScript objects for older browsers, but some -features are still unavailable: - -- Array.from -- Array.first -- Async functions -- Generators -- Array destructuring -- For..Of -- Symbol/Symbol.iterator -- Spread - -Until these are polyfilled appropriately, they should not be used. Please -update this list with additional unsupported features. - -### RSpec errors due to JavaScript - -By default RSpec unit tests will not run JavaScript in the headless browser -and will simply rely on inspecting the HTML generated by rails. - -If an integration test depends on JavaScript to run correctly, you need to make -sure the spec is configured to enable JavaScript when the tests are run. If you -don't do this you'll see vague error messages from the spec runner. - -To enable a JavaScript driver in an `rspec` test, add `:js` to the -individual spec or the context block containing multiple specs that need -JavaScript enabled: - -```ruby -# For one spec -it 'presents information about abuse report', :js do - # assertions... -end - -describe "Admin::AbuseReports", :js do - it 'presents information about abuse report' do - # assertions... - end - it 'shows buttons for adding to abuse report' do - # assertions... - end -end -``` - -### Spinach errors due to missing JavaScript - -> **Note:** Since we are discouraging the use of Spinach when writing new -> feature tests, you shouldn't ever need to use this. This information is kept -> available for legacy purposes only. - -In Spinach, the JavaScript driver is enabled differently. In the `*.feature` -file for the failing spec, add the `@javascript` flag above the Scenario: - -``` -@javascript -Scenario: Developer can approve merge request - Given I am a "Shop" developer - And I visit project "Shop" merge requests page - And merge request 'Bug NS-04' must be approved - And I click link "Bug NS-04" - When I click link "Approve" - Then I should see approved merge request "Bug NS-04" -``` - -[capybara]: http://teamcapybara.github.io/capybara/ -[jasmine]: https://jasmine.github.io/ -[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html -[jasmine-jquery]: https://github.com/velesin/jasmine-jquery -[karma]: http://karma-runner.github.io/ -[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components +This document was moved to [../testing_guide/frontend_testing.md](../testing_guide/frontend_testing.md). diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md index 79707aaf671..b34ec754742 100644 --- a/doc/development/i18n/translation.md +++ b/doc/development/i18n/translation.md @@ -58,6 +58,18 @@ For example, in French we translate `you` as the informal `tu`. You can refer to other translated strings and notes in the glossary to assist determining a suitable level of formality. +### Inclusive language + +[Diversity] is one of GitLab's values. +We ask you to avoid translations which exclude people based on their gender or ethnicity. +In languages which distinguish between a male and female form, +use both or choose a neutral formulation. + +For example in German, the word "user" can be translated into "Benutzer" (male) or "Benutzerin" (female). +Therefore "create a new user" would translate into "Benutzer(in) anlegen". + +[Diversity]: https://about.gitlab.com/handbook/values/#diversity + ### Updating the glossary To propose additions to the glossary please diff --git a/doc/development/testing.md b/doc/development/testing.md index 4d5b90de6fc..45b1519ece8 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -1,566 +1 @@ -# Testing Standards and Style Guidelines - -This guide outlines standards and best practices for automated testing of GitLab -CE and EE. - -It is meant to be an _extension_ of the [thoughtbot testing -styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If -this guide defines a rule that contradicts the thoughtbot guide, this guide -takes precedence. Some guidelines may be repeated verbatim to stress their -importance. - -## Definitions - -### Unit tests - -Formal definition: https://en.wikipedia.org/wiki/Unit_testing - -These kind of tests ensure that a single unit of code (a method) works as -expected (given an input, it has a predictable output). These tests should be -isolated as much as possible. For example, model methods that don't do anything -with the database shouldn't need a DB record. Classes that don't need database -records should use stubs/doubles as much as possible. - -| Code path | Tests path | Testing engine | Notes | -| --------- | ---------- | -------------- | ----- | -| `app/finders/` | `spec/finders/` | RSpec | | -| `app/helpers/` | `spec/helpers/` | RSpec | | -| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). | -| `app/policies/` | `spec/policies/` | RSpec | | -| `app/presenters/` | `spec/presenters/` | RSpec | | -| `app/routing/` | `spec/routing/` | RSpec | | -| `app/serializers/` | `spec/serializers/` | RSpec | | -| `app/services/` | `spec/services/` | RSpec | | -| `app/tasks/` | `spec/tasks/` | RSpec | | -| `app/uploaders/` | `spec/uploaders/` | RSpec | | -| `app/views/` | `spec/views/` | RSpec | | -| `app/workers/` | `spec/workers/` | RSpec | | -| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. | - -### Integration tests - -Formal definition: https://en.wikipedia.org/wiki/Integration_testing - -These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). These tests should assert at the request/response level: status code, headers, body. They're useful to test permissions, redirections, what view is rendered etc. - -| Code path | Tests path | Testing engine | Notes | -| --------- | ---------- | -------------- | ----- | -| `app/controllers/` | `spec/controllers/` | RSpec | | -| `app/mailers/` | `spec/mailers/` | RSpec | | -| `lib/api/` | `spec/requests/api/` | RSpec | | -| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | | -| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. | - -#### About controller tests - -In an ideal world, controllers should be thin. However, when this is not the -case, it's acceptable to write a system/feature test without JavaScript instead -of a controller test. The reason is that testing a fat controller usually -involves a lot of stubbing, things like: - -```ruby -controller.instance_variable_set(:@user, user) -``` - -and use methods which are deprecated in Rails 5 ([#23768]). - -[#23768]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23768 - -#### About Karma - -As you may have noticed, Karma is both in the Unit tests and the Integration -tests category. That's because Karma is a tool that provides an environment to -run JavaScript tests, so you can either run unit tests (e.g. test a single -JavaScript method), or integration tests (e.g. test a component that is composed -of multiple components). - -### System tests or Feature tests - -Formal definition: https://en.wikipedia.org/wiki/System_testing. - -These kind of tests ensure the application works as expected from a user point -of view (aka black-box testing). These tests should test a happy path for a -given page or set of pages, and a test case should be added for any regression -that couldn't have been caught at lower levels with better tests (i.e. if a -regression is found, regression tests should be added at the lowest-level -possible). - -| Tests path | Testing engine | Notes | -| ---------- | -------------- | ----- | -| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. | -| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). | - -[Capybara]: https://github.com/teamcapybara/capybara -[RSpec]: https://github.com/rspec/rspec-rails#feature-specs -[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist -[RackTest]: https://github.com/teamcapybara/capybara#racktest - -#### Best practices - -- Create only the necessary records in the database -- Test a happy path and a less happy path but that's it -- Every other possible path should be tested with Unit or Integration tests -- Test what's displayed on the page, not the internals of ActiveRecord models. - For instance, if you want to verify that a record was created, add - expectations that its attributes are displayed on the page, not that - `Model.count` increased by one. -- It's ok to look for DOM elements but don't abuse it since it makes the tests - more brittle - -If we're confident that the low-level components work well (and we should be if -we have enough Unit & Integration tests), we shouldn't need to duplicate their -thorough testing at the System test level. - -It's very easy to add tests, but a lot harder to remove or improve tests, so one -should take care of not introducing too many (slow and duplicated) specs. - -The reasons why we should follow these best practices are as follows: - -- System tests are slow to run since they spin up the entire application stack - in a headless browser, and even slower when they integrate a JS driver -- When system tests run with a JavaScript driver, the tests are run in a - different thread than the application. This means it does not share a - database connection and your test will have to commit the transactions in - order for the running application to see the data (and vice-versa). In that - case we need to truncate the database after each spec instead of simply - rolling back a transaction (the faster strategy that's in use for other kind - of tests). This is slower than transactions, however, so we want to use - truncation only when necessary. - -### Black-box tests or End-to-end tests - -GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse], -[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces -are configured and packaged by [GitLab Omnibus]. - -[GitLab QA] is a tool that allows to test that all these pieces integrate well -together by building a Docker image for a given version of GitLab Rails and -running feature tests (i.e. using Capybara) against it. - -The actual test scenarios and steps are [part of GitLab Rails] so that they're -always in-sync with the codebase. - -[multiple pieces]: ./architecture.md#components -[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell -[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse -[Gitaly]: https://gitlab.com/gitlab-org/gitaly -[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages -[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner -[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab -[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa -[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa - -## Test for what should not be there - -This is particularly important for permission calls and might be called a -negative assertion: make sure only the bare minimum is returned and nothing else. - -See an issue about [leaking tokens] as an example of a vulnerability that is -captured by such a test. - -[leaking tokens]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37948 - -## How to test at the correct level? - -As many things in life, deciding what to test at each level of testing is a -trade-off: - -- Unit tests are usually cheap, and you should consider them like the basement - of your house: you need them to be confident that your code is behaving - correctly. However if you run only unit tests without integration / system - tests, you might [miss] the [big] [picture]! -- Integration tests are a bit more expensive, but don't abuse them. A system test - is often better than an integration test that is stubbing a lot of internals. -- System tests are expensive (compared to unit tests), even more if they require - a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed) - section. - -Another way to see it is to think about the "cost of tests", this is well -explained [in this article][tests-cost] and the basic idea is that the cost of a -test includes: - -- The time it takes to write the test -- The time it takes to run the test every time the suite runs -- The time it takes to understand the test -- The time it takes to fix the test if it breaks and the underlying code is OK -- Maybe, the time it takes to change the code to make the code testable. - -[miss]: https://twitter.com/ThePracticalDev/status/850748070698651649 -[big]: https://twitter.com/timbray/status/822470746773409794 -[picture]: https://twitter.com/withzombies/status/829716565834752000 -[tests-cost]: https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e - -## Frontend testing - -Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md). - -## RSpec - -### General Guidelines - -- Use a single, top-level `describe ClassName` block. -- Use `.method` to describe class methods and `#method` to describe instance - methods. -- Use `context` to test branching logic. -- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). -- Try to match the ordering of tests to the ordering within the class. -- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines - to separate phases. -- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'` -- Don't assert against the absolute value of a sequence-generated attribute (see - [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). -- Don't supply the `:each` argument to hooks since it's the default. -- On `before` and `after` hooks, prefer it scoped to `:context` over `:all` - -[four-phase-test]: https://robots.thoughtbot.com/four-phase-test - -### Automatic retries and flaky tests detection - -On our CI, we use [rspec-retry] to automatically retry a failing example a few -times (see [`spec/spec_helper.rb`] for the precise retries count). - -We also use a home-made `RspecFlaky::Listener` listener which records flaky -examples in a JSON report file on `master` (`retrieve-tests-metadata` and `update-tests-metadata` jobs), and warns when a new flaky example -is detected in any other branch (`flaky-examples-check` job). In the future, the -`flaky-examples-check` job will not be allowed to fail. - -[rspec-retry]: https://github.com/NoRedInk/rspec-retry -[`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb - -### `let` variables - -GitLab's RSpec suite has made extensive use of `let` variables to reduce -duplication. However, this sometimes [comes at the cost of clarity][lets-not], -so we need to set some guidelines for their use going forward: - -- `let` variables are preferable to instance variables. Local variables are - preferable to `let` variables. -- Use `let` to reduce duplication throughout an entire spec file. -- Don't use `let` to define variables used by a single test; define them as - local variables inside the test's `it` block. -- Don't define a `let` variable inside the top-level `describe` block that's - only used in a more deeply-nested `context` or `describe` block. Keep the - definition as close as possible to where it's used. -- Try to avoid overriding the definition of one `let` variable with another. -- Don't define a `let` variable that's only used by the definition of another. - Use a helper method instead. - -[lets-not]: https://robots.thoughtbot.com/lets-not - -#### `set` variables - -In some cases there is no need to recreate the same object for tests again for -each example. For example, a project is needed to test issues on the same -project, one project will do for the entire file. This can be achieved by using -`set` in the same way you would use `let`. - -`rspec-set` only works on ActiveRecord objects, and before new examples it -reloads or recreates the model, _only_ if needed. That is, when you changed -properties or destroyed the object. - -There is one gotcha; you can't reference a model defined in a `let` block in a -`set` block. - -### Time-sensitive tests - -[Timecop](https://github.com/travisjeffery/timecop) is available in our -Ruby-based tests for verifying things that are time-sensitive. Any test that -exercises or verifies something time-sensitive should make use of Timecop to -prevent transient test failures. - -Example: - -```ruby -it 'is overdue' do - issue = build(:issue, due_date: Date.tomorrow) - - Timecop.freeze(3.days.from_now) do - expect(issue).to be_overdue - end -end -``` - -### System / Feature tests - -- Feature specs should be named `ROLE_ACTION_spec.rb`, such as - `user_changes_password_spec.rb`. -- Use only one `feature` block per feature spec file. -- Use scenario titles that describe the success and failure paths. -- Avoid scenario titles that add no information, such as "successfully". -- Avoid scenario titles that repeat the feature title. - -### Table-based / Parameterized tests - -This style of testing is used to exercise one piece of code with a comprehensive -range of inputs. By specifying the test case once, alongside a table of inputs -and the expected output for each, your tests can be made easier to read and more -compact. - -We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized) -gem. A short example, using the table syntax and checking Ruby equality for a -range of inputs, might look like this: - -```ruby -describe "#==" do - using RSpec::Parameterized::TableSyntax - - let(:project1) { create(:project) } - let(:project2) { create(:project) } - where(:a, :b, :result) do - 1 | 1 | true - 1 | 2 | false - true | true | true - true | false | false - project1 | project1 | true - project2 | project2 | true - project 1 | project2 | false - end - - with_them do - it { expect(a == b).to eq(result) } - - it 'is isomorphic' do - expect(b == a).to eq(result) - end - end -end -``` - -### Matchers - -Custom matchers should be created to clarify the intent and/or hide the -complexity of RSpec expectations.They should be placed under -`spec/support/matchers/`. Matchers can be placed in subfolder if they apply to -a certain type of specs only (e.g. features, requests etc.) but shouldn't be if -they apply to multiple type of specs. - -#### have_gitlab_http_status - -Prefer `have_gitlab_http_status` over `have_http_status` because the former -could also show the response body whenever the status mismatched. This would -be very useful whenever some tests start breaking and we would love to know -why without editing the source and rerun the tests. - -This is especially useful whenever it's showing 500 internal server error. - -### Shared contexts - -All shared contexts should be be placed under `spec/support/shared_contexts/`. -Shared contexts can be placed in subfolder if they apply to a certain type of -specs only (e.g. features, requests etc.) but shouldn't be if they apply to -multiple type of specs. - -Each file should include only one context and have a descriptive name, e.g. -`spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb`. - -### Shared examples - -All shared examples should be be placed under `spec/support/shared_examples/`. -Shared examples can be placed in subfolder if they apply to a certain type of -specs only (e.g. features, requests etc.) but shouldn't be if they apply to -multiple type of specs. - -Each file should include only one context and have a descriptive name, e.g. -`spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb`. - -### Helpers - -Helpers are usually modules that provide some methods to hide the complexity of -specific RSpec examples. You can define helpers in RSpec files if they're not -intended to be shared with other specs. Otherwise, they should be be placed -under `spec/support/helpers/`. Helpers can be placed in subfolder if they apply -to a certain type of specs only (e.g. features, requests etc.) but shouldn't be -if they apply to multiple type of specs. - -Helpers should follow the Rails naming / namespacing convention. For instance -`spec/support/helpers/cycle_analytics_helpers.rb` should define: - -```ruby -module Spec - module Support - module Helpers - module CycleAnalyticsHelpers - def create_commit_referencing_issue(issue, branch_name: random_git_name) - project.repository.add_branch(user, branch_name, 'master') - create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name) - end - end - end - end -end -``` - -Helpers should not change the RSpec config. For instance, the helpers module -described above should not include: - -```ruby -RSpec.configure do |config| - config.include Spec::Support::Helpers::CycleAnalyticsHelpers -end -``` - -### Factories - -GitLab uses [factory_girl] as a test fixture replacement. - -- Factory definitions live in `spec/factories/`, named using the pluralization - of their corresponding model (`User` factories are defined in `users.rb`). -- There should be only one top-level factory definition per file. -- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and - should) call `create(...)` instead of `FactoryGirl.create(...)`. -- Make use of [traits] to clean up definitions and usages. -- When defining a factory, don't define attributes that are not required for the - resulting record to pass validation. -- When instantiating from a factory, don't supply attributes that aren't - required by the test. -- Factories don't have to be limited to `ActiveRecord` objects. - [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d). - -[factory_girl]: https://github.com/thoughtbot/factory_girl -[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits - -### Fixtures - -All fixtures should be be placed under `spec/fixtures/`. - -### Config - -RSpec config files are files that change the RSpec config (i.e. -`RSpec.configure do |config|` blocks). They should be placed under -`spec/support/config/`. - -Each file should be related to a specific domain, e.g. -`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc. - -Helpers can be included in the `spec/support/config/rspec.rb` file. If a -helpers module applies only to a certain kind of specs, it should add modifiers -to the `config.include` call. For instance if -`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and -`type: :model` specs only, you would write the following: - -```ruby -RSpec.configure do |config| - config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib - config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model -end -``` - -## Testing Rake Tasks - -To make testing Rake tasks a little easier, there is a helper that can be included -in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use -`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures -a few other things to make testing Rake tasks easier. - -At a minimum, requiring the Rake helper will redirect `stdout`, include the -runtime task helpers, and include the `RakeHelpers` Spec support module. - -The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make -executing tasks simple. See `spec/support/rake_helpers.rb` for all available -methods. - -Example: - -```ruby -require 'rake_helper' - -describe 'gitlab:shell rake tasks' do - before do - Rake.application.rake_require 'tasks/gitlab/shell' - - stub_warn_user_is_not_gitlab - end - - describe 'install task' do - it 'invokes create_hooks task' do - expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke) - - run_rake_task('gitlab:shell:install') - end - end -end -``` - -## Test speed - -GitLab has a massive test suite that, without [parallelization], can take hours -to run. It's important that we make an effort to write tests that are accurate -and effective _as well as_ fast. - -Here are some things to keep in mind regarding test performance: - -- `double` and `spy` are faster than `FactoryGirl.build(...)` -- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`. -- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`, - `spy`, or `double` will do. Database persistence is slow! -- Don't mark a feature as requiring JavaScript (through `@javascript` in - Spinach or `:js` in RSpec) unless it's _actually_ required for the test - to be valid. Headless browser testing is slow! - -[parallelization]: #test-suite-parallelization-on-the-ci - -### Test suite parallelization on the CI - -Our current CI parallelization setup is as follows: - -1. The `retrieve-tests-metadata` job in the `prepare` stage ensures that we have - a `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file: - - The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched - from S3, if it's not here we initialize the file with `{}`. -1. Each `rspec-pg x y`/`rspec-mysql x y` job is run with `knapsack rspec` and - should have an evenly distributed share of tests: - - It works because the jobs have access to the - `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts - from all previous stages are passed by default". [^1] - - The jobs set their own report path to - `KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`. - - If knapsack is doing its job, test files that are run should be listed under - `Report specs`, not under `Leftover specs`. -1. The `update-tests-metadata` job takes all the - `knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json` - files from the `rspec-pg x y`/`rspec-mysql x y`jobs and merge them all together - into a single `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that - is then uploaded to S3. - -After that, the next pipeline will use the up-to-date -`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy -is used for Spinach tests as well. - -### Monitoring - -The GitLab test suite is [monitored] for the `master` branch, and any branch -that includes `rspec-profile` in their name. - -A [public dashboard] is available for everyone to see. Feel free to look at the -slowest test files and try to improve them. - -[monitored]: ./performance.md#rspec-profiling -[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default - -## CI setup - -- On CE and EE, the test suite runs both PostgreSQL and MySQL. -- Rails logging to `log/test.log` is disabled by default in CI [for - performance reasons][logging]. To override this setting, provide the - `RAILS_ENABLE_TEST_LOG` environment variable. - -[logging]: https://jtway.co/speed-up-your-rails-test-suite-by-6-in-1-line-13fedb869ec4 - -## Spinach (feature) tests - -GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) -for its feature/integration tests in September 2012. - -As of March 2016, we are [trying to avoid adding new Spinach -tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward, -opting for [RSpec feature](#features-integration) specs. - -Adding new Spinach scenarios is acceptable _only if_ the new scenario requires -no more than one new `step` definition. If more than that is required, the -test should be re-implemented using RSpec instead. - ---- - -[Return to Development documentation](README.md) - -[^1]: /ci/yaml/README.html#dependencies +This document was moved to [testing_guide/index.md](testing_guide/index.md). diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md new file mode 100644 index 00000000000..613423dbd9a --- /dev/null +++ b/doc/development/testing_guide/best_practices.md @@ -0,0 +1,272 @@ +# Testing best practices + +## Test speed + +GitLab has a massive test suite that, without [parallelization], can take hours +to run. It's important that we make an effort to write tests that are accurate +and effective _as well as_ fast. + +Here are some things to keep in mind regarding test performance: + +- `double` and `spy` are faster than `FactoryGirl.build(...)` +- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`. +- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`, + `spy`, or `double` will do. Database persistence is slow! +- Don't mark a feature as requiring JavaScript (through `@javascript` in + Spinach or `:js` in RSpec) unless it's _actually_ required for the test + to be valid. Headless browser testing is slow! + +[parallelization]: ci.md#test-suite-parallelization-on-the-ci + +## RSpec + +### General guidelines + +- Use a single, top-level `describe ClassName` block. +- Use `.method` to describe class methods and `#method` to describe instance + methods. +- Use `context` to test branching logic. +- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](../gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). +- Try to match the ordering of tests to the ordering within the class. +- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines + to separate phases. +- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'` +- Don't assert against the absolute value of a sequence-generated attribute (see + [Gotchas](../gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)). +- Don't supply the `:each` argument to hooks since it's the default. +- On `before` and `after` hooks, prefer it scoped to `:context` over `:all` + +[four-phase-test]: https://robots.thoughtbot.com/four-phase-test + +### System / Feature tests + +NOTE: **Note:** Before writing a new system test, [please consider **not** +writing one](testing_levels.md#consider-not-writing-a-system-test)! + +- Feature specs should be named `ROLE_ACTION_spec.rb`, such as + `user_changes_password_spec.rb`. +- Use scenario titles that describe the success and failure paths. +- Avoid scenario titles that add no information, such as "successfully". +- Avoid scenario titles that repeat the feature title. +- Create only the necessary records in the database +- Test a happy path and a less happy path but that's it +- Every other possible path should be tested with Unit or Integration tests +- Test what's displayed on the page, not the internals of ActiveRecord models. + For instance, if you want to verify that a record was created, add + expectations that its attributes are displayed on the page, not that + `Model.count` increased by one. +- It's ok to look for DOM elements but don't abuse it since it makes the tests + more brittle + +### `let` variables + +GitLab's RSpec suite has made extensive use of `let` variables to reduce +duplication. However, this sometimes [comes at the cost of clarity][lets-not], +so we need to set some guidelines for their use going forward: + +- `let` variables are preferable to instance variables. Local variables are + preferable to `let` variables. +- Use `let` to reduce duplication throughout an entire spec file. +- Don't use `let` to define variables used by a single test; define them as + local variables inside the test's `it` block. +- Don't define a `let` variable inside the top-level `describe` block that's + only used in a more deeply-nested `context` or `describe` block. Keep the + definition as close as possible to where it's used. +- Try to avoid overriding the definition of one `let` variable with another. +- Don't define a `let` variable that's only used by the definition of another. + Use a helper method instead. + +[lets-not]: https://robots.thoughtbot.com/lets-not + +### `set` variables + +In some cases there is no need to recreate the same object for tests again for +each example. For example, a project is needed to test issues on the same +project, one project will do for the entire file. This can be achieved by using +`set` in the same way you would use `let`. + +`rspec-set` only works on ActiveRecord objects, and before new examples it +reloads or recreates the model, _only_ if needed. That is, when you changed +properties or destroyed the object. + +There is one gotcha; you can't reference a model defined in a `let` block in a +`set` block. + +### Time-sensitive tests + +[Timecop](https://github.com/travisjeffery/timecop) is available in our +Ruby-based tests for verifying things that are time-sensitive. Any test that +exercises or verifies something time-sensitive should make use of Timecop to +prevent transient test failures. + +Example: + +```ruby +it 'is overdue' do + issue = build(:issue, due_date: Date.tomorrow) + + Timecop.freeze(3.days.from_now) do + expect(issue).to be_overdue + end +end +``` + +### Table-based / Parameterized tests + +This style of testing is used to exercise one piece of code with a comprehensive +range of inputs. By specifying the test case once, alongside a table of inputs +and the expected output for each, your tests can be made easier to read and more +compact. + +We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized) +gem. A short example, using the table syntax and checking Ruby equality for a +range of inputs, might look like this: + +```ruby +describe "#==" do + using RSpec::Parameterized::TableSyntax + + let(:project1) { create(:project) } + let(:project2) { create(:project) } + where(:a, :b, :result) do + 1 | 1 | true + 1 | 2 | false + true | true | true + true | false | false + project1 | project1 | true + project2 | project2 | true + project 1 | project2 | false + end + + with_them do + it { expect(a == b).to eq(result) } + + it 'is isomorphic' do + expect(b == a).to eq(result) + end + end +end +``` + +### Matchers + +Custom matchers should be created to clarify the intent and/or hide the +complexity of RSpec expectations.They should be placed under +`spec/support/matchers/`. Matchers can be placed in subfolder if they apply to +a certain type of specs only (e.g. features, requests etc.) but shouldn't be if +they apply to multiple type of specs. + +#### `have_gitlab_http_status` + +Prefer `have_gitlab_http_status` over `have_http_status` because the former +could also show the response body whenever the status mismatched. This would +be very useful whenever some tests start breaking and we would love to know +why without editing the source and rerun the tests. + +This is especially useful whenever it's showing 500 internal server error. + +### Shared contexts + +All shared contexts should be be placed under `spec/support/shared_contexts/`. +Shared contexts can be placed in subfolder if they apply to a certain type of +specs only (e.g. features, requests etc.) but shouldn't be if they apply to +multiple type of specs. + +Each file should include only one context and have a descriptive name, e.g. +`spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb`. + +### Shared examples + +All shared examples should be be placed under `spec/support/shared_examples/`. +Shared examples can be placed in subfolder if they apply to a certain type of +specs only (e.g. features, requests etc.) but shouldn't be if they apply to +multiple type of specs. + +Each file should include only one context and have a descriptive name, e.g. +`spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb`. + +### Helpers + +Helpers are usually modules that provide some methods to hide the complexity of +specific RSpec examples. You can define helpers in RSpec files if they're not +intended to be shared with other specs. Otherwise, they should be be placed +under `spec/support/helpers/`. Helpers can be placed in subfolder if they apply +to a certain type of specs only (e.g. features, requests etc.) but shouldn't be +if they apply to multiple type of specs. + +Helpers should follow the Rails naming / namespacing convention. For instance +`spec/support/helpers/cycle_analytics_helpers.rb` should define: + +```ruby +module Spec + module Support + module Helpers + module CycleAnalyticsHelpers + def create_commit_referencing_issue(issue, branch_name: random_git_name) + project.repository.add_branch(user, branch_name, 'master') + create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name) + end + end + end + end +end +``` + +Helpers should not change the RSpec config. For instance, the helpers module +described above should not include: + +```ruby +RSpec.configure do |config| + config.include Spec::Support::Helpers::CycleAnalyticsHelpers +end +``` + +### Factories + +GitLab uses [factory_girl] as a test fixture replacement. + +- Factory definitions live in `spec/factories/`, named using the pluralization + of their corresponding model (`User` factories are defined in `users.rb`). +- There should be only one top-level factory definition per file. +- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and + should) call `create(...)` instead of `FactoryGirl.create(...)`. +- Make use of [traits] to clean up definitions and usages. +- When defining a factory, don't define attributes that are not required for the + resulting record to pass validation. +- When instantiating from a factory, don't supply attributes that aren't + required by the test. +- Factories don't have to be limited to `ActiveRecord` objects. + [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d). + +[factory_girl]: https://github.com/thoughtbot/factory_girl +[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits + +### Fixtures + +All fixtures should be be placed under `spec/fixtures/`. + +### Config + +RSpec config files are files that change the RSpec config (i.e. +`RSpec.configure do |config|` blocks). They should be placed under +`spec/support/config/`. + +Each file should be related to a specific domain, e.g. +`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc. + +Helpers can be included in the `spec/support/config/rspec.rb` file. If a +helpers module applies only to a certain kind of specs, it should add modifiers +to the `config.include` call. For instance if +`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and +`type: :model` specs only, you would write the following: + +```ruby +RSpec.configure do |config| + config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib + config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model +end +``` + +--- + +[Return to Testing documentation](index.md) diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md new file mode 100644 index 00000000000..e90de55068d --- /dev/null +++ b/doc/development/testing_guide/ci.md @@ -0,0 +1,52 @@ +# GitLab tests in the Continuous Integration (CI) context + +### Test suite parallelization on the CI + +Our current CI parallelization setup is as follows: + +1. The `knapsack` job in the prepare stage that is supposed to ensure we have a + `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file: + - The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched + from S3, if it's not here we initialize the file with `{}`. +1. Each `rspec x y` job are run with `knapsack rspec` and should have an evenly + distributed share of tests: + - It works because the jobs have access to the + `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts + from all previous stages are passed by default". [^1] + - the jobs set their own report path to + `KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`. + - if knapsack is doing its job, test files that are run should be listed under + `Report specs`, not under `Leftover specs`. +1. The `update-knapsack` job takes all the + `knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json` + files from the `rspec x y` jobs and merge them all together into a single + `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that is then + uploaded to S3. + +After that, the next pipeline will use the up-to-date +`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy +is used for Spinach tests as well. + +### Monitoring + +The GitLab test suite is [monitored] for the `master` branch, and any branch +that includes `rspec-profile` in their name. + +A [public dashboard] is available for everyone to see. Feel free to look at the +slowest test files and try to improve them. + +[monitored]: ../performance.md#rspec-profiling +[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default + +## CI setup + +- On CE and EE, the test suite runs both PostgreSQL and MySQL. +- Rails logging to `log/test.log` is disabled by default in CI [for + performance reasons][logging]. To override this setting, provide the + `RAILS_ENABLE_TEST_LOG` environment variable. + +[logging]: https://jtway.co/speed-up-your-rails-test-suite-by-6-in-1-line-13fedb869ec4 + +--- + +[Return to Testing documentation](index.md) diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md new file mode 100644 index 00000000000..bbb2313ea7b --- /dev/null +++ b/doc/development/testing_guide/flaky_tests.md @@ -0,0 +1,74 @@ +# Flaky tests + +## What's a flaky test? + +It's a test that sometimes fails, but if you retry it enough times, it passes, +eventually. + +## Automatic retries and flaky tests detection + +On our CI, we use [rspec-retry] to automatically retry a failing example a few +times (see [`spec/spec_helper.rb`] for the precise retries count). + +We also use a home-made `RspecFlaky::Listener` listener which records flaky +examples in a JSON report file on `master` (`retrieve-tests-metadata` and `update-tests-metadata` jobs), and warns when a new flaky example +is detected in any other branch (`flaky-examples-check` job). In the future, the +`flaky-examples-check` job will not be allowed to fail. + +This was originally implemented in: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13021. + +[rspec-retry]: https://github.com/NoRedInk/rspec-retry +[`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb + +## Problems we had in the past at GitLab + +- [`rspec-retry` is bitting us when some API specs fail](https://gitlab.com/gitlab-org/gitlab-ce/issues/29242): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9825 +- [Sporadic RSpec failures due to `PG::UniqueViolation`](https://gitlab.com/gitlab-org/gitlab-ce/issues/28307#note_24958837): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9846 + - Follow-up: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10688 + - [Capybara.reset_session! should be called before requests are blocked](https://gitlab.com/gitlab-org/gitlab-ce/issues/33779): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12224 +- FFaker generates funky data that tests are not ready to handle (and tests should be predictable so that's bad!): + - [Make `spec/mailers/notify_spec.rb` more robust](https://gitlab.com/gitlab-org/gitlab-ce/issues/20121): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10015 + - [Transient failure in spec/requests/api/commits_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/27988#note_25342521): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9944 + - [Replace FFaker factory data with sequences](https://gitlab.com/gitlab-org/gitlab-ce/issues/29643): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10184 + - [Transient failure in spec/finders/issues_finder_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/30211#note_26707685): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10404 + +### Time-sensitive flaky tests + +- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10046 +- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10306 + +### Array order expectation + +- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10148 + +### Feature tests + +- [Be sure to create all the data the test need before starting exercize](https://gitlab.com/gitlab-org/gitlab-ce/issues/32622#note_31128195): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12059 +- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34609#note_34048715): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12604 +- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34698#note_34276286): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12664 +- [Assert against the underlying database state instead of against a page's content](https://gitlab.com/gitlab-org/gitlab-ce/issues/31437): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10934 + +#### Capybara viewport size related issues + +- [Transient failure of spec/features/issues/filtered_search/filter_issues_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/29241#note_26743936): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10411 + +#### Capybara JS driver related issues + +- [Don't wait for AJAX when no AJAX request is fired](https://gitlab.com/gitlab-org/gitlab-ce/issues/30461): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10454 +- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34647): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12626 + +#### PhantomJS / WebKit related issues + +- Memory is through the roof! (TL;DR: Load images but block images requests!): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12003 + +## Resources + +- [Flaky Tests: Are You Sure You Want to Rerun Them?](http://semaphoreci.com/blog/2017/04/20/flaky-tests.html) +- [How to Deal With and Eliminate Flaky Tests](https://semaphoreci.com/community/tutorials/how-to-deal-with-and-eliminate-flaky-tests) +- [Tips on Treating Flakiness in your Rails Test Suite](http://semaphoreci.com/blog/2017/08/03/tips-on-treating-flakiness-in-your-test-suite.html) +- ['Flaky' tests: a short story](https://www.ombulabs.com/blog/rspec/continuous-integration/how-to-track-down-a-flaky-test.html) +- [Using Insights to Discover Flaky, Slow, and Failed Tests](https://circleci.com/blog/using-insights-to-discover-flaky-slow-and-failed-tests/) + +--- + +[Return to Testing documentation](index.md) diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md new file mode 100644 index 00000000000..0c63f51cb45 --- /dev/null +++ b/doc/development/testing_guide/frontend_testing.md @@ -0,0 +1,254 @@ +# Frontend testing standards and style guidelines + +There are two types of test suites you'll encounter while developing frontend code +at GitLab. We use Karma and Jasmine for JavaScript unit and integration testing, +and RSpec feature tests with Capybara for e2e (end-to-end) integration testing. + +Unit and feature tests need to be written for all new features. +Most of the time, you should use [RSpec] for your feature tests. + +Regression tests should be written for bug fixes to prevent them from recurring +in the future. + +See the [Testing Standards and Style Guidelines](index.md) page for more +information on general testing practices at GitLab. + +## Karma test suite + +GitLab uses the [Karma][karma] test runner with [Jasmine] as its test +framework for our JavaScript unit and integration tests. For integration tests, +we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples). +Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`). +Adding these static fixtures should be avoided as they are harder to keep up to date with real views. +The existing static fixtures will be migrated over time. +Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress. +Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin. + +JavaScript tests live in `spec/javascripts/`, matching the folder structure +of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js` +has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file. + +Keep in mind that in a CI environment, these tests are run in a headless +browser and you will not have access to certain APIs, such as +[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), +which will have to be stubbed. + +### Best practices + +#### Naming unit tests + +When writing describe test blocks to test specific functions/methods, +please use the method name as the describe block name. + +```javascript +// Good +describe('methodName', () => { + it('passes', () => { + expect(true).toEqual(true); + }); +}); + +// Bad +describe('#methodName', () => { + it('passes', () => { + expect(true).toEqual(true); + }); +}); + +// Bad +describe('.methodName', () => { + it('passes', () => { + expect(true).toEqual(true); + }); +}); +``` +#### Testing promises + +When testing Promises you should always make sure that the test is asynchronous and rejections are handled. +Your Promise chain should therefore end with a call of the `done` callback and `done.fail` in case an error occurred. + +```javascript +// Good +it('tests a promise', (done) => { + promise + .then((data) => { + expect(data).toBe(asExpected); + }) + .then(done) + .catch(done.fail); +}); + +// Good +it('tests a promise rejection', (done) => { + promise + .then(done.fail) + .catch((error) => { + expect(error).toBe(expectedError); + }) + .then(done) + .catch(done.fail); +}); + +// Bad (missing done callback) +it('tests a promise', () => { + promise + .then((data) => { + expect(data).toBe(asExpected); + }) +}); + +// Bad (missing catch) +it('tests a promise', (done) => { + promise + .then((data) => { + expect(data).toBe(asExpected); + }) + .then(done) +}); + +// Bad (use done.fail in asynchronous tests) +it('tests a promise', (done) => { + promise + .then((data) => { + expect(data).toBe(asExpected); + }) + .then(done) + .catch(fail) +}); + +// Bad (missing catch) +it('tests a promise rejection', (done) => { + promise + .catch((error) => { + expect(error).toBe(expectedError); + }) + .then(done) +}); +``` + +#### Stubbing + +For unit tests, you should stub methods that are unrelated to the current unit you are testing. +If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely. + +For integration tests, you should stub methods that will effect the stability of the test if they +execute their original behaviour. i.e. Network requests. + +### Vue.js unit tests + +See this [section][vue-test]. + +### Running frontend tests + +`rake karma` runs the frontend-only (JavaScript) tests. +It consists of two subtasks: + +- `rake karma:fixtures` (re-)generates fixtures +- `rake karma:tests` actually executes the tests + +As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`) +is sufficient (and saves you some time). + +### Live testing and focused testing + +While developing locally, it may be helpful to keep karma running so that you +can get instant feedback on as you write tests and modify code. To do this +you can start karma with `npm run karma-start`. It will compile the javascript +assets and run a server at `http://localhost:9876/` where it will automatically +run the tests on any browser which connects to it. You can enter that url on +multiple browsers at once to have it run the tests on each in parallel. + +While karma is running, any changes you make will instantly trigger a recompile +and retest of the entire test suite, so you can see instantly if you've broken +a test with your changes. You can use [jasmine focused][jasmine-focus] or +excluded tests (with `fdescribe` or `xdescribe`) to get karma to run only the +tests you want while you're working on a specific feature, but make sure to +remove these directives when you commit your code. + +## RSpec feature integration tests + +Information on setting up and running RSpec integration tests with +[Capybara] can be found in the [Testing Best Practices](best_practices.md). + +## Gotchas + +### Errors due to use of unsupported JavaScript features + +Similar errors will be thrown if you're using JavaScript features not yet +supported by the PhantomJS test runner which is used for both Karma and RSpec +tests. We polyfill some JavaScript objects for older browsers, but some +features are still unavailable: + +- Array.from +- Array.first +- Async functions +- Generators +- Array destructuring +- For..Of +- Symbol/Symbol.iterator +- Spread + +Until these are polyfilled appropriately, they should not be used. Please +update this list with additional unsupported features. + +### RSpec errors due to JavaScript + +By default RSpec unit tests will not run JavaScript in the headless browser +and will simply rely on inspecting the HTML generated by rails. + +If an integration test depends on JavaScript to run correctly, you need to make +sure the spec is configured to enable JavaScript when the tests are run. If you +don't do this you'll see vague error messages from the spec runner. + +To enable a JavaScript driver in an `rspec` test, add `:js` to the +individual spec or the context block containing multiple specs that need +JavaScript enabled: + +```ruby +# For one spec +it 'presents information about abuse report', :js do + # assertions... +end + +describe "Admin::AbuseReports", :js do + it 'presents information about abuse report' do + # assertions... + end + it 'shows buttons for adding to abuse report' do + # assertions... + end +end +``` + +### Spinach errors due to missing JavaScript + +NOTE: **Note:** Since we are discouraging the use of Spinach when writing new +feature tests, you shouldn't ever need to use this. This information is kept +available for legacy purposes only. + +In Spinach, the JavaScript driver is enabled differently. In the `*.feature` +file for the failing spec, add the `@javascript` flag above the Scenario: + +``` +@javascript +Scenario: Developer can approve merge request + Given I am a "Shop" developer + And I visit project "Shop" merge requests page + And merge request 'Bug NS-04' must be approved + And I click link "Bug NS-04" + When I click link "Approve" + Then I should see approved merge request "Bug NS-04" +``` + +[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html +[jasmine-jquery]: https://github.com/velesin/jasmine-jquery +[karma]: http://karma-runner.github.io/ +[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components +[RSpec]: https://github.com/rspec/rspec-rails#feature-specs +[Capybara]: https://github.com/teamcapybara/capybara +[Karma]: http://karma-runner.github.io/ +[Jasmine]: https://jasmine.github.io/ + +--- + +[Return to Testing documentation](index.md) diff --git a/doc/development/fe_guide/img/testing_triangle.png b/doc/development/testing_guide/img/testing_triangle.png Binary files differindex 7a9a848c2ee..7a9a848c2ee 100644 --- a/doc/development/fe_guide/img/testing_triangle.png +++ b/doc/development/testing_guide/img/testing_triangle.png diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md new file mode 100644 index 00000000000..8045bbad7ba --- /dev/null +++ b/doc/development/testing_guide/index.md @@ -0,0 +1,91 @@ +# Testing standards and style guidelines + +This document describes various guidelines and best practices for automated +testing of the GitLab project. + +It is meant to be an _extension_ of the [thoughtbot testing +styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If +this guide defines a rule that contradicts the thoughtbot guide, this guide +takes precedence. Some guidelines may be repeated verbatim to stress their +importance. + +## Overview + +GitLab is built on top of [Ruby on Rails][rails], and we're using [RSpec] for all +the backend tests, with [Capybara] for end-to-end integration testing. +On the frontend side, we're using [Karma] and [Jasmine] for JavaScript unit and +integration testing. + +Following are two great articles that everyone should read to understand what +automated testing means, and what are its principles: + +- [Five Factor Testing](https://www.devmynd.com/blog/five-factor-testing): Why do we need tests? +- [Principles of Automated Testing](http://www.lihaoyi.com/post/PrinciplesofAutomatedTesting.html): Levels of testing. Prioritize tests. Cost of tests. + +--- + +## [Testing levels](testing_levels.md) + +Learn about the different testing levels, and how to decide at what level your +changes should be tested. + +--- + +## [Testing best practices](best_practices.md) + +Everything you should know about how to write good tests: RSpec, FactoryGirl, +system tests, parameterized tests etc. + +--- + +## [Frontend testing standards and style guidelines](frontend_testing.md) + +Everything you should know about how to write good Frontend tests: Karma, +testing promises, stubbing etc. + +--- + +## [Flaky tests](flaky_tests.md) + +What are flaky tests, the different kind of flaky tests we encountered, and what +we do about them. + +--- + +## [GitLab tests in the Continuous Integration (CI) context](ci.md) + +How GitLab test suite is run in the CI context: setup, caches, artifacts, +parallelization, monitoring. + +--- + +## [Testing Rake tasks](testing_rake_tasks.md) + +Everything you should know about how to test Rake tasks. + +--- + +## Spinach (feature) tests + +GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) +for its feature/integration tests in September 2012. + +As of March 2016, we are [trying to avoid adding new Spinach +tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward, +opting for [RSpec feature](#features-integration) specs. + +Adding new Spinach scenarios is acceptable _only if_ the new scenario requires +no more than one new `step` definition. If more than that is required, the +test should be re-implemented using RSpec instead. + +--- + +[Return to Development documentation](../README.md) + +[^1]: /ci/yaml/README.html#dependencies + +[rails]: http://rubyonrails.org/ +[RSpec]: https://github.com/rspec/rspec-rails#feature-specs +[Capybara]: https://github.com/teamcapybara/capybara +[Karma]: http://karma-runner.github.io/ +[Jasmine]: https://jasmine.github.io/ diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md new file mode 100644 index 00000000000..9b9ba0baa71 --- /dev/null +++ b/doc/development/testing_guide/testing_levels.md @@ -0,0 +1,173 @@ +# Testing levels + +![Testing priority triangle](img/testing_triangle.png) + +_This diagram demonstrates the relative priority of each test type we use. `e2e` stands for end-to-end._ + +## Unit tests + +Formal definition: https://en.wikipedia.org/wiki/Unit_testing + +These kind of tests ensure that a single unit of code (a method) works as +expected (given an input, it has a predictable output). These tests should be +isolated as much as possible. For example, model methods that don't do anything +with the database shouldn't need a DB record. Classes that don't need database +records should use stubs/doubles as much as possible. + +| Code path | Tests path | Testing engine | Notes | +| --------- | ---------- | -------------- | ----- | +| `app/finders/` | `spec/finders/` | RSpec | | +| `app/helpers/` | `spec/helpers/` | RSpec | | +| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). | +| `app/policies/` | `spec/policies/` | RSpec | | +| `app/presenters/` | `spec/presenters/` | RSpec | | +| `app/routing/` | `spec/routing/` | RSpec | | +| `app/serializers/` | `spec/serializers/` | RSpec | | +| `app/services/` | `spec/services/` | RSpec | | +| `app/tasks/` | `spec/tasks/` | RSpec | | +| `app/uploaders/` | `spec/uploaders/` | RSpec | | +| `app/views/` | `spec/views/` | RSpec | | +| `app/workers/` | `spec/workers/` | RSpec | | +| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Frontent Testing guide](frontend_testing.md) section. | + +## Integration tests + +Formal definition: https://en.wikipedia.org/wiki/Integration_testing + +These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). These tests should assert at the request/response level: status code, headers, body. They're useful to test permissions, redirections, what view is rendered etc. + +| Code path | Tests path | Testing engine | Notes | +| --------- | ---------- | -------------- | ----- | +| `app/controllers/` | `spec/controllers/` | RSpec | | +| `app/mailers/` | `spec/mailers/` | RSpec | | +| `lib/api/` | `spec/requests/api/` | RSpec | | +| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | | +| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. | + +### About controller tests + +In an ideal world, controllers should be thin. However, when this is not the +case, it's acceptable to write a system/feature test without JavaScript instead +of a controller test. The reason is that testing a fat controller usually +involves a lot of stubbing, things like: + +```ruby +controller.instance_variable_set(:@user, user) +``` + +and use methods which are deprecated in Rails 5 ([#23768]). + +[#23768]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23768 + +### About Karma + +As you may have noticed, Karma is both in the Unit tests and the Integration +tests category. That's because Karma is a tool that provides an environment to +run JavaScript tests, so you can either run unit tests (e.g. test a single +JavaScript method), or integration tests (e.g. test a component that is composed +of multiple components). + +## System tests or feature tests + +Formal definition: https://en.wikipedia.org/wiki/System_testing. + +These kind of tests ensure the application works as expected from a user point +of view (aka black-box testing). These tests should test a happy path for a +given page or set of pages, and a test case should be added for any regression +that couldn't have been caught at lower levels with better tests (i.e. if a +regression is found, regression tests should be added at the lowest-level +possible). + +| Tests path | Testing engine | Notes | +| ---------- | -------------- | ----- | +| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. | +| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). | + +### Consider **not** writing a system test! + +If we're confident that the low-level components work well (and we should be if +we have enough Unit & Integration tests), we shouldn't need to duplicate their +thorough testing at the System test level. + +It's very easy to add tests, but a lot harder to remove or improve tests, so one +should take care of not introducing too many (slow and duplicated) specs. + +The reasons why we should follow these best practices are as follows: + +- System tests are slow to run since they spin up the entire application stack + in a headless browser, and even slower when they integrate a JS driver +- When system tests run with a JavaScript driver, the tests are run in a + different thread than the application. This means it does not share a + database connection and your test will have to commit the transactions in + order for the running application to see the data (and vice-versa). In that + case we need to truncate the database after each spec instead of simply + rolling back a transaction (the faster strategy that's in use for other kind + of tests). This is slower than transactions, however, so we want to use + truncation only when necessary. + +[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist +[RackTest]: https://github.com/teamcapybara/capybara#racktest + +## Black-box tests or end-to-end tests + +GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse], +[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces +are configured and packaged by [GitLab Omnibus]. + +[GitLab QA] is a tool that allows to test that all these pieces integrate well +together by building a Docker image for a given version of GitLab Rails and +running feature tests (i.e. using Capybara) against it. + +The actual test scenarios and steps are [part of GitLab Rails] so that they're +always in-sync with the codebase. + +[multiple pieces]: ../architecture.md#components +[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell +[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse +[Gitaly]: https://gitlab.com/gitlab-org/gitaly +[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages +[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner +[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab +[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa +[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa + +## How to test at the correct level? + +As many things in life, deciding what to test at each level of testing is a +trade-off: + +- Unit tests are usually cheap, and you should consider them like the basement + of your house: you need them to be confident that your code is behaving + correctly. However if you run only unit tests without integration / system + tests, you might [miss] the [big] [picture]! +- Integration tests are a bit more expensive, but don't abuse them. A system test + is often better than an integration test that is stubbing a lot of internals. +- System tests are expensive (compared to unit tests), even more if they require + a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed) + section. + +Another way to see it is to think about the "cost of tests", this is well +explained [in this article][tests-cost] and the basic idea is that the cost of a +test includes: + +- The time it takes to write the test +- The time it takes to run the test every time the suite runs +- The time it takes to understand the test +- The time it takes to fix the test if it breaks and the underlying code is OK +- Maybe, the time it takes to change the code to make the code testable. + +### Frontend-related tests + +There are cases where the behaviour you are testing is not worth the time spent +running the full application, for example, if you are testing styling, animation, +edge cases or small actions that don't involve the backend, +you should write an integration test using Jasmine. + +[miss]: https://twitter.com/ThePracticalDev/status/850748070698651649 +[big]: https://twitter.com/timbray/status/822470746773409794 +[picture]: https://twitter.com/withzombies/status/829716565834752000 +[tests-cost]: https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e + +--- + +[Return to Testing documentation](index.md) diff --git a/doc/development/testing_guide/testing_rake_tasks.md b/doc/development/testing_guide/testing_rake_tasks.md new file mode 100644 index 00000000000..5bf185dd7b5 --- /dev/null +++ b/doc/development/testing_guide/testing_rake_tasks.md @@ -0,0 +1,39 @@ +## Testing Rake tasks + +To make testing Rake tasks a little easier, there is a helper that can be included +in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use +`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures +a few other things to make testing Rake tasks easier. + +At a minimum, requiring the Rake helper will redirect `stdout`, include the +runtime task helpers, and include the `RakeHelpers` Spec support module. + +The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make +executing tasks simple. See `spec/support/rake_helpers.rb` for all available +methods. + +Example: + +```ruby +require 'rake_helper' + +describe 'gitlab:shell rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/shell' + + stub_warn_user_is_not_gitlab + end + + describe 'install task' do + it 'invokes create_hooks task' do + expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke) + + run_rake_task('gitlab:shell:install') + end + end +end +``` + +--- + +[Return to Testing documentation](index.md) diff --git a/doc/install/installation.md b/doc/install/installation.md index af6c797dc00..2c93297ca2f 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -121,7 +121,7 @@ The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab in production, frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH, and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly -advise everyone to follow the instructions below to use a system Ruby. +advise everyone to follow the instructions below to use a system Ruby. Linux distributions generally have older versions of Ruby available, so these instructions are designed to install Ruby from the official source code. @@ -299,9 +299,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 10-0-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-1-stable gitlab -**Note:** You can change `10-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `10-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_chart.md b/doc/install/kubernetes/gitlab_chart.md index ddfd47df099..fa564d83785 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -1,6 +1,6 @@ # GitLab Helm Chart > **Note**: -* This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). For more information on available charts, please see our [overview](index.md#chart-overview). +* This chart is deprecated, and is being replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). For more information on available charts, please see our [overview](index.md#chart-overview). * These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). @@ -8,7 +8,9 @@ For more information on available GitLab Helm Charts, please see our [overview]( ## Introduction -The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. For most deployments we **strongly recommended** the [gitlab-omnibus](gitlab_omnibus.md) chart, which will replace this chart once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). Due to the difficulty in supporting upgrades to the `omnibus-gitlab` chart, migrating will require exporting data out of this instance and importing it into the new deployment. +The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. We **strongly recommend** the [gitlab-omnibus](gitlab_omnibus.md) chart. + +This chart is deprecated, and will be replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). Due to the difficulty in supporting upgrades, migrating will require exporting data out of this instance and importing it into the new deployment. This chart includes the following: diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index 8c110a37380..6659c3cf7b2 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -1,7 +1,6 @@ # GitLab-Omnibus Helm Chart > **Note:** -* This Helm chart is in beta, while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being worked on. -* GitLab is working on a [cloud native set of Charts](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) which will eventually replace these. +* This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). * These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work. @@ -12,6 +11,8 @@ For more information on available GitLab Helm Charts, please see our [overview]( This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned via [Let's Encrypt](https://letsencrypt.org/). +This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) once available. Due to the difficulty in supporting upgrades, migrating will require exporting data out of this instance and importing it into the new deployment. + The deployment includes: - A [GitLab Omnibus](https://docs.gitlab.com/omnibus/) Pod, including Mattermost, Container Registry, and Prometheus @@ -23,9 +24,8 @@ The deployment includes: ### Limitations -* This chart is suited for small to medium size deployments, because [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) will not be supported. -* It is in beta. Additional features to support production deployments, like backups, are [in development](https://gitlab.com/charts/charts.gitlab.io/issues/68). Once completed, this chart will be generally available. -* A new generation of [cloud native charts](index.md#upcoming-cloud-native-helm-charts) is in development, and will eventually deprecate these. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We do not expect these to be production ready before the second half of 2018. +* This chart is in beta, and suited for small to medium size deployments. [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) are not supported. +* A new generation [cloud native GitLab chart](index.md#cloud-native-gitlab-chart) is in development, and will deprecate this chart. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We plan to release the new chart in beta by the end of 2017. For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md index aed00ae9e2c..dd350820c18 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -9,11 +9,11 @@ should be deployed, upgraded, and configured. ## Chart Overview -* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today. It is suited for small to medium deployments, and is in beta while support for backups and other features are added. +* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small to medium deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart). * **[Cloud Native GitLab Chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in development. Will support large deployments with horizontal scaling of individual GitLab components. * Other Charts * [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner. - * [Advanced GitLab Installation](gitlab_chart.md): Provides additional deployment options, but provides less functionality out-of-the-box. It's beta, no longer actively developed, and will be deprecated by [gitlab-omnibus](#gitlab-omnibus-chart-recommended) once it supports these options. + * [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box. * [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart. ## GitLab-Omnibus Chart (Recommended) @@ -21,13 +21,13 @@ should be deployed, upgraded, and configured. This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](../../user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html). -Once the [cloud native charts](#upcoming-cloud-native-helm-charts) are ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. +Once the [cloud native GitLab chart](#cloud-native-gitlab-chart) is ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. -Learn more about the [gitlab-omnibus chart.](gitlab_omnibus.md) +Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md). ## Cloud Native GitLab Chart -GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended). +GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current chart](#gitlab-omnibus-chart-recommended). By offering individual containers and charts, we will be able to provide a number of benefits: * Easier horizontal scaling of each service, @@ -35,9 +35,9 @@ By offering individual containers and charts, we will be able to provide a numbe * Potential for rolling updates and canaries within a service, * and plenty more. -This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We do not expect these to be production ready before the second half of 2018. +This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We are planning to launch this chart in beta by the end of 2017. -Learn more about the [cloud native GitLab chart.](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) +Learn more about the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). ## Other Charts diff --git a/doc/integration/google.md b/doc/integration/google.md index d5b523e6dc0..0611cbb59dc 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -1,83 +1,92 @@ # Google OAuth2 OmniAuth Provider -To enable the Google OAuth2 OmniAuth provider you must register your application with Google. Google will generate a client ID and secret key for you to use. - -1. Sign in to the [Google Developers Console](https://console.developers.google.com/) with the Google account you want to use to register GitLab. - -1. Select "Create Project". - -1. Provide the project information - - Project name: 'GitLab' works just fine here. - - Project ID: Must be unique to all Google Developer registered applications. Google provides a randomly generated Project ID by default. You can use the randomly generated ID or choose a new one. -1. Refresh the page. You should now see your new project in the list. Click on the project. - -1. Select the "Google APIs" tab in the Overview. - -1. Select and enable the following Google APIs - listed under "Popular APIs" - - Enable `Contacts API` - - Enable `Google+ API` +To enable the Google OAuth2 OmniAuth provider you must register your application +with Google. Google will generate a client ID and secret key for you to use. + +## Enabling Google OAuth + +In Google's side: + +1. Navigate to the [cloud resource manager](https://console.cloud.google.com/cloud-resource-manager) page +1. Select **Create Project** +1. Provide the project information: + - **Project name** - "GitLab" works just fine here. + - **Project ID** - Must be unique to all Google Developer registered applications. + Google provides a randomly generated Project ID by default. You can use + the randomly generated ID or choose a new one. +1. Refresh the page and you should see your new project in the list +1. Go to the [Google API Console](https://console.developers.google.com/apis/dashboard) +1. Select the previously created project form the upper left corner +1. Select **Credentials** from the sidebar +1. Select **OAuth consent screen** and fill the form with the required information +1. In the **Credentials** tab, select **Create credentials > OAuth client ID** +1. Fill in the required information + - **Application type** - Choose "Web Application" + - **Name** - Use the default one or provide your own + - **Authorized JavaScript origins** -This isn't really used by GitLab but go + ahead and put `https://gitlab.example.com` + - **Authorized redirect URIs** - Enter your domain name followed by the + callback URIs one at a time: -1. Select "Credentials" in the submenu. + ``` + https://gitlab.example.com/users/auth/google_oauth2/callback + https://gitlab.exampl.com/-/google_api/auth/callback + ``` -1. Select "Create New Client ID". +1. You should now be able to see a Client ID and Client secret. Note them down + or keep this page open as you will need them later. +1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Google Cloud APIs > Container Engine API > Enable** -1. Fill in the required information - - Application type: "Web Application" - - Authorized JavaScript origins: This isn't really used by GitLab but go ahead and put 'https://gitlab.example.com' here. - - Authorized redirect URI: 'https://gitlab.example.com/users/auth/google_oauth2/callback' -1. Under the heading "Client ID for web application" you should see a Client ID and Client secret (see screenshot). Keep this page open as you continue configuration. ![Google app](img/google_app.png) +On your GitLab server: -1. On your GitLab server, open the configuration file. +1. Open the configuration file. - For omnibus package: + For Omnibus GitLab: ```sh - sudo editor /etc/gitlab/gitlab.rb + sudo editor /etc/gitlab/gitlab.rb ``` For installations from source: ```sh - cd /home/git/gitlab - - sudo -u git -H editor config/gitlab.yml + cd /home/git/gitlab + sudo -u git -H editor config/gitlab.yml ``` -1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. - -1. Add the provider configuration: +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. +1. Add the provider configuration: - For omnibus package: + For Omnibus GitLab: ```ruby - gitlab_rails['omniauth_providers'] = [ - { - "name" => "google_oauth2", - "app_id" => "YOUR_APP_ID", - "app_secret" => "YOUR_APP_SECRET", - "args" => { "access_type" => "offline", "approval_prompt" => '' } - } - ] + gitlab_rails['omniauth_providers'] = [ + { + "name" => "google_oauth2", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET", + "args" => { "access_type" => "offline", "approval_prompt" => '' } + } + ] ``` For installations from source: ``` - - { name: 'google_oauth2', app_id: 'YOUR_APP_ID', - app_secret: 'YOUR_APP_SECRET', - args: { access_type: 'offline', approval_prompt: '' } } + - { name: 'google_oauth2', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', + args: { access_type: 'offline', approval_prompt: '' } } ``` -1. Change 'YOUR_APP_ID' to the client ID from the Google Developer page from step 10. - -1. Change 'YOUR_APP_SECRET' to the client secret from the Google Developer page from step 10. - -1. Make sure that you configure GitLab to use an FQDN as Google will not accept raw IP addresses. +1. Change `YOUR_APP_ID` to the client ID from the Google Developer page +1. Similarly, change `YOUR_APP_SECRET` to the client secret +1. Make sure that you configure GitLab to use an FQDN as Google will not accept + raw IP addresses. For Omnibus packages: ```ruby - external_url 'https://gitlab.example.com' + external_url 'https://gitlab.example.com' ``` For installations from source: @@ -88,21 +97,32 @@ To enable the Google OAuth2 OmniAuth provider you must register your application ``` 1. Save the configuration file. - 1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you installed GitLab via Omnibus or from source respectively. -On the sign in page there should now be a Google icon below the regular sign in form. Click the icon to begin the authentication process. Google will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. +On the sign in page there should now be a Google icon below the regular sign in +form. Click the icon to begin the authentication process. Google will ask the +user to sign in and authorize the GitLab application. If everything goes well +the user will be returned to GitLab and will be signed in. ## Further Configuration -This further configuration is not required for Google authentication to function but it is strongly recommended. Taking these steps will increase usability for users by providing a little more recognition and branding. - -At this point, when users first try to authenticate to your GitLab installation with Google they will see a generic application name on the prompt screen. The prompt informs the user that "Project Default Service Account" would like to access their account. "Project Default Service Account" isn't very recognizable and may confuse or cause users to be concerned. This is easily changeable. - -1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for instructions on how to get here if you closed your window). -1. Scroll down until you find "Product Name". Change the product name to something more descriptive. -1. Add any additional information as you wish - homepage, logo, privacy policy, etc. None of this is required, but it may help your users. +This further configuration is not required for Google authentication to function +but it is strongly recommended. Taking these steps will increase usability for +users by providing a little more recognition and branding. + +At this point, when users first try to authenticate to your GitLab installation +with Google they will see a generic application name on the prompt screen. The +prompt informs the user that "Project Default Service Account" would like to +access their account. "Project Default Service Account" isn't very recognizable +and may confuse or cause users to be concerned. This is easily changeable: + +1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for + instructions on how to get here if you closed your window). +1. Scroll down until you find "Product Name". Change the product name to + something more descriptive. +1. Add any additional information as you wish - homepage, logo, privacy policy, + etc. None of this is required, but it may help your users. [reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart GitLab]: ../administration/restart_gitlab.md#installations-from-source diff --git a/doc/update/10.0-to-10.1.md b/doc/update/10.0-to-10.1.md index 4a9384f3ad6..dc14c779026 100644 --- a/doc/update/10.0-to-10.1.md +++ b/doc/update/10.0-to-10.1.md @@ -150,7 +150,7 @@ sudo -u git -H make #### 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'. +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 ' @@ -335,11 +335,11 @@ 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 (9.5) +## Things went south? Revert to previous version (10.0) ### 1. Revert the code to the previous version -Follow the [upgrade guide from 9.4 to 9.5](9.4-to-9.5.md), except for the +Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the database migration (the backup is already migrated to the previous version). ### 2. Restore from the backup diff --git a/doc/user/discussions/img/image_resolved_discussion.png b/doc/user/discussions/img/image_resolved_discussion.png Binary files differnew file mode 100755 index 00000000000..ed00b5c77fe --- /dev/null +++ b/doc/user/discussions/img/image_resolved_discussion.png diff --git a/doc/user/discussions/img/onion_skin_view.png b/doc/user/discussions/img/onion_skin_view.png Binary files differnew file mode 100755 index 00000000000..91c3b396844 --- /dev/null +++ b/doc/user/discussions/img/onion_skin_view.png diff --git a/doc/user/discussions/img/start_image_discussion.gif b/doc/user/discussions/img/start_image_discussion.gif Binary files differnew file mode 100644 index 00000000000..43efbf2fbb2 --- /dev/null +++ b/doc/user/discussions/img/start_image_discussion.gif diff --git a/doc/user/discussions/img/swipe_view.png b/doc/user/discussions/img/swipe_view.png Binary files differnew file mode 100755 index 00000000000..82d6e52173c --- /dev/null +++ b/doc/user/discussions/img/swipe_view.png diff --git a/doc/user/discussions/img/two_up_view.png b/doc/user/discussions/img/two_up_view.png Binary files differnew file mode 100755 index 00000000000..d9e90708e87 --- /dev/null +++ b/doc/user/discussions/img/two_up_view.png diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index ab46befb91c..2206b2860f4 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -153,42 +153,71 @@ comments in greater detail. ![Discussion comment](img/discussion_comment.png) -## Locking discussions +## Image discussions -> [Introduced][ce-14531] in GitLab 10.1. +> [Introduced][ce-14061] in GitLab 10.1. + +Sometimes a discussion is revolved around an image. With image discussions, +you can easily target a specific coordinate of an image and start a discussion +around it. Image discussions are available in merge requests and commit detail views. + +To start an image discussion, hover your mouse over the image. Your mouse pointer +should convert into an icon, indicating that the image is available for commenting. +Simply click anywhere on the image to create a new discussion. + +![Start image discussion](img/start_image_discussion.gif) + +After you click on the image, a comment form will be displayed that would be the start +of your discussion. Once you save your comment, you will see a new badge displayed on +top of your image. This badge represents your discussion. + +>**Note:** +This discussion badge is typically associated with a number that is only used as a visual +reference for each discussion. In the merge request discussion tab, +this badge will be indicated with a comment icon since each discussion will render a new +image section. -There might be some cases where a discussion is better off if it's locked down. -For example: +Image discussions also work on diffs that replace an existing image. In this diff view +mode, you can toggle the different view modes and still see the discussion point badges. -- Discussions that are several years old and the issue/merge request is closed, - but people continue to try to resurrect the discussion. -- Discussions where someone or a group of people are trolling, are abusive, or - in-general are causing the discussion to be unproductive. +| 2-up | Swipe | Onion Skin | +| :-----------: | :----------: | :----------: | +| ![2-up view](img/two_up_view.png) | ![swipe view](img/swipe_view.png) | ![onion skin view](img/onion_skin_view.png) | + +Image discussions also work well with resolvable discussions. Resolved discussions +on diffs (not on the merge request discussion tab) will appear collapsed on page +load and will have a corresponding badge counter to match the counter on the image. + +![Image resolved discussion](img/image_resolved_discussion.png) + +## Lock discussions + +> [Introduced][ce-14531] in GitLab 10.1. -In locked discussions, only team members can write new comments and edit the old -ones. +For large projects with many contributors, it may be useful to stop discussions +in issues or merge requests in these scenarios: -To lock or unlock a discussion, you need to have at least Master [permissions]: +- The project maintainer has already resolved the discussion and it is not helpful +for continued feedback. The project maintainer has already directed new conversation +to newer issues or merge requests. +- The people participating in the discussion are trolling, abusive, or otherwise +being unproductive. -1. Find the "Lock" section in the sidebar and click **Edit** -1. In the dialog that will appear, you can choose to turn on or turn off the - discussion lock -1. Optionally, leave a comment to explain your reasoning behind that action +In these cases, a user with Master permissions or higher in the project can lock (and unlock) +an issue or a merge request, using the "Lock" section in the sidebar: -| Turn off discussion lock | Turn on discussion lock | +| Unlock | Lock | | :-----------: | :----------: | | ![Turn off discussion lock](img/turn_off_lock.png) | ![Turn on discussion lock](img/turn_on_lock.png) | -Every change is indicated by a system note in the issue's or merge request's -comments. +System notes indicate locking and unlocking. ![Discussion lock system notes](img/discussion_lock_system_notes.png) -Once an issue or merge request is locked, project members can see the indicator -in the comment area, whereas non project members can only see the information -that the discussion is locked. +In a locked issue or merge request, only team members can add new comments and +edit existing comments. Non-team members are restricted from adding or editing comments. -| Team member | Not a member | +| Team member | Non-team member | | :-----------: | :----------: | | ![Comment form member](img/lock_form_member.png) | ![Comment form non-member](img/lock_form_non_member.png) | @@ -198,6 +227,7 @@ that the discussion is locked. [ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180 [ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266 [ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053 +[ce-14061]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14061 [ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531 [resolve-discussion-button]: img/resolve_discussion_button.png [resolve-comment-button]: img/resolve_comment_button.png diff --git a/doc/user/permissions.md b/doc/user/permissions.md index be72d42625a..c03700a3501 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -25,7 +25,7 @@ The following table depicts the various user permission levels in a project. | Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ | | Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | -| Lock comments | | | | ✓ | ✓ | +| Lock discussions (issues and merge requests) | | | | ✓ | ✓ | | See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ | | See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ | | Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ | @@ -54,7 +54,7 @@ The following table depicts the various user permission levels in a project. | Create or update commit status | | | ✓ | ✓ | ✓ | | Update a container registry | | | ✓ | ✓ | ✓ | | Remove a container registry image | | | ✓ | ✓ | ✓ | -| Create new milestones | | | | ✓ | ✓ | +| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ | | Enable/disable branch protection | | | | ✓ | ✓ | @@ -76,6 +76,7 @@ The following table depicts the various user permission levels in a project. | Force push to protected branches [^4] | | | | | | | Remove protected branches [^4] | | | | | | | Remove pages | | | | | ✓ | +| Manage clusters | | | | ✓ | ✓ | ## Project features permissions @@ -143,6 +144,7 @@ group. | Manage group members | | | | | ✓ | | Remove group | | | | | ✓ | | Manage group labels | | ✓ | ✓ | ✓ | ✓ | +| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ | ### Subgroup permissions diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md new file mode 100644 index 00000000000..7d9e771f570 --- /dev/null +++ b/doc/user/project/clusters/index.md @@ -0,0 +1,90 @@ +# Connecting GitLab with GKE + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/35954) in 10.1. + +CAUTION: **Warning:** +The Cluster integration is currently in **Beta**. + +Connect your project to Google Container Engine (GKE) in a few steps. + +With a cluster associated to your project, you can use Review Apps, deploy your +applications, run your pipelines, and much more in an easy way. + +NOTE: **Note:** +The Cluster integration will eventually supersede the +[Kubernetes integration](../integrations/kubernetes.md). For the moment, +you can create only one cluster. + +## Prerequisites + +In order to be able to manage your GKE cluster through GitLab, the following +prerequisites must be met: + +- The [Google authentication integration](../../../integration/google.md) must + be enabled in GitLab at the instance level. If that's not the case, ask your + administrator to enable it. +- Your associated Google account must have the right privileges to manage + clusters on GKE. That would mean that a + [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) + must be set up. +- You must have Master [permissions] in order to be able to access the **Cluster** + page. + +If all of the above requirements are met, you can proceed to add a new cluster. + +## Adding a cluster + +NOTE: **Note:** +You need Master [permissions] and above to add a cluster. + +To add a new cluster: + +1. Navigate to your project's **CI/CD > Cluster** page. +1. Connect your Google account if you haven't done already by clicking the + "Sign-in with Google" button. +1. Fill in the requested values: + - **Cluster name** (required) - The name you wish to give the cluster. + - **GCP project ID** (required) - The ID of the project you created in your GCP + console that will host the Kubernetes cluster. This must **not** be confused + with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). + - **Zone** - The zone under which the cluster will be created. Read more about + [the available zones](https://cloud.google.com/compute/docs/regions-zones/). + - **Number of nodes** - The number of nodes you wish the cluster to have. + - **Machine type** - The machine type of the Virtual Machine instance that + the cluster will be based on. Read more about [the available machine types](https://cloud.google.com/compute/docs/machine-types). + - **Project namespace** - The unique namespace for this project. By default you + don't have to fill it in; by leaving it blank, GitLab will create one for you. +1. Click the **Create cluster** button. + +After a few moments your cluster should be created. If something goes wrong, +you will be notified. + +Now, you can proceed to [enable the Cluster integration](#enabling-or-disabling-the-cluster-integration). + +## Enabling or disabling the Cluster integration + +After you have successfully added your cluster information, you can enable the +Cluster integration: + +1. Click the "Enabled/Disabled" switch +1. Hit **Save** for the changes to take effect + +You can now start using your Kubernetes cluster for your deployments. + +To disable the Cluster integration, follow the same procedure. + +## Removing the Cluster integration + +NOTE: **Note:** +You need Master [permissions] and above to remove a cluster integration. + +NOTE: **Note:** +When you remove a cluster, you only remove its relation to GitLab, not the +cluster itself. To remove the cluster, you can do so by visiting the GKE +dashboard or using `kubectl`. + +To remove the Cluster integration from your project, simply click on the +**Remove integration** button. You will then be able to follow the procedure +and [add a cluster](#adding-a-cluster) again. + +[permissions]: ../../permissions.md diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 03bbc46bd8c..97d0d529886 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -63,6 +63,8 @@ common actions on issues or merge requests browse, and download job artifacts - [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job), timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more + - [GKE cluster integration](clusters/index.md): Connecting your GitLab project + with Google Container Engine - [GitLab Pages](pages/index.md): Build, test, and deploy your static website with GitLab Pages diff --git a/features/explore/projects.feature b/features/explore/projects.feature deleted file mode 100644 index 4e0f4486ab7..00000000000 --- a/features/explore/projects.feature +++ /dev/null @@ -1,144 +0,0 @@ -@public -Feature: Explore Projects - Background: - Given public project "Community" - And internal project "Internal" - And private project "Enterprise" - - Scenario: I visit public area - Given archived project "Archive" - When I visit the public projects area - Then I should see project "Community" - And I should not see project "Internal" - And I should not see project "Enterprise" - And I should not see project "Archive" - - Scenario: I visit public project page - When I visit project "Community" page - Then I should see project "Community" home page - - Scenario: I visit internal project page - When I visit project "Internal" page - Then I should be redirected to sign in page - - Scenario: I visit private project page - When I visit project "Enterprise" page - Then I should be redirected to sign in page - - Scenario: I visit an empty public project page - Given public empty project "Empty Public Project" - When I visit empty project page - Then I should see empty public project details - And I should see empty public project details with http clone info - - Scenario: I visit an empty public project page as user with no ssh-keys - Given I sign in as a user - And I have no ssh keys - And public empty project "Empty Public Project" - When I visit empty project page - Then I should see empty public project details - And I should see empty public project details with http clone info - - Scenario: I visit an empty public project page as user with an ssh-key - Given I sign in as a user - And I have an ssh key - And public empty project "Empty Public Project" - When I visit empty project page - Then I should see empty public project details - And I should see empty public project details with ssh clone info - - Scenario: I visit public area as user - Given archived project "Archive" - And I sign in as a user - When I visit the public projects area - Then I should see project "Community" - And I should see project "Internal" - And I should not see project "Enterprise" - And I should not see project "Archive" - - Scenario: I visit internal project page as user - Given I sign in as a user - When I visit project "Internal" page - Then I should see project "Internal" home page - - Scenario: I visit public project page - When I visit project "Community" page - Then I should see project "Community" home page - And I should see an http link to the repository - - Scenario: I visit public project page as user with no ssh-keys - Given I sign in as a user - And I have no ssh keys - When I visit project "Community" page - Then I should see project "Community" home page - And I should see an http link to the repository - - Scenario: I visit public project page as user with an ssh-key - Given I sign in as a user - And I have an ssh key - When I visit project "Community" page - Then I should see project "Community" home page - And I should see an ssh link to the repository - - Scenario: I visit an empty public project page - Given public empty project "Empty Public Project" - When I visit empty project page - Then I should see empty public project details - - Scenario: I visit public project issues page as a non authorized user - Given I visit project "Community" page - Then I should not see command line instructions - And I visit "Community" issues page - Then I should see list of issues for "Community" project - - Scenario: I visit public project issues page as authorized user - Given I sign in as a user - Given I visit project "Community" page - And I visit "Community" issues page - Then I should see list of issues for "Community" project - - Scenario: I visit internal project issues page as authorized user - Given I sign in as a user - Given I visit project "Internal" page - And I visit "Internal" issues page - Then I should see list of issues for "Internal" project - - Scenario: I visit public project merge requests page as an authorized user - Given I sign in as a user - Given I visit project "Community" page - And I visit "Community" merge requests page - And project "Community" has "Bug fix" open merge request - Then I should see list of merge requests for "Community" project - - Scenario: I visit public project merge requests page as a non authorized user - Given I visit project "Community" page - And I visit "Community" merge requests page - And project "Community" has "Bug fix" open merge request - Then I should see list of merge requests for "Community" project - - Scenario: I visit internal project merge requests page as an authorized user - Given I sign in as a user - Given I visit project "Internal" page - And I visit "Internal" merge requests page - And project "Internal" has "Feature implemented" open merge request - Then I should see list of merge requests for "Internal" project - - Scenario: Trending page - Given archived project "Archive" - And project "Archive" has comments - And I sign in as a user - And project "Community" has comments - And trending projects are refreshed - When I visit the explore trending projects - Then I should see project "Community" - And I should not see project "Internal" - And I should not see project "Enterprise" - And I should not see project "Archive" - - Scenario: Most starred page - Given archived project "Archive" - And I sign in as a user - When I visit the explore starred projects - Then I should see project "Community" - And I should see project "Internal" - And I should not see project "Archive" diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb deleted file mode 100644 index 962e39dde9a..00000000000 --- a/features/steps/explore/projects.rb +++ /dev/null @@ -1,145 +0,0 @@ -class Spinach::Features::ExploreProjects < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - include SharedUser - - step 'I should see project "Empty Public Project"' do - expect(page).to have_content "Empty Public Project" - end - - step 'I should see public project details' do - expect(page).to have_content '32 branches' - expect(page).to have_content '16 tags' - end - - step 'I should see project readme' do - expect(page).to have_content 'README.md' - end - - step 'I should see empty public project details' do - expect(page).not_to have_content 'Git global setup' - end - - step 'I should see empty public project details with http clone info' do - project = Project.find_by(name: 'Empty Public Project') - page.all(:css, '.git-empty .clone').each do |element| - expect(element.text).to include(project.http_url_to_repo) - end - end - - step 'I should see empty public project details with ssh clone info' do - project = Project.find_by(name: 'Empty Public Project') - page.all(:css, '.git-empty .clone').each do |element| - expect(element.text).to include(project.url_to_repo) - end - end - - step 'I should see project "Community" home page' do - page.within '.breadcrumbs .breadcrumb-item-text' do - expect(page).to have_content 'Community' - end - end - - step 'I should see project "Internal" home page' do - page.within '.breadcrumbs .breadcrumb-item-text' do - expect(page).to have_content 'Internal' - end - end - - step 'I should see an http link to the repository' do - project = Project.find_by(name: 'Community') - expect(page).to have_field('project_clone', with: project.http_url_to_repo) - end - - step 'I should see an ssh link to the repository' do - project = Project.find_by(name: 'Community') - expect(page).to have_field('project_clone', with: project.url_to_repo) - end - - step 'I visit "Community" issues page' do - create(:issue, - title: "Bug", - project: public_project - ) - create(:issue, - title: "New feature", - project: public_project - ) - visit project_issues_path(public_project) - end - - step 'I should see list of issues for "Community" project' do - expect(page).to have_content "Bug" - expect(page).to have_content public_project.name - expect(page).to have_content "New feature" - end - - step 'I visit "Internal" issues page' do - create(:issue, - title: "Internal Bug", - project: internal_project - ) - create(:issue, - title: "New internal feature", - project: internal_project - ) - visit project_issues_path(internal_project) - end - - step 'I should see list of issues for "Internal" project' do - expect(page).to have_content "Internal Bug" - expect(page).to have_content internal_project.name - expect(page).to have_content "New internal feature" - end - - step 'I visit "Community" merge requests page' do - visit project_merge_requests_path(public_project) - end - - step 'project "Community" has "Bug fix" open merge request' do - create(:merge_request, - title: "Bug fix for public project", - source_project: public_project, - target_project: public_project - ) - end - - step 'I should see list of merge requests for "Community" project' do - expect(page).to have_content public_project.name - expect(page).to have_content public_merge_request.source_project.name - end - - step 'I visit "Internal" merge requests page' do - visit project_merge_requests_path(internal_project) - end - - step 'project "Internal" has "Feature implemented" open merge request' do - create(:merge_request, - title: "Feature implemented", - source_project: internal_project, - target_project: internal_project - ) - end - - step 'I should see list of merge requests for "Internal" project' do - expect(page).to have_content internal_project.name - expect(page).to have_content internal_merge_request.source_project.name - end - - def internal_project - @internal_project ||= Project.find_by!(name: 'Internal') - end - - def public_project - @public_project ||= Project.find_by!(name: 'Community') - end - - def internal_merge_request - @internal_merge_request ||= MergeRequest.find_by!(title: 'Feature implemented') - end - - def public_merge_request - @public_merge_request ||= MergeRequest.find_by!(title: 'Bug fix for public project') - end -end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index be69a96c3ee..dc0e3ac59a5 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -454,19 +454,6 @@ module SharedPaths # ---------------------------------------- # Public Projects # ---------------------------------------- - - step 'I visit the public projects area' do - visit explore_projects_path - end - - step 'I visit the explore trending projects' do - visit trending_explore_projects_path - end - - step 'I visit the explore starred projects' do - visit starred_explore_projects_path - end - step 'I visit the public groups area' do visit explore_groups_path end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 96cc0745e97..5e4edaf99a6 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -112,10 +112,6 @@ module SharedProject # Visibility of archived project # ---------------------------------------- - step 'archived project "Archive"' do - create(:project, :archived, :public, :repository, name: 'Archive') - end - step 'I should not see project "Archive"' do project = Project.find_by(name: "Archive") expect(page).not_to have_content project.name_with_namespace @@ -126,11 +122,6 @@ module SharedProject expect(page).to have_content project.name_with_namespace end - step 'project "Archive" has comments' do - project = Project.find_by(name: "Archive") - 2.times { create(:note_on_issue, project: project) } - end - # ---------------------------------------- # Visibility level # ---------------------------------------- @@ -209,15 +200,6 @@ module SharedProject create :project_empty_repo, :public, name: "Empty Public Project" end - step 'project "Community" has comments' do - project = Project.find_by(name: "Community") - 2.times { create(:note_on_issue, project: project) } - end - - step 'trending projects are refreshed' do - TrendingProject.refresh! - end - step 'project "Shop" has labels: "bug", "feature", "enhancement"' do project = Project.find_by(name: "Shop") create(:label, project: project, title: 'bug') diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index e79f988f549..87b9db66efd 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -42,6 +42,38 @@ module API # Helper Methods for Grape Endpoint module HelperMethods + def find_current_user + user = + find_user_from_private_token || + find_user_from_oauth_token || + find_user_from_warden + + return nil unless user + + raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) + + user + end + + def private_token + params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] + end + + private + + def find_user_from_private_token + token_string = private_token.to_s + return nil unless token_string.present? + + user = + find_user_by_authentication_token(token_string) || + find_user_by_personal_access_token(token_string) + + raise UnauthorizedError unless user + + user + end + # Invokes the doorkeeper guard. # # If token is presented and valid, then it sets @current_user. @@ -60,70 +92,89 @@ module API # scopes: (optional) scopes required for this guard. # Defaults to empty array. # - def doorkeeper_guard(scopes: []) - access_token = find_access_token - return nil unless access_token - - case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) - when AccessTokenValidationService::INSUFFICIENT_SCOPE - raise InsufficientScopeError.new(scopes) - - when AccessTokenValidationService::EXPIRED - raise ExpiredError + def find_user_from_oauth_token + access_token = find_oauth_access_token + return unless access_token - when AccessTokenValidationService::REVOKED - raise RevokedError + find_user_by_access_token(access_token) + end - when AccessTokenValidationService::VALID - User.find(access_token.resource_owner_id) - end + def find_user_by_authentication_token(token_string) + User.find_by_authentication_token(token_string) end - def find_user_by_private_token(scopes: []) - token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s + def find_user_by_personal_access_token(token_string) + access_token = PersonalAccessToken.find_by_token(token_string) + return unless access_token - return nil unless token_string.present? + find_user_by_access_token(access_token) + end - user = - find_user_by_authentication_token(token_string) || - find_user_by_personal_access_token(token_string, scopes) + # Check the Rails session for valid authentication details + def find_user_from_warden + warden.try(:authenticate) if verified_request? + end - raise UnauthorizedError unless user + def warden + env['warden'] + end - user + # Check if the request is GET/HEAD, or if CSRF token is valid. + def verified_request? + Gitlab::RequestForgeryProtection.verified?(env) end - private + def find_oauth_access_token + return @oauth_access_token if defined?(@oauth_access_token) - def find_user_by_authentication_token(token_string) - User.find_by_authentication_token(token_string) - end + token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods) + return @oauth_access_token = nil unless token - def find_user_by_personal_access_token(token_string, scopes) - access_token = PersonalAccessToken.active.find_by_token(token_string) - return unless access_token + @oauth_access_token = OauthAccessToken.by_token(token) + raise UnauthorizedError unless @oauth_access_token - if AccessTokenValidationService.new(access_token, request: request).include_any_scope?(scopes) - User.find(access_token.user_id) - end + @oauth_access_token.revoke_previous_refresh_token! + @oauth_access_token end - def find_access_token - return @access_token if defined?(@access_token) + def find_user_by_access_token(access_token) + scopes = scopes_registered_for_endpoint - token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods) - return @access_token = nil unless token + case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) + when AccessTokenValidationService::INSUFFICIENT_SCOPE + raise InsufficientScopeError.new(scopes) + + when AccessTokenValidationService::EXPIRED + raise ExpiredError - @access_token = Doorkeeper::AccessToken.by_token(token) - raise UnauthorizedError unless @access_token + when AccessTokenValidationService::REVOKED + raise RevokedError - @access_token.revoke_previous_refresh_token! - @access_token + when AccessTokenValidationService::VALID + access_token.user + end end def doorkeeper_request @doorkeeper_request ||= ActionDispatch::Request.new(env) end + + # An array of scopes that were registered (using `allow_access_with_scope`) + # for the current endpoint class. It also returns scopes registered on + # `API::API`, since these are meant to apply to all API routes. + def scopes_registered_for_endpoint + @scopes_registered_for_endpoint ||= + begin + endpoint_classes = [options[:for].presence, ::API::API].compact + endpoint_classes.reduce([]) do |memo, endpoint| + if endpoint.respond_to?(:allowed_scopes) + memo.concat(endpoint.allowed_scopes) + else + memo + end + end + end + end end module ClassMethods diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a87297a604c..2b316b58ed9 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -3,8 +3,6 @@ module API include Gitlab::Utils include Helpers::Pagination - UnauthorizedError = Class.new(StandardError) - SUDO_HEADER = "HTTP_SUDO".freeze SUDO_PARAM = :sudo @@ -379,47 +377,16 @@ module API private - def private_token - params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER] - end - - def warden - env['warden'] - end - - # Check if the request is GET/HEAD, or if CSRF token is valid. - def verified_request? - Gitlab::RequestForgeryProtection.verified?(env) - end - - # Check the Rails session for valid authentication details - def find_user_from_warden - warden.try(:authenticate) if verified_request? - end - def initial_current_user return @initial_current_user if defined?(@initial_current_user) begin @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user } - rescue APIGuard::UnauthorizedError, UnauthorizedError + rescue APIGuard::UnauthorizedError unauthorized! end end - def find_current_user - user = - find_user_by_private_token(scopes: scopes_registered_for_endpoint) || - doorkeeper_guard(scopes: scopes_registered_for_endpoint) || - find_user_from_warden - - return nil unless user - - raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) - - user - end - def sudo! return unless sudo_identifier return unless initial_current_user @@ -479,22 +446,5 @@ module API exception.status == 500 end - - # An array of scopes that were registered (using `allow_access_with_scope`) - # for the current endpoint class. It also returns scopes registered on - # `API::API`, since these are meant to apply to all API routes. - def scopes_registered_for_endpoint - @scopes_registered_for_endpoint ||= - begin - endpoint_classes = [options[:for].presence, ::API::API].compact - endpoint_classes.reduce([]) do |memo, endpoint| - if endpoint.respond_to?(:allowed_scopes) - memo.concat(endpoint.allowed_scopes) - else - memo - end - end - end - end end end diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb index a8cb7fc3fe7..0e9ef4f897c 100644 --- a/lib/gitlab/file_detector.rb +++ b/lib/gitlab/file_detector.rb @@ -6,31 +6,33 @@ module Gitlab module FileDetector PATTERNS = { # Project files - readme: /\Areadme/i, - changelog: /\A(changelog|history|changes|news)/i, - license: /\A(licen[sc]e|copying)(\..+|\z)/i, - contributing: /\Acontributing/i, + readme: /\Areadme[^\/]*\z/i, + changelog: /\A(changelog|history|changes|news)[^\/]*\z/i, + license: /\A(licen[sc]e|copying)(\.[^\/]+)?\z/i, + contributing: /\Acontributing[^\/]*\z/i, version: 'version', avatar: /\Alogo\.(png|jpg|gif)\z/, + issue_template: /\A\.gitlab\/issue_templates\/[^\/]+\.md\z/, + merge_request_template: /\A\.gitlab\/merge_request_templates\/[^\/]+\.md\z/, # Configuration files gitignore: '.gitignore', koding: '.koding.yml', gitlab_ci: '.gitlab-ci.yml', - route_map: 'route-map.yml', + route_map: '.gitlab/route-map.yml', # Dependency files - cartfile: /\ACartfile/, + cartfile: /\ACartfile[^\/]*\z/, composer_json: 'composer.json', gemfile: /\A(Gemfile|gems\.rb)\z/, gemfile_lock: 'Gemfile.lock', - gemspec: /\.gemspec\z/, + gemspec: /\A[^\/]*\.gemspec\z/, godeps_json: 'Godeps.json', package_json: 'package.json', podfile: 'Podfile', - podspec_json: /\.podspec\.json\z/, - podspec: /\.podspec\z/, - requirements_txt: /requirements\.txt\z/, + podspec_json: /\A[^\/]*\.podspec\.json\z/, + podspec: /\A[^\/]*\.podspec\z/, + requirements_txt: /\A[^\/]*requirements\.txt\z/, yarn_lock: 'yarn.lock' }.freeze @@ -63,13 +65,11 @@ module Gitlab # type_of('README.md') # => :readme # type_of('VERSION') # => :version def self.type_of(path) - name = File.basename(path) - PATTERNS.each do |type, search| did_match = if search.is_a?(Regexp) - name =~ search + path =~ search else - name.casecmp(search) == 0 + path.casecmp(search) == 0 end return type if did_match diff --git a/lib/gitlab/git/env.rb b/lib/gitlab/git/env.rb index f80193ac553..80f0731cd99 100644 --- a/lib/gitlab/git/env.rb +++ b/lib/gitlab/git/env.rb @@ -11,9 +11,11 @@ module Gitlab # # This class is thread-safe via RequestStore. class Env - WHITELISTED_GIT_VARIABLES = %w[ + WHITELISTED_VARIABLES = %w[ GIT_OBJECT_DIRECTORY + GIT_OBJECT_DIRECTORY_RELATIVE GIT_ALTERNATE_OBJECT_DIRECTORIES + GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE ].freeze def self.set(env) @@ -33,7 +35,7 @@ module Gitlab end def self.whitelist_git_env(env) - env.select { |key, _| WHITELISTED_GIT_VARIABLES.include?(key.to_s) }.with_indifferent_access + env.select { |key, _| WHITELISTED_VARIABLES.include?(key.to_s) }.with_indifferent_access end end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 0f059bef808..a6b2d189f18 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -12,6 +12,10 @@ module Gitlab GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES ].freeze + ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES = %w[ + GIT_OBJECT_DIRECTORY_RELATIVE + GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE + ].freeze SEARCH_CONTEXT_LINES = 3 NoRepository = Class.new(StandardError) @@ -193,7 +197,7 @@ module Gitlab def has_local_branches? gitaly_migrate(:has_local_branches) do |is_enabled| if is_enabled - gitaly_ref_client.has_local_branches? + gitaly_repository_client.has_local_branches? else has_local_branches_rugged? end @@ -1082,6 +1086,12 @@ module Gitlab @has_visible_content = has_local_branches? end + def fetch(remote = 'origin') + args = %W(#{Gitlab.config.git.bin_path} fetch #{remote}) + + popen(args, @path).last.zero? + end + def gitaly_repository Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository) end @@ -1220,7 +1230,16 @@ module Gitlab end def alternate_object_directories - Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).compact + relative_paths = Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact + + if relative_paths.any? + relative_paths.map { |d| File.join(path, d) } + else + Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES) + .flatten + .compact + .flat_map { |d| d.split(File::PATH_SEPARATOR) } + end end # Get the content of a blob for a given commit. If the blob is a commit diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 8214b7d63fa..b0c73395cb1 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -57,14 +57,6 @@ module Gitlab branch_names.count end - # TODO implement a more efficient RPC for this https://gitlab.com/gitlab-org/gitaly/issues/616 - def has_local_branches? - request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo) - response = GitalyClient.call(@storage, :ref_service, :find_all_branch_names, request).first - - response&.names.present? - end - def local_branches(sort_by: nil) request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo) request.sort_by = sort_by_param(sort_by) if sort_by diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index fdf912214e0..cef692d3c2a 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -58,6 +58,13 @@ module Gitlab request = Gitaly::CreateRepositoryRequest.new(repository: @gitaly_repo) GitalyClient.call(@storage, :repository_service, :create_repository, request) end + + def has_local_branches? + request = Gitaly::HasLocalBranchesRequest.new(repository: @gitaly_repo) + response = GitalyClient.call(@storage, :repository_service, :has_local_branches, request) + + response.value + end end end end diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb index da43c616b94..a1222a7e718 100644 --- a/lib/gitlab/gitaly_client/util.rb +++ b/lib/gitlab/gitaly_client/util.rb @@ -3,12 +3,18 @@ module Gitlab module Util class << self def repository(repository_storage, relative_path, gl_repository) + git_object_directory = Gitlab::Git::Env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence || + Gitlab::Git::Env['GIT_OBJECT_DIRECTORY'].presence + git_alternate_object_directories = + Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE']).presence || + Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES']).flat_map { |d| d.split(File::PATH_SEPARATOR) } + Gitaly::Repository.new( storage_name: repository_storage, relative_path: relative_path, gl_repository: gl_repository, - git_object_directory: Gitlab::Git::Env['GIT_OBJECT_DIRECTORY'].to_s, - git_alternate_object_directories: Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES']) + git_object_directory: git_object_directory.to_s, + git_alternate_object_directories: git_alternate_object_directories ) end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 3bc095a99a9..639f4f0c3f0 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -2,7 +2,7 @@ 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(milestones).freeze + RESTRICT_PROJECT_AND_GROUP = %i(milestone milestones).freeze def initialize(user:, shared:, project:) @path = File.join(shared.export_path, 'project.json') diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index a76cf1addc0..469b230377d 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -37,7 +37,7 @@ module Gitlab def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:) @relation_name = OVERRIDES[relation_sym] || relation_sym - @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project.id) + @relation_hash = relation_hash.except('noteable_id') @members_mapper = members_mapper @user = user @project = project @@ -58,22 +58,21 @@ module Gitlab private def setup_models - if @relation_name == :notes - set_note_author - - # attachment is deprecated and note uploads are handled by Markdown uploader - @relation_hash['attachment'] = nil + case @relation_name + when :merge_request_diff then setup_st_diff_commits + 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 + else + @relation_hash['project_id'] = @project.id end update_user_references update_project_references - handle_group_label if group_label? reset_tokens! remove_encrypted_attributes! - - set_st_diff_commits if @relation_name == :merge_request_diff - set_diff if @relation_name == :merge_request_diff_files end def update_user_references @@ -84,6 +83,12 @@ module Gitlab end end + def setup_note + set_note_author + # attachment is deprecated and note uploads are handled by Markdown uploader + @relation_hash['attachment'] = nil + end + # Sets the author for a note. If the user importing the project # has admin access, an actual mapping with new project members # will be used. Otherwise, a note stating the original author name @@ -136,11 +141,9 @@ module Gitlab @relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id'] end - def group_label? - @relation_hash['type'] == 'GroupLabel' - end + def setup_label + return unless @relation_hash['type'] == 'GroupLabel' - def handle_group_label # If there's no group, move the label to a project label if @relation_hash['group_id'] @relation_hash['project_id'] = nil @@ -150,6 +153,14 @@ module Gitlab end end + def setup_milestone + if @relation_hash['group_id'] + @relation_hash['group_id'] = @project.group.id + else + @relation_hash['project_id'] = @project.id + end + end + def reset_tokens! return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name.to_s) @@ -198,14 +209,14 @@ module Gitlab relation_class: relation_class) end - def set_st_diff_commits + def setup_st_diff_commits @relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs') HashUtil.deep_symbolize_array!(@relation_hash['st_diffs']) HashUtil.deep_symbolize_array_with_date!(@relation_hash['st_commits']) end - def set_diff + def setup_diff @relation_hash['diff'] = @relation_hash.delete('utf8_diff') end @@ -250,7 +261,13 @@ module Gitlab end def find_or_create_object! - finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] + 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? diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index e68160c8faf..7c02c9c5c48 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -33,7 +33,6 @@ module Gitlab explore favicon.ico files - google_api groups health_check help diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index 732fbf68dad..ae136202f0c 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -1,9 +1,9 @@ module Gitlab class ProjectTemplate - attr_reader :title, :name + attr_reader :title, :name, :description, :preview - def initialize(name, title) - @name, @title = name, title + def initialize(name, title, description, preview) + @name, @title, @description, @preview = name, title, description, preview end alias_method :logo, :name @@ -25,9 +25,9 @@ module Gitlab end TEMPLATES_TABLE = [ - ProjectTemplate.new('rails', 'Ruby on Rails'), - ProjectTemplate.new('spring', 'Spring'), - ProjectTemplate.new('express', 'NodeJS Express') + ProjectTemplate.new('rails', 'Ruby on Rails', 'Includes an MVC structure, gemfile, rakefile, and .gitlab-ci.yml file, along with many others, to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/rails'), + ProjectTemplate.new('spring', 'Spring', 'Includes an MVC structure, mvnw, pom.xml, and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/spring'), + ProjectTemplate.new('express', 'NodeJS Express', 'Includes an MVC structure and .gitlab-ci.yml file to help you get started.', 'https://gitlab.com/gitlab-org/project-templates/express') ].freeze class << self diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index f200c694562..58d5b0da1c4 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -103,11 +103,16 @@ module Gitlab end def send_git_diff(repository, diff_refs) - params = { - 'RepoPath' => repository.path_to_repo, - 'ShaFrom' => diff_refs.base_sha, - 'ShaTo' => diff_refs.head_sha - } + params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff) + { + 'GitalyServer' => gitaly_server_hash(repository), + 'RawDiffRequest' => Gitaly::RawDiffRequest.new( + gitaly_diff_or_patch_hash(repository, diff_refs) + ).to_json + } + else + workhorse_diff_or_patch_hash(repository, diff_refs) + end [ SEND_DATA_HEADER, @@ -116,11 +121,16 @@ module Gitlab end def send_git_patch(repository, diff_refs) - params = { - 'RepoPath' => repository.path_to_repo, - 'ShaFrom' => diff_refs.base_sha, - 'ShaTo' => diff_refs.head_sha - } + params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch) + { + 'GitalyServer' => gitaly_server_hash(repository), + 'RawPatchRequest' => Gitaly::RawPatchRequest.new( + gitaly_diff_or_patch_hash(repository, diff_refs) + ).to_json + } + else + workhorse_diff_or_patch_hash(repository, diff_refs) + end [ SEND_DATA_HEADER, @@ -216,6 +226,22 @@ module Gitlab token: Gitlab::GitalyClient.token(repository.project.repository_storage) } end + + def workhorse_diff_or_patch_hash(repository, diff_refs) + { + 'RepoPath' => repository.path_to_repo, + 'ShaFrom' => diff_refs.base_sha, + 'ShaTo' => diff_refs.head_sha + } + end + + def gitaly_diff_or_patch_hash(repository, diff_refs) + { + repository: repository.gitaly_repository, + left_commit_id: diff_refs.base_sha, + right_commit_id: diff_refs.head_sha + } + end end end end diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb index 996286430b9..b00e925986b 100644 --- a/qa/qa/runtime/namespace.rb +++ b/qa/qa/runtime/namespace.rb @@ -8,7 +8,7 @@ module QA end def name - 'qa_test_' + time.strftime('%d_%m_%Y_%H-%M-%S') + 'qa-test-' + time.strftime('%d-%m-%Y-%H-%M-%S') end def sandbox_name diff --git a/rubocop/cop/migration/datetime.rb b/rubocop/cop/migration/datetime.rb index 651935dd53e..9cba3c35b26 100644 --- a/rubocop/cop/migration/datetime.rb +++ b/rubocop/cop/migration/datetime.rb @@ -7,14 +7,18 @@ module RuboCop class Datetime < RuboCop::Cop::Cop include MigrationHelpers - MSG = 'Do not use the `datetime` data type, use `datetime_with_timezone` instead'.freeze + MSG = 'Do not use the `%s` data type, use `datetime_with_timezone` instead'.freeze # Check methods in table creation. def on_def(node) return unless in_migration?(node) node.each_descendant(:send) do |send_node| - add_offense(send_node, :selector) if method_name(send_node) == :datetime + method_name = node.children[1] + + if method_name == :datetime || method_name == :timestamp + add_offense(send_node, :selector, format(MSG, method_name)) + end end end @@ -23,12 +27,14 @@ module RuboCop return unless in_migration?(node) node.each_descendant do |descendant| - add_offense(node, :expression) if descendant.type == :sym && descendant.children.last == :datetime - end - end + next unless descendant.type == :sym - def method_name(node) - node.children[1] + last_argument = descendant.children.last + + if last_argument == :datetime || last_argument == :timestamp + add_offense(node, :expression, format(MSG, last_argument)) + end + end end end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 5e0b57e9b2e..3b3b63444c7 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -62,7 +62,7 @@ describe Projects::BranchesController do let(:branch) { "feature%2Ftest" } let(:ref) { "<script>alert('ref');</script>" } it { is_expected.to render_template('new') } - it { project.repository.branch_names.include?('feature/test') } + it { project.repository.branch_exists?('feature/test') } end end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 2c732aaf4ed..7c4a22c94c2 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -73,6 +73,12 @@ FactoryGirl.define do merge_user author end + trait :remove_source_branch do + merge_params do + { 'force_remove_source_branch' => '1' } + end + end + after(:build) do |merge_request| target_project = merge_request.target_project source_project = merge_request.source_project diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index 4c120b09345..1cb78410957 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -1,14 +1,15 @@ require 'spec_helper' feature 'Dashboard Groups page', :js do - let!(:user) { create :user } - let!(:group) { create(:group) } - let!(:nested_group) { create(:group, :nested) } - let!(:another_group) { create(:group) } + let(:user) { create :user } + let(:group) { create(:group) } + let(:nested_group) { create(:group, :nested) } + let(:another_group) { create(:group) } it 'shows groups user is member of' do group.add_owner(user) nested_group.add_owner(user) + expect(another_group).to be_persisted sign_in(user) visit dashboard_groups_path @@ -22,6 +23,7 @@ feature 'Dashboard Groups page', :js do before do group.add_owner(user) nested_group.add_owner(user) + expect(another_group).to be_persisted sign_in(user) @@ -51,7 +53,7 @@ feature 'Dashboard Groups page', :js do end end - describe 'group with subgroups' do + describe 'group with subgroups', :nested_groups do let!(:subgroup) { create(:group, :public, parent: group) } before do @@ -90,7 +92,8 @@ feature 'Dashboard Groups page', :js do end describe 'when using pagination' do - let(:group2) { create(:group) } + let(:group) { create(:group, created_at: 5.days.ago) } + let(:group2) { create(:group, created_at: 2.days.ago) } before do group.add_owner(user) @@ -102,12 +105,9 @@ feature 'Dashboard Groups page', :js do visit dashboard_groups_path end - it 'shows pagination' do - expect(page).to have_selector('.gl-pagination') + it 'loads results for next page' do expect(page).to have_selector('.gl-pagination .page', count: 2) - end - it 'loads results for next page' do # Check first page expect(page).to have_content(group2.full_name) expect(page).to have_selector("#group-#{group2.id}") diff --git a/spec/features/explore/user_explores_projects_spec.rb b/spec/features/explore/user_explores_projects_spec.rb new file mode 100644 index 00000000000..6ac9497b024 --- /dev/null +++ b/spec/features/explore/user_explores_projects_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe 'User explores projects' do + set(:archived_project) { create(:project, :archived) } + set(:internal_project) { create(:project, :internal) } + set(:private_project) { create(:project, :private) } + set(:public_project) { create(:project, :public) } + + shared_examples_for 'shows public projects' do + it 'shows projects' do + expect(page).to have_content(public_project.title) + expect(page).not_to have_content(internal_project.title) + expect(page).not_to have_content(private_project.title) + expect(page).not_to have_content(archived_project.title) + end + end + + shared_examples_for 'shows public and internal projects' do + it 'shows projects' do + expect(page).to have_content(public_project.title) + expect(page).to have_content(internal_project.title) + expect(page).not_to have_content(private_project.title) + expect(page).not_to have_content(archived_project.title) + end + end + + context 'when not signed in' do + context 'when viewing public projects' do + before do + visit(explore_projects_path) + end + + include_examples 'shows public projects' + end + end + + context 'when signed in' do + set(:user) { create(:user) } + + before do + sign_in(user) + end + + context 'when viewing public projects' do + before do + visit(explore_projects_path) + end + + include_examples 'shows public and internal projects' + end + + context 'when viewing most starred projects' do + before do + visit(starred_explore_projects_path) + end + + include_examples 'shows public and internal projects' + end + + context 'when viewing trending projects' do + before do + [archived_project, public_project].each { |project| create(:note_on_issue, project: project) } + + TrendingProject.refresh! + + visit(trending_explore_projects_path) + end + + include_examples 'shows public projects' + end + end +end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index 56144d17d4f..12aa54a3da1 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -18,6 +18,27 @@ feature 'Group milestones', :js do visit new_group_milestone_path(group) end + it 'renders description preview' do + form = find('.gfm-form') + + form.fill_in(:milestone_description, with: '') + + click_link('Preview') + + preview = find('.js-md-preview') + + expect(preview).to have_content('Nothing to preview.') + + click_link('Write') + + form.fill_in(:milestone_description, with: ':+1: Nice') + + click_link('Preview') + + expect(preview).to have_css('gl-emoji') + expect(find('#milestone_description', visible: false)).not_to be_visible + end + it 'creates milestone with start date' do fill_in 'Title', with: 'testing' find('#milestone_start_date').click diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index b475bcb3921..2b580ca1809 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -88,14 +88,24 @@ feature 'Diff notes resolve', :js do end end - it 'hides resolved discussion' do - page.within '.diff-content' do - click_button 'Resolve discussion' + describe 'resolved discussion' do + before do + page.within '.diff-content' do + click_button 'Resolve discussion' + end + + visit_merge_request end - visit_merge_request + it 'hides when resolve discussion is clicked' do + expect(page).to have_selector('.discussion-body', visible: false) + end - expect(page).to have_selector('.discussion-body', visible: false) + it 'shows resolved discussion when toggled' do + find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click + + expect(page.find(".timeline-content #note_#{note.noteable_id}")).to be_visible + end end it 'allows user to resolve from reply form without a comment' do diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index e32bbaf517b..21bbf82be77 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -44,12 +44,8 @@ feature 'Diffs URL', :js do visit "#{diffs_project_merge_request_path(project, merge_request)}#{fragment}" end - it 'shows collapsed note' do - wait_for_requests - - expect(page).to have_selector('.discussion-notes.collapsed') do |note_container| - expect(note_container).to have_selector(fragment, visible: false) - end + it 'shows expanded note' do + expect(page).to have_selector(fragment, visible: true) end end end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index c928459f911..026aa03f7cf 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -27,6 +27,7 @@ feature 'Import/Export - project import integration test', :js do select2(namespace.id, from: '#project_namespace_id') fill_in :project_path, with: project_path, visible: true + click_import_project_tab click_link 'GitLab export' expect(page).to have_content('Import an exported GitLab project') @@ -51,6 +52,7 @@ feature 'Import/Export - project import integration test', :js do context 'path is not prefilled' do scenario 'user imports an exported project successfully' do visit new_project_path + click_import_project_tab click_link 'GitLab export' fill_in :path, with: 'test-project-path', visible: true @@ -72,6 +74,7 @@ feature 'Import/Export - project import integration test', :js do select2(user.namespace.id, from: '#project_namespace_id') fill_in :project_path, with: project.name, visible: true + click_import_project_tab click_link 'GitLab export' attach_file('file', file) click_on 'Import project' @@ -81,19 +84,6 @@ feature 'Import/Export - project import integration test', :js do end end - context 'when limited to the default user namespace' do - scenario 'passes correct namespace ID in the URL' do - visit new_project_path - - fill_in :project_path, with: 'test-project-path', visible: true - - click_link 'GitLab export' - - expect(page).to have_content('GitLab project export') - expect(URI.parse(current_url).query).to eq("namespace_id=#{user.namespace.id}&path=test-project-path") - end - end - def wiki_exists?(project) wiki = ProjectWiki.new(project) File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty? @@ -102,4 +92,8 @@ feature 'Import/Export - project import integration test', :js do def project_hook_exists?(project) Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository).exists? end + + def click_import_project_tab + find('#import-project-tab').trigger('click') + end end diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 62b23121c5a..9f67216705d 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -118,7 +118,7 @@ feature 'issuable templates', :js do context 'user creates a merge request from a forked project using templates' do let(:template_content) { 'this is a test "feature-proposal" template' } let(:fork_user) { create(:user) } - let(:forked_project) { fork_project(project, fork_user) } + let(:forked_project) { fork_project(project, fork_user, repository: true) } let(:merge_request) { create(:merge_request, :with_diffs, source_project: forked_project, target_project: project) } background do diff --git a/spec/features/projects/issues/list_spec.rb b/spec/features/projects/issues/list_spec.rb deleted file mode 100644 index 9fc03f49f5b..00000000000 --- a/spec/features/projects/issues/list_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -feature 'Issues List' do - let(:user) { create(:user) } - let(:project) { create(:project) } - - background do - project.team << [user, :developer] - - sign_in(user) - end - - scenario 'user does not see create new list button' do - create(:issue, project: project) - - visit project_issues_path(project) - - expect(page).not_to have_selector('.js-new-board-list') - end -end diff --git a/spec/features/projects/issues/user_views_issues_spec.rb b/spec/features/projects/issues/user_views_issues_spec.rb new file mode 100644 index 00000000000..d35009b8974 --- /dev/null +++ b/spec/features/projects/issues/user_views_issues_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe 'User views issues' do + set(:user) { create(:user) } + + shared_examples_for 'shows issues' do + it 'shows issues' do + expect(page).to have_content(project.name) + .and have_content(issue1.title) + .and have_content(issue2.title) + .and have_no_selector('.js-new-board-list') + end + end + + context 'when project is public' do + set(:project) { create(:project_empty_repo, :public) } + set(:issue1) { create(:issue, project: project) } + set(:issue2) { create(:issue, project: project) } + + context 'when signed in' do + before do + project.add_developer(user) + sign_in(user) + + visit(project_issues_path(project)) + end + + include_examples 'shows issues' + end + + context 'when not signed in' do + before do + visit(project_issues_path(project)) + end + + include_examples 'shows issues' + end + end + + context 'when project is internal' do + set(:project) { create(:project_empty_repo, :internal) } + set(:issue1) { create(:issue, project: project) } + set(:issue2) { create(:issue, project: project) } + + context 'when signed in' do + before do + project.add_developer(user) + sign_in(user) + + visit(project_issues_path(project)) + end + + include_examples 'shows issues' + end + end +end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index d265191e0c6..171b001aa7a 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -300,13 +300,13 @@ feature 'Jobs' do shared_examples 'expected variables behavior' do it 'shows variable key and value after click', :js do - expect(page).to have_css('.reveal-variables') + expect(page).to have_css('.js-reveal-variables') expect(page).not_to have_css('.js-build-variable') expect(page).not_to have_css('.js-build-value') click_button 'Reveal Variables' - expect(page).not_to have_css('.reveal-variables') + expect(page).not_to have_css('.js-reveal-variables') expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1') expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1') end diff --git a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb index 8970cf54457..3aac93eaf7c 100644 --- a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb +++ b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb @@ -6,7 +6,7 @@ describe 'User views an open merge request' do end context 'when a merge request does not have repository' do - let(:project) { create(:project, :public) } + let(:project) { create(:project, :public, :repository) } before do visit(merge_request_path(merge_request)) diff --git a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb index 07b8c1ef479..bf95dbb7d09 100644 --- a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb +++ b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb @@ -1,72 +1,115 @@ require 'spec_helper' describe 'User views open merge requests' do - let(:project) { create(:project, :public, :repository) } + set(:user) { create(:user) } - context "when the target branch is the project's default branch" do - let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } - let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) } - - before do - visit(project_merge_requests_path(project)) + shared_examples_for 'shows merge requests' do + it 'shows merge requests' do + expect(page).to have_content(project.name).and have_content(merge_request.source_project.name) end + end - it 'shows open merge requests' do - expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title) - end + context 'when project is public' do + set(:project) { create(:project, :public, :repository) } - it 'does not show target branch name' do - expect(page).to have_content(merge_request.title) - expect(find('.issuable-info')).not_to have_content(project.default_branch) - end - end + context 'when not signed in' do + context "when the target branch is the project's default branch" do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) } - context "when the target branch is different from the project's default branch" do - let!(:merge_request) do - create(:merge_request, - source_project: project, - target_project: project, - source_branch: 'fix', - target_branch: 'feature_conflict') - end + before do + visit(project_merge_requests_path(project)) + end - before do - visit(project_merge_requests_path(project)) - end + include_examples 'shows merge requests' - it 'shows target branch name' do - expect(page).to have_content(merge_request.target_branch) - end - end + it 'shows open merge requests' do + expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title) + end - context 'when a merge request has pipelines' do - let!(:build) { create :ci_build, pipeline: pipeline } + it 'does not show target branch name' do + expect(page).to have_content(merge_request.title) + expect(find('.issuable-info')).not_to have_content(project.default_branch) + end + end - let(:merge_request) do - create(:merge_request_with_diffs, - source_project: project, - target_project: project, - source_branch: 'merge-test') - end + context "when the target branch is different from the project's default branch" do + let!(:merge_request) do + create(:merge_request, + source_project: project, + target_project: project, + source_branch: 'fix', + target_branch: 'feature_conflict') + end + + before do + visit(project_merge_requests_path(project)) + end + + it 'shows target branch name' do + expect(page).to have_content(merge_request.target_branch) + end + end - let(:pipeline) do - create(:ci_pipeline, - project: project, - sha: merge_request.diff_head_sha, - ref: merge_request.source_branch, - head_pipeline_of: merge_request) + context 'when a merge request has pipelines' do + let!(:build) { create :ci_build, pipeline: pipeline } + + let(:merge_request) do + create(:merge_request_with_diffs, + source_project: project, + target_project: project, + source_branch: 'merge-test') + end + + let(:pipeline) do + create(:ci_pipeline, + project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + head_pipeline_of: merge_request) + end + + before do + project.enable_ci + + visit(project_merge_requests_path(project)) + end + + it 'shows pipeline status' do + page.within('.mr-list') do + expect(page).to have_link('Pipeline: pending') + end + end + end end - before do - project.enable_ci + context 'when signed in' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + + before do + project.add_developer(user) + sign_in(user) - visit(project_merge_requests_path(project)) + visit(project_merge_requests_path(project)) + end + + include_examples 'shows merge requests' end + end - it 'shows pipeline status' do - page.within('.mr-list') do - expect(page).to have_link('Pipeline: pending') + context 'when project is internal' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + set(:project) { create(:project, :internal, :repository) } + + context 'when signed in' do + before do + project.add_developer(user) + sign_in(user) + + visit(project_merge_requests_path(project)) end + + include_examples 'shows merge requests' end end end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index cd3dc72d3c6..8e11cb94350 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -9,12 +9,14 @@ feature 'New project' do sign_in(user) end - it 'shows "New project" page' do + it 'shows "New project" page', :js do visit new_project_path expect(page).to have_content('Project path') expect(page).to have_content('Project name') + find('#import-project-tab').trigger('click') + expect(page).to have_link('GitHub') expect(page).to have_link('Bitbucket') expect(page).to have_link('GitLab.com') @@ -23,14 +25,15 @@ feature 'New project' do expect(page).to have_link('GitLab export') end - context 'Visibility level selector' do + context 'Visibility level selector', :js do Gitlab::VisibilityLevel.options.each do |key, level| it "sets selector to #{key}" do stub_application_setting(default_project_visibility: level) visit new_project_path - - expect(find_field("project_visibility_level_#{level}")).to be_checked + page.within('#blank-project-pane') do + expect(find_field("project_visibility_level_#{level}")).to be_checked + end end it "saves visibility level #{level} on validation error" do @@ -38,8 +41,9 @@ feature 'New project' do choose(s_(key)) click_button('Create project') - - expect(find_field("project_visibility_level_#{level}")).to be_checked + page.within('#blank-project-pane') do + expect(find_field("project_visibility_level_#{level}")).to be_checked + end end end end @@ -51,9 +55,11 @@ feature 'New project' do end it 'selects the user namespace' do - namespace = find('#project_namespace_id') + page.within('#blank-project-pane') do + namespace = find('#project_namespace_id') - expect(namespace.text).to eq user.username + expect(namespace.text).to eq user.username + end end end @@ -66,9 +72,11 @@ feature 'New project' do end it 'selects the group namespace' do - namespace = find('#project_namespace_id option[selected]') + page.within('#blank-project-pane') do + namespace = find('#project_namespace_id option[selected]') - expect(namespace.text).to eq group.name + expect(namespace.text).to eq group.name + end end end @@ -82,9 +90,11 @@ feature 'New project' do end it 'selects the group namespace' do - namespace = find('#project_namespace_id option[selected]') + page.within('#blank-project-pane') do + namespace = find('#project_namespace_id option[selected]') - expect(namespace.text).to eq subgroup.full_path + expect(namespace.text).to eq subgroup.full_path + end end end @@ -124,9 +134,10 @@ feature 'New project' do end end - context 'Import project options' do + context 'Import project options', :js do before do visit new_project_path + find('#import-project-tab').trigger('click') end context 'from git repository url' do diff --git a/spec/features/projects/user_views_details_spec.rb b/spec/features/projects/user_views_details_spec.rb new file mode 100644 index 00000000000..ffc063654cd --- /dev/null +++ b/spec/features/projects/user_views_details_spec.rb @@ -0,0 +1,151 @@ +require 'spec_helper' + +describe 'User views details' do + set(:user) { create(:user) } + + shared_examples_for 'redirects to the sign in page' do + it 'redirects to the sign in page' do + expect(current_path).to eq(new_user_session_path) + end + end + + shared_examples_for 'shows details of empty project' do + let(:user_has_ssh_key) { false } + + it 'shows details' do + expect(page).not_to have_content('Git global setup') + + page.all(:css, '.git-empty .clone').each do |element| + expect(element.text).to include(project.http_url_to_repo) + end + + expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key + end + end + + shared_examples_for 'shows details of non empty project' do + let(:user_has_ssh_key) { false } + + it 'shows details' do + page.within('.breadcrumbs .breadcrumb-item-text') do + expect(page).to have_content(project.title) + end + + expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key + end + end + + context 'when project is public' do + context 'when project is empty' do + set(:project) { create(:project_empty_repo, :public) } + + context 'when not signed in' do + before do + visit(project_path(project)) + end + + include_examples 'shows details of empty project' + end + + context 'when signed in' do + before do + sign_in(user) + end + + context 'when user does not have ssh keys' do + before do + visit(project_path(project)) + end + + include_examples 'shows details of empty project' + end + + context 'when user has ssh keys' do + before do + create(:personal_key, user: user) + + visit(project_path(project)) + end + + include_examples 'shows details of empty project' do + let(:user_has_ssh_key) { true } + end + end + end + end + + context 'when project is not empty' do + set(:project) { create(:project, :public, :repository) } + + before do + visit(project_path(project)) + end + + context 'when not signed in' do + before do + allow(Gitlab.config.gitlab).to receive(:host).and_return('www.example.com') + end + + include_examples 'shows details of non empty project' + end + + context 'when signed in' do + before do + sign_in(user) + end + + context 'when user does not have ssh keys' do + before do + visit(project_path(project)) + end + + include_examples 'shows details of non empty project' + end + + context 'when user has ssh keys' do + before do + create(:personal_key, user: user) + + visit(project_path(project)) + end + + include_examples 'shows details of non empty project' do + let(:user_has_ssh_key) { true } + end + end + end + end + end + + context 'when project is internal' do + set(:project) { create(:project, :internal, :repository) } + + context 'when not signed in' do + before do + visit(project_path(project)) + end + + include_examples 'redirects to the sign in page' + end + + context 'when signed in' do + before do + sign_in(user) + + visit(project_path(project)) + end + + include_examples 'shows details of non empty project' + end + end + + context 'when project is private' do + set(:project) { create(:project, :private) } + + before do + visit(project_path(project)) + end + + include_examples 'redirects to the sign in page' + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index ac62280e4ca..3bc7ec3123f 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -12,8 +12,9 @@ feature 'Project' do visit new_project_path end - it "allows creation from templates" do - page.choose(template.name) + it "allows creation from templates", :js do + find('#create-from-template-tab').trigger('click') + find("##{template.name}").trigger('click') fill_in("project_path", with: template.name) page.within '#content-body' do diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 485b0b287ad..2dc3c5e3927 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' feature 'Task Lists' do include Warden::Test::Helpers - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 10bc5f2ecd2..87ae1fa5660 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -57,15 +57,17 @@ describe ApplicationHelper do end describe 'project_icon' do + let(:asset_host) { 'http://assets' } + it 'returns an url for the avatar' do - project = create(:project, avatar: File.open(uploaded_image_temp_path)) + project = create(:project, :public, avatar: File.open(uploaded_image_temp_path)) avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon(project.full_path).to_s) .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />" - allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) - avatar_url = "#{gitlab_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif" + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + avatar_url = "#{asset_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon(project.full_path).to_s) .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />" diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 76e5964ccf7..97f0ed4904e 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe GroupsHelper do include ApplicationHelper + let(:asset_host) { 'http://assets' } + describe 'group_icon' do avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') @@ -10,14 +12,53 @@ describe GroupsHelper do group = create(:group) group.avatar = fixture_file_upload(avatar_file_path) group.save! - expect(group_icon(group.path).to_s) + + avatar_url = "/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif" + + expect(helper.group_icon(group).to_s) + .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />" + + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + avatar_url = "#{asset_host}/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif" + + expect(helper.group_icon(group).to_s) + .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />" + end + end + + describe 'group_icon_url' do + avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') + + it 'returns an url for the avatar' do + group = create(:group) + group.avatar = fixture_file_upload(avatar_file_path) + group.save! + expect(group_icon_url(group.path).to_s) + .to match("/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif") + end + + it 'returns an CDN url for the avatar' do + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + group = create(:group) + group.avatar = fixture_file_upload(avatar_file_path) + group.save! + expect(group_icon_url(group.path).to_s) + .to match("#{asset_host}/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif") + end + + it 'returns an based url for the avatar if private' do + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + group = create(:group, :private) + group.avatar = fixture_file_upload(avatar_file_path) + group.save! + expect(group_icon_url(group.path).to_s) .to match("/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif") end it 'gives default avatar_icon when no avatar is present' do group = create(:group) group.save! - expect(group_icon(group.path)).to match_asset_path('group_avatar.png') + expect(group_icon_url(group.path)).to match_asset_path('group_avatar.png') end end diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index a22b71fd1dc..268b5b83b73 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -28,7 +28,7 @@ import '~/lib/utils/common_utils'; preloadFixtures('merge_requests/diff_comment.html.raw'); beforeEach(function(done) { loadFixtures('merge_requests/diff_comment.html.raw'); - $('body').data('page', 'projects:merge_requests:show'); + $('body').attr('data-page', 'projects:merge_requests:show'); loadAwardsHandler(true).then((obj) => { awardsHandler = obj; spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb()); @@ -55,6 +55,9 @@ import '~/lib/utils/common_utils'; // restore original url root value gon.relative_url_root = urlRoot; + // Undo what we did to the shared <body> + $('body').removeAttr('data-page'); + awardsHandler.destroy(); }); describe('::showEmojiMenu', function() { diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index f62bf43adb9..d5300d9c63d 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -19,6 +19,11 @@ describe('Quick Submit behavior', () => { this.textarea = $('.js-quick-submit textarea').first(); }); + afterEach(() => { + // Undo what we did to the shared <body> + $('body').removeAttr('data-page'); + }); + it('does not respond to other keyCodes', () => { this.textarea.trigger(keydownEvent({ keyCode: 32, diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js new file mode 100644 index 00000000000..060ffaa339b --- /dev/null +++ b/spec/javascripts/flash_spec.js @@ -0,0 +1,269 @@ +import flash, { + createFlashEl, + createAction, + hideFlash, +} from '~/flash'; + +describe('Flash', () => { + describe('createFlashEl', () => { + let el; + + beforeEach(() => { + el = document.createElement('div'); + }); + + afterEach(() => { + el.innerHTML = ''; + }); + + it('creates flash element with type', () => { + el.innerHTML = createFlashEl('testing', 'alert'); + + expect( + el.querySelector('.flash-alert'), + ).not.toBeNull(); + }); + + it('escapes text', () => { + el.innerHTML = createFlashEl('<script>alert("a");</script>', 'alert'); + + expect( + el.querySelector('.flash-text').textContent.trim(), + ).toBe('<script>alert("a");</script>'); + }); + + it('adds container classes when inside content wrapper', () => { + el.innerHTML = createFlashEl('testing', 'alert', true); + + expect( + el.querySelector('.flash-text').classList.contains('container-fluid'), + ).toBeTruthy(); + expect( + el.querySelector('.flash-text').classList.contains('container-limited'), + ).toBeTruthy(); + }); + }); + + describe('hideFlash', () => { + let el; + + beforeEach(() => { + el = document.createElement('div'); + el.className = 'js-testing'; + }); + + it('sets transition style', () => { + hideFlash(el); + + expect( + el.style.transition, + ).toBe('opacity 0.3s'); + }); + + it('sets opacity style', () => { + hideFlash(el); + + expect( + el.style.opacity, + ).toBe('0'); + }); + + it('does not set styles when fadeTransition is false', () => { + hideFlash(el, false); + + expect( + el.style.opacity, + ).toBe(''); + expect( + el.style.transition, + ).toBe(''); + }); + + it('removes element after transitionend', () => { + document.body.appendChild(el); + + hideFlash(el); + el.dispatchEvent(new Event('transitionend')); + + expect( + document.querySelector('.js-testing'), + ).toBeNull(); + }); + + it('calls event listener callback once', () => { + spyOn(el, 'remove').and.callThrough(); + document.body.appendChild(el); + + hideFlash(el); + + el.dispatchEvent(new Event('transitionend')); + el.dispatchEvent(new Event('transitionend')); + + expect( + el.remove.calls.count(), + ).toBe(1); + }); + }); + + describe('createAction', () => { + let el; + + beforeEach(() => { + el = document.createElement('div'); + }); + + it('creates link with href', () => { + el.innerHTML = createAction({ + href: 'testing', + title: 'test', + }); + + expect( + el.querySelector('.flash-action').href, + ).toContain('testing'); + }); + + it('uses hash as href when no href is present', () => { + el.innerHTML = createAction({ + title: 'test', + }); + + expect( + el.querySelector('.flash-action').href, + ).toContain('#'); + }); + + it('adds role when no href is present', () => { + el.innerHTML = createAction({ + title: 'test', + }); + + expect( + el.querySelector('.flash-action').getAttribute('role'), + ).toBe('button'); + }); + + it('escapes the title text', () => { + el.innerHTML = createAction({ + title: '<script>alert("a")</script>', + }); + + expect( + el.querySelector('.flash-action').textContent.trim(), + ).toBe('<script>alert("a")</script>'); + }); + }); + + describe('createFlash', () => { + describe('no flash-container', () => { + it('does not add to the DOM', () => { + const flashEl = flash('testing'); + + expect( + flashEl, + ).toBeNull(); + expect( + document.querySelector('.flash-alert'), + ).toBeNull(); + }); + }); + + describe('with flash-container', () => { + beforeEach(() => { + document.body.innerHTML += ` + <div class="content-wrapper js-content-wrapper"> + <div class="flash-container"></div> + </div> + `; + }); + + afterEach(() => { + document.querySelector('.js-content-wrapper').remove(); + }); + + it('adds flash element into container', () => { + flash('test'); + + expect( + document.querySelector('.flash-alert'), + ).not.toBeNull(); + }); + + it('adds flash into specified parent', () => { + flash( + 'test', + 'alert', + document.querySelector('.content-wrapper'), + ); + + expect( + document.querySelector('.content-wrapper .flash-alert'), + ).not.toBeNull(); + }); + + it('adds container classes when inside content-wrapper', () => { + flash('test'); + + expect( + document.querySelector('.flash-text').className, + ).toBe('flash-text container-fluid container-limited'); + }); + + it('does not add container when outside of content-wrapper', () => { + document.querySelector('.content-wrapper').className = 'js-content-wrapper'; + flash('test'); + + expect( + document.querySelector('.flash-text').className.trim(), + ).toBe('flash-text'); + }); + + it('removes element after clicking', () => { + flash('test', 'alert', document, null, false); + + document.querySelector('.flash-alert').click(); + + expect( + document.querySelector('.flash-alert'), + ).toBeNull(); + }); + + describe('with actionConfig', () => { + it('adds action link', () => { + flash( + 'test', + 'alert', + document, + { + title: 'test', + }, + ); + + expect( + document.querySelector('.flash-action'), + ).not.toBeNull(); + }); + + it('calls actionConfig clickHandler on click', () => { + const actionConfig = { + title: 'test', + clickHandler: jasmine.createSpy('actionConfig'), + }; + + flash( + 'test', + 'alert', + document, + actionConfig, + ); + + document.querySelector('.flash-action').click(); + + expect( + actionConfig.clickHandler, + ).toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js index fa24aa426b6..2779686a6f5 100644 --- a/spec/javascripts/gl_field_errors_spec.js +++ b/spec/javascripts/gl_field_errors_spec.js @@ -1,110 +1,108 @@ /* eslint-disable space-before-function-paren, arrow-body-style */ -import '~/gl_field_errors'; +import GlFieldErrors from '~/gl_field_errors'; -((global) => { +describe('GL Style Field Errors', function() { preloadFixtures('static/gl_field_errors.html.raw'); - describe('GL Style Field Errors', function() { - beforeEach(function() { - loadFixtures('static/gl_field_errors.html.raw'); - const $form = this.$form = $('form.gl-show-field-errors'); - this.fieldErrors = new global.GlFieldErrors($form); - }); + beforeEach(function() { + loadFixtures('static/gl_field_errors.html.raw'); + const $form = this.$form = $('form.gl-show-field-errors'); + this.fieldErrors = new GlFieldErrors($form); + }); - it('should select the correct input elements', function() { - expect(this.$form).toBeDefined(); - expect(this.$form.length).toBe(1); - expect(this.fieldErrors).toBeDefined(); - const inputs = this.fieldErrors.state.inputs; - expect(inputs.length).toBe(4); - }); + it('should select the correct input elements', function() { + expect(this.$form).toBeDefined(); + expect(this.$form.length).toBe(1); + expect(this.fieldErrors).toBeDefined(); + const inputs = this.fieldErrors.state.inputs; + expect(inputs.length).toBe(4); + }); - it('should ignore elements with custom error handling', function() { - const customErrorFlag = 'gl-field-error-ignore'; - const customErrorElem = $(`.${customErrorFlag}`); + it('should ignore elements with custom error handling', function() { + const customErrorFlag = 'gl-field-error-ignore'; + const customErrorElem = $(`.${customErrorFlag}`); - expect(customErrorElem.length).toBe(1); + expect(customErrorElem.length).toBe(1); - const customErrors = this.fieldErrors.state.inputs.filter((input) => { - return input.inputElement.hasClass(customErrorFlag); - }); - expect(customErrors.length).toBe(0); + const customErrors = this.fieldErrors.state.inputs.filter((input) => { + return input.inputElement.hasClass(customErrorFlag); }); + expect(customErrors.length).toBe(0); + }); - it('should not show any errors before submit attempt', function() { - this.$form.find('.email').val('not-a-valid-email').keyup(); - this.$form.find('.text-required').val('').keyup(); - this.$form.find('.alphanumberic').val('?---*').keyup(); + it('should not show any errors before submit attempt', function() { + this.$form.find('.email').val('not-a-valid-email').keyup(); + this.$form.find('.text-required').val('').keyup(); + this.$form.find('.alphanumberic').val('?---*').keyup(); - const errorsShown = this.$form.find('.gl-field-error-outline'); - expect(errorsShown.length).toBe(0); - }); + const errorsShown = this.$form.find('.gl-field-error-outline'); + expect(errorsShown.length).toBe(0); + }); - it('should show errors when input valid is submitted', function() { - this.$form.find('.email').val('not-a-valid-email').keyup(); - this.$form.find('.text-required').val('').keyup(); - this.$form.find('.alphanumberic').val('?---*').keyup(); + it('should show errors when input valid is submitted', function() { + this.$form.find('.email').val('not-a-valid-email').keyup(); + this.$form.find('.text-required').val('').keyup(); + this.$form.find('.alphanumberic').val('?---*').keyup(); - this.$form.submit(); + this.$form.submit(); - const errorsShown = this.$form.find('.gl-field-error-outline'); - expect(errorsShown.length).toBe(4); - }); + const errorsShown = this.$form.find('.gl-field-error-outline'); + expect(errorsShown.length).toBe(4); + }); - it('should properly track validity state on input after invalid submission attempt', function() { - this.$form.submit(); - - const emailInputModel = this.fieldErrors.state.inputs[1]; - const fieldState = emailInputModel.state; - const emailInputElement = emailInputModel.inputElement; - - // No input - expect(emailInputElement).toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(true); - expect(fieldState.valid).toBe(false); - - // Then invalid input - emailInputElement.val('not-a-valid-email').keyup(); - expect(emailInputElement).toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(false); - expect(fieldState.valid).toBe(false); - - // Then valid input - emailInputElement.val('email@gitlab.com').keyup(); - expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(false); - expect(fieldState.valid).toBe(true); - - // Then invalid input - emailInputElement.val('not-a-valid-email').keyup(); - expect(emailInputElement).toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(false); - expect(fieldState.valid).toBe(false); - - // Then empty input - emailInputElement.val('').keyup(); - expect(emailInputElement).toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(true); - expect(fieldState.valid).toBe(false); - - // Then valid input - emailInputElement.val('email@gitlab.com').keyup(); - expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(false); - expect(fieldState.valid).toBe(true); - }); + it('should properly track validity state on input after invalid submission attempt', function() { + this.$form.submit(); + + const emailInputModel = this.fieldErrors.state.inputs[1]; + const fieldState = emailInputModel.state; + const emailInputElement = emailInputModel.inputElement; + + // No input + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(true); + expect(fieldState.valid).toBe(false); + + // Then invalid input + emailInputElement.val('not-a-valid-email').keyup(); + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(false); + + // Then valid input + emailInputElement.val('email@gitlab.com').keyup(); + expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(true); + + // Then invalid input + emailInputElement.val('not-a-valid-email').keyup(); + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(false); + + // Then empty input + emailInputElement.val('').keyup(); + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(true); + expect(fieldState.valid).toBe(false); + + // Then valid input + emailInputElement.val('email@gitlab.com').keyup(); + expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(true); + }); - it('should properly infer error messages', function() { - this.$form.submit(); - const trackedInputs = this.fieldErrors.state.inputs; - const inputHasTitle = trackedInputs[1]; - const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error'); - const inputNoTitle = trackedInputs[2]; - const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error'); + it('should properly infer error messages', function() { + this.$form.submit(); + const trackedInputs = this.fieldErrors.state.inputs; + const inputHasTitle = trackedInputs[1]; + const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error'); + const inputNoTitle = trackedInputs[2]; + const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error'); - expect(noTitleErrorElem.text()).toBe('This field is required.'); - expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.'); - }); + expect(noTitleErrorElem.text()).toBe('This field is required.'); + expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.'); }); -})(window.gl || (window.gl = {})); +}); diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js index 837feacec1d..124fc030774 100644 --- a/spec/javascripts/gl_form_spec.js +++ b/spec/javascripts/gl_form_spec.js @@ -1,18 +1,11 @@ import autosize from 'vendor/autosize'; -import '~/gl_form'; +import GLForm from '~/gl_form'; import '~/lib/utils/text_utility'; import '~/lib/utils/common_utils'; window.autosize = autosize; describe('GLForm', () => { - const global = window.gl || (window.gl = {}); - const GLForm = global.GLForm; - - it('should be defined in the global scope', () => { - expect(GLForm).toBeDefined(); - }); - describe('when instantiated', function () { beforeEach((done) => { this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>'); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index 0e01934d3a3..4751eb868a4 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,53 +1,48 @@ -/* eslint-disable space-before-function-paren, no-var */ - import '~/header'; -import '~/lib/utils/text_utility'; -(function() { - describe('Header', function() { - var todosPendingCount = '.todos-count'; - var fixtureTemplate = 'issues/open-issue.html.raw'; +describe('Header', function () { + const todosPendingCount = '.todos-count'; + const fixtureTemplate = 'issues/open-issue.html.raw'; - function isTodosCountHidden() { - return $(todosPendingCount).hasClass('hidden'); - } + function isTodosCountHidden() { + return $(todosPendingCount).hasClass('hidden'); + } - function triggerToggle(newCount) { - $(document).trigger('todo:toggle', newCount); - } + function triggerToggle(newCount) { + $(document).trigger('todo:toggle', newCount); + } - preloadFixtures(fixtureTemplate); - beforeEach(function() { - loadFixtures(fixtureTemplate); - }); + preloadFixtures(fixtureTemplate); + beforeEach(() => { + loadFixtures(fixtureTemplate); + }); - it('should update todos-count after receiving the todo:toggle event', function() { - triggerToggle(5); - expect($(todosPendingCount).text()).toEqual('5'); - }); + it('should update todos-count after receiving the todo:toggle event', () => { + triggerToggle('5'); + expect($(todosPendingCount).text()).toEqual('5'); + }); - it('should hide todos-count when it is 0', function() { - triggerToggle(0); - expect(isTodosCountHidden()).toEqual(true); + it('should hide todos-count when it is 0', () => { + triggerToggle('0'); + expect(isTodosCountHidden()).toEqual(true); + }); + + it('should show todos-count when it is more than 0', () => { + triggerToggle('10'); + expect(isTodosCountHidden()).toEqual(false); + }); + + describe('when todos-count is 1000', () => { + beforeEach(() => { + triggerToggle('1000'); }); - it('should show todos-count when it is more than 0', function() { - triggerToggle(10); + it('should show todos-count', () => { expect(isTodosCountHidden()).toEqual(false); }); - describe('when todos-count is 1000', function() { - beforeEach(function() { - triggerToggle(1000); - }); - - it('should show todos-count', function() { - expect(isTodosCountHidden()).toEqual(false); - }); - - it('should show 99+ for todos-count', function() { - expect($(todosPendingCount).text()).toEqual('99+'); - }); + it('should show 99+ for todos-count', () => { + expect($(todosPendingCount).text()).toEqual('99+'); }); }); -}).call(window); +}); diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js index 57846555261..9033eb9ce02 100644 --- a/spec/javascripts/integrations/integration_settings_form_spec.js +++ b/spec/javascripts/integrations/integration_settings_form_spec.js @@ -138,9 +138,9 @@ describe('IntegrationSettingsForm', () => { deferred.resolve({ error: true, message: errorMessage, service_response: 'some error' }); const $flashContainer = $('.flash-container'); - expect($flashContainer.find('.flash-text').text()).toEqual('Test failed. some error'); + expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error'); expect($flashContainer.find('.flash-action')).toBeDefined(); - expect($flashContainer.find('.flash-action').text()).toEqual('Save anyway'); + expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway'); }); it('should submit form if ajax request responds without any error in test', () => { @@ -168,7 +168,7 @@ describe('IntegrationSettingsForm', () => { expect($flashAction).toBeDefined(); spyOn(integrationSettingsForm.$form, 'submit'); - $flashAction.click(); + $flashAction.get(0).click(); expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); }); @@ -181,7 +181,7 @@ describe('IntegrationSettingsForm', () => { deferred.reject(); - expect($('.flash-container .flash-text').text()).toEqual(errorMessage); + expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage); }); it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => { diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index b45b0be804f..60a452f2223 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -128,7 +128,7 @@ describe('Issue', function() { }); it(`${action}s the issue`, function() { - this.$triggeredButton.click(); + this.$triggeredButton.trigger('click'); this.issueStateDeferred.resolve({ id: 34 }); @@ -143,7 +143,7 @@ describe('Issue', function() { }); it(`fails to ${action} the issue if saved:false`, function() { - this.$triggeredButton.click(); + this.$triggeredButton.trigger('click'); this.issueStateDeferred.resolve({ saved: false }); @@ -159,7 +159,7 @@ describe('Issue', function() { }); it(`fails to ${action} the issue if HTTP error occurs`, function() { - this.$triggeredButton.click(); + this.$triggeredButton.trigger('click'); this.issueStateDeferred.reject(); this.canCreateBranchDeferred.resolve({ can_create_branch: isIssueInitiallyOpen @@ -173,7 +173,7 @@ describe('Issue', function() { }); it('disables the new branch button if Ajax call fails', function() { - this.$triggeredButton.click(); + this.$triggeredButton.trigger('click'); this.issueStateDeferred.reject(); this.canCreateBranchDeferred.reject(); @@ -184,7 +184,7 @@ describe('Issue', function() { Issue.$btnNewBranch = $(); this.canCreateBranchDeferred = null; - this.$triggeredButton.click(); + this.$triggeredButton.trigger('click'); this.issueStateDeferred.reject(); }); }); diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/job_spec.js index d5b0f23e7b7..5e67911d338 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/job_spec.js @@ -1,13 +1,11 @@ -/* eslint-disable no-new */ -/* global Build */ import { bytesToKiB } from '~/lib/utils/number_utils'; import '~/lib/utils/datetime_utility'; import '~/lib/utils/url_utility'; -import '~/build'; +import Job from '~/job'; import '~/breakpoints'; -describe('Build', () => { - const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; +describe('Job', () => { + const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; preloadFixtures('builds/build-with-artifacts.html.raw'); @@ -26,14 +24,14 @@ describe('Build', () => { describe('setup', () => { beforeEach(function () { - this.build = new Build(); + this.job = new Job(); }); it('copies build options', function () { - expect(this.build.pageUrl).toBe(BUILD_URL); - expect(this.build.buildStatus).toBe('success'); - expect(this.build.buildStage).toBe('test'); - expect(this.build.state).toBe(''); + expect(this.job.pageUrl).toBe(JOB_URL); + expect(this.job.buildStatus).toBe('success'); + expect(this.job.buildStage).toBe('test'); + expect(this.job.state).toBe(''); }); it('only shows the jobs matching the current stage', () => { @@ -87,15 +85,15 @@ describe('Build', () => { complete: true, }); - this.build = new Build(); + this.job = new Job(); expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - expect(this.build.state).toBe('newstate'); + expect(this.job.state).toBe('newstate'); jasmine.clock().tick(4001); expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); - expect(this.build.state).toBe('finalstate'); + expect(this.job.state).toBe('finalstate'); }); it('replaces the entire build trace', () => { @@ -122,7 +120,7 @@ describe('Build', () => { append: false, }); - this.build = new Build(); + this.job = new Job(); expect($('#build-trace .js-build-output').text()).toMatch(/Update/); @@ -148,7 +146,7 @@ describe('Build', () => { total: 100, }); - this.build = new Build(); + this.job = new Job(); expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); }); @@ -167,7 +165,7 @@ describe('Build', () => { total: 100, }); - this.build = new Build(); + this.job = new Job(); expect( document.querySelector('.js-truncated-info-size').textContent.trim(), @@ -193,7 +191,7 @@ describe('Build', () => { deferred2.resolve(); - this.build = new Build(); + this.job = new Job(); expect( document.querySelector('.js-truncated-info-size').textContent.trim(), @@ -227,7 +225,7 @@ describe('Build', () => { total: 100, }); - this.build = new Build(); + this.job = new Job(); expect( document.querySelector('.js-raw-link').textContent.trim(), @@ -249,7 +247,7 @@ describe('Build', () => { total: 100, }); - this.build = new Build(); + this.job = new Job(); expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); }); @@ -270,7 +268,7 @@ describe('Build', () => { total: 100, }); - this.build = new Build(); + this.job = new Job(); }); it('should render trace controls', () => { @@ -293,11 +291,12 @@ describe('Build', () => { describe('getBuildTrace', () => { it('should request build trace with state parameter', (done) => { spyOn(jQuery, 'ajax').and.callThrough(); - new Build(); + // eslint-disable-next-line no-new + new Job(); setTimeout(() => { expect(jQuery.ajax).toHaveBeenCalledWith( - { url: `${BUILD_URL}/trace.json`, data: { state: '' } }, + { url: `${JOB_URL}/trace.json`, data: { state: '' } }, ); done(); }, 0); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index f1a975ba962..829b3ef5735 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -1,4 +1,4 @@ -import '~/lib/utils/text_utility'; +import { highCountTrim } from '~/lib/utils/text_utility'; describe('text_utility', () => { describe('gl.text.getTextWidth', () => { @@ -35,14 +35,14 @@ describe('text_utility', () => { }); }); - describe('gl.text.highCountTrim', () => { + describe('highCountTrim', () => { it('returns 99+ for count >= 100', () => { - expect(gl.text.highCountTrim(105)).toBe('99+'); - expect(gl.text.highCountTrim(100)).toBe('99+'); + expect(highCountTrim(105)).toBe('99+'); + expect(highCountTrim(100)).toBe('99+'); }); it('returns exact number for count < 100', () => { - expect(gl.text.highCountTrim(45)).toBe(45); + expect(highCountTrim(45)).toBe(45); }); }); diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index 7ba754e672a..ac6ace48108 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -23,12 +23,17 @@ describe('Merge request notes', () => { loadFixtures(discussionTabFixture); gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; - $('body').data('page', 'projects:merge_requests:show'); + $('body').attr('data-page', 'projects:merge_requests:show'); window.gon.current_user_id = $('.note:last').data('author-id'); return new Notes('', []); }); + afterEach(() => { + // Undo what we did to the shared <body> + $('body').removeAttr('data-page'); + }); + describe('up arrow', () => { it('edits last comment when triggered in main form', () => { const upArrowEvent = $.Event('keydown'); @@ -71,12 +76,17 @@ describe('Merge request notes', () => { <textarea class="js-note-text"></textarea> </form>`; setFixtures(diffsResponse.html + noteFormHtml); - $('body').data('page', 'projects:merge_requests:show'); + $('body').attr('data-page', 'projects:merge_requests:show'); window.gon.current_user_id = $('.note:last').data('author-id'); return new Notes('', []); }); + afterEach(() => { + // Undo what we did to the shared <body> + $('body').removeAttr('data-page'); + }); + describe('up arrow', () => { it('edits last comment in discussion when triggered in discussion form', (done) => { const upArrowEvent = $.Event('keydown'); @@ -84,7 +94,7 @@ describe('Merge request notes', () => { spyOnEvent('.note:last .js-note-edit', 'click'); - $('.js-discussion-reply-button').click(); + $('.js-discussion-reply-button').trigger('click'); setTimeout(() => { $('.js-note-text').trigger(upArrowEvent); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index ccdbfcba692..18916c5aa97 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -277,7 +277,7 @@ import 'vendor/jquery.scrollTo'; describe('loadDiff', function () { beforeEach(() => { loadFixtures('merge_requests/diff_comment.html.raw'); - spyOn(window.gl.utils, 'getPagePath').and.returnValue('merge_requests'); + $('body').attr('data-page', 'projects:merge_requests:show'); window.gl.ImageFile = () => {}; window.notes = new Notes('', []); spyOn(window.notes, 'toggleDiffNote').and.callThrough(); @@ -286,6 +286,9 @@ import 'vendor/jquery.scrollTo'; afterEach(() => { delete window.gl.ImageFile; delete window.notes; + + // Undo what we did to the shared <body> + $('body').removeAttr('data-page'); }); it('requires an absolute pathname', function () { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 3e791a31604..66c52611614 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -39,7 +39,12 @@ import '~/notes'; loadFixtures(commentsTemplate); gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; - $('body').data('page', 'projects:merge_requets:show'); + $('body').attr('data-page', 'projects:merge_requets:show'); + }); + + afterEach(() => { + // Undo what we did to the shared <body> + $('body').removeAttr('data-page'); }); describe('task lists', function() { @@ -426,19 +431,17 @@ import '~/notes'; }); describe('putEditFormInPlace', () => { - it('should call gl.GLForm with GFM parameter passed through', () => { - spyOn(gl, 'GLForm'); - - const $el = jasmine.createSpyObj('$form', ['find', 'closest']); - $el.find.and.returnValue($('<div>')); - $el.closest.and.returnValue($('<div>')); + it('should call GLForm with GFM parameter passed through', () => { + const notes = new Notes('', []); + const $el = $(` + <div> + <form></form> + </div> + `); - Notes.prototype.putEditFormInPlace.call({ - getEditFormSelector: () => '', - enableGFM: true - }, $el); + notes.putEditFormInPlace($el); - expect(gl.GLForm).toHaveBeenCalledWith(jasmine.any(Object), true); + expect(notes.glForm.enableGFM).toBeTruthy(); }); }); @@ -815,7 +818,7 @@ import '~/notes'; }); it('shows a flash message', () => { - this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline); + this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0)); expect($('.flash-alert').is(':visible')).toBeTruthy(); }); @@ -828,7 +831,7 @@ import '~/notes'; }); it('hides visible flash message', () => { - this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline); + this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0)); this.notes.clearFlash(); diff --git a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js index e90c20e20d2..5b316b319a5 100644 --- a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js +++ b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js @@ -92,7 +92,7 @@ describe('Pipeline Variable List', () => { }); it('should remove the row when clicking the remove button', () => { - $markup.find('.js-row-remove-button').click(); + $markup.find('.js-row-remove-button').trigger('click'); expect($markup.find('.js-row').length).toBe(0); }); diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js index 2b3a821dbd9..b24567ffc0c 100644 --- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js +++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js @@ -109,12 +109,16 @@ describe('PrometheusMetrics', () => { it('should show loader animation while response is being loaded and hide it when request is complete', (done) => { const deferred = $.Deferred(); - spyOn($, 'getJSON').and.returnValue(deferred.promise()); + spyOn($, 'ajax').and.returnValue(deferred.promise()); prometheusMetrics.loadActiveMetrics(); expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); - expect($.getJSON).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint); + expect($.ajax).toHaveBeenCalledWith({ + url: prometheusMetrics.activeMetricsEndpoint, + dataType: 'json', + global: false, + }); deferred.resolve({ data: metrics, success: true }); @@ -126,7 +130,7 @@ describe('PrometheusMetrics', () => { it('should show empty state if response failed to load', (done) => { const deferred = $.Deferred(); - spyOn($, 'getJSON').and.returnValue(deferred.promise()); + spyOn($, 'ajax').and.returnValue(deferred.promise()); spyOn(prometheusMetrics, 'populateActiveMetrics'); prometheusMetrics.loadActiveMetrics(); @@ -142,7 +146,7 @@ describe('PrometheusMetrics', () => { it('should populate metrics list once response is loaded', (done) => { const deferred = $.Deferred(); - spyOn($, 'getJSON').and.returnValue(deferred.promise()); + spyOn($, 'ajax').and.returnValue(deferred.promise()); spyOn(prometheusMetrics, 'populateActiveMetrics'); prometheusMetrics.loadActiveMetrics(); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index a53f58b5d0d..cf811af3d6c 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -6,7 +6,7 @@ import '~/lib/utils/common_utils'; import 'vendor/fuzzaldrin-plus'; (function() { - var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; + var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; var userName = 'root'; widget = null; @@ -29,25 +29,31 @@ import 'vendor/fuzzaldrin-plus'; groupName = 'Gitlab Org'; + const removeBodyAttributes = function() { + const $body = $('body'); + + $body.removeAttr('data-page'); + $body.removeAttr('data-project'); + $body.removeAttr('data-group'); + }; + // Add required attributes to body before starting the test. // section would be dashboard|group|project - addBodyAttributes = function(section) { - var $body; + const addBodyAttributes = function(section) { if (section == null) { section = 'dashboard'; } - $body = $('body'); - $body.removeAttr('data-page'); - $body.removeAttr('data-project'); - $body.removeAttr('data-group'); + + const $body = $('body'); + removeBodyAttributes(); switch (section) { case 'dashboard': - return $body.data('page', 'root:index'); + return $body.attr('data-page', 'root:index'); case 'group': - $body.data('page', 'groups:show'); + $body.attr('data-page', 'groups:show'); return $body.data('group', 'gitlab-org'); case 'project': - $body.data('page', 'projects:show'); + $body.attr('data-page', 'projects:show'); return $body.data('project', 'gitlab-ce'); } }; @@ -108,7 +114,7 @@ import 'vendor/fuzzaldrin-plus'; preloadFixtures('static/search_autocomplete.html.raw'); beforeEach(function() { loadFixtures('static/search_autocomplete.html.raw'); - widget = new gl.SearchAutocomplete; + // Prevent turbolinks from triggering within gl_dropdown spyOn(window.gl.utils, 'visitUrl').and.returnValue(true); @@ -120,6 +126,8 @@ import 'vendor/fuzzaldrin-plus'; }); afterEach(function() { + // Undo what we did to the shared <body> + removeBodyAttributes(); window.gon = {}; }); it('should show Dashboard specific dropdown menu', function() { diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index e924107b29b..29b15f3a782 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -1,72 +1,63 @@ -/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, max-len */ -/* global MockU2FDevice */ -/* global U2FAuthenticate */ - -import '~/u2f/authenticate'; -import '~/u2f/util'; -import '~/u2f/error'; +import U2FAuthenticate from '~/u2f/authenticate'; import 'vendor/u2f'; -import './mock_u2f_device'; +import MockU2FDevice from './mock_u2f_device'; + +describe('U2FAuthenticate', () => { + preloadFixtures('u2f/authenticate.html.raw'); -(function() { - describe('U2FAuthenticate', function() { - preloadFixtures('u2f/authenticate.html.raw'); + beforeEach(() => { + loadFixtures('u2f/authenticate.html.raw'); + this.u2fDevice = new MockU2FDevice(); + this.container = $('#js-authenticate-u2f'); + this.component = new U2FAuthenticate( + this.container, + '#js-login-u2f-form', + { + sign_requests: [], + }, + document.querySelector('#js-login-2fa-device'), + document.querySelector('.js-2fa-form'), + ); - beforeEach(function() { - loadFixtures('u2f/authenticate.html.raw'); - this.u2fDevice = new MockU2FDevice; - this.container = $("#js-authenticate-u2f"); - this.component = new window.gl.U2FAuthenticate( - this.container, - '#js-login-u2f-form', - { - sign_requests: [] - }, - document.querySelector('#js-login-2fa-device'), - document.querySelector('.js-2fa-form') - ); + // bypass automatic form submission within renderAuthenticated + spyOn(this.component, 'renderAuthenticated').and.returnValue(true); - // bypass automatic form submission within renderAuthenticated - spyOn(this.component, 'renderAuthenticated').and.returnValue(true); + return this.component.start(); + }); - return this.component.start(); + it('allows authenticating via a U2F device', () => { + const inProgressMessage = this.container.find('p'); + expect(inProgressMessage.text()).toContain('Trying to communicate with your device'); + this.u2fDevice.respondToAuthenticateRequest({ + deviceData: 'this is data from the device', }); - it('allows authenticating via a U2F device', function() { - var inProgressMessage; - inProgressMessage = this.container.find("p"); - expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); + }); + + return describe('errors', () => { + it('displays an error message', () => { + const setupButton = this.container.find('#js-login-u2f-device'); + setupButton.trigger('click'); this.u2fDevice.respondToAuthenticateRequest({ - deviceData: "this is data from the device" + errorCode: 'error!', }); - expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); + const errorMessage = this.container.find('p'); + return expect(errorMessage.text()).toContain('There was a problem communicating with your device'); }); - return describe("errors", function() { - it("displays an error message", function() { - var errorMessage, setupButton; - setupButton = this.container.find("#js-login-u2f-device"); - setupButton.click(); - this.u2fDevice.respondToAuthenticateRequest({ - errorCode: "error!" - }); - errorMessage = this.container.find("p"); - return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); + return it('allows retrying authentication after an error', () => { + let setupButton = this.container.find('#js-login-u2f-device'); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + errorCode: 'error!', }); - return it("allows retrying authentication after an error", function() { - var retryButton, setupButton; - setupButton = this.container.find("#js-login-u2f-device"); - setupButton.click(); - this.u2fDevice.respondToAuthenticateRequest({ - errorCode: "error!" - }); - retryButton = this.container.find("#js-u2f-try-again"); - retryButton.click(); - setupButton = this.container.find("#js-login-u2f-device"); - setupButton.click(); - this.u2fDevice.respondToAuthenticateRequest({ - deviceData: "this is data from the device" - }); - expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); + const retryButton = this.container.find('#js-u2f-try-again'); + retryButton.trigger('click'); + setupButton = this.container.find('#js-login-u2f-device'); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + deviceData: 'this is data from the device', }); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); }); -}).call(window); +}); diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index 4eb8ad3d9e4..5a1ace2b4d6 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -1,31 +1,28 @@ -/* eslint-disable space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, max-len */ +/* eslint-disable prefer-rest-params, wrap-iife, +no-unused-expressions, no-return-assign, no-param-reassign*/ -(function() { - this.MockU2FDevice = (function() { - function MockU2FDevice() { - this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this); - this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this); - window.u2f || (window.u2f = {}); - window.u2f.register = (function(_this) { - return function(appId, registerRequests, signRequests, callback) { - return _this.registerCallback = callback; - }; - })(this); - window.u2f.sign = (function(_this) { - return function(appId, challenges, signRequests, callback) { - return _this.authenticateCallback = callback; - }; - })(this); - } +export default class MockU2FDevice { + constructor() { + this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this); + this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this); + window.u2f || (window.u2f = {}); + window.u2f.register = (function (_this) { + return function (appId, registerRequests, signRequests, callback) { + return _this.registerCallback = callback; + }; + })(this); + window.u2f.sign = (function (_this) { + return function (appId, challenges, signRequests, callback) { + return _this.authenticateCallback = callback; + }; + })(this); + } - MockU2FDevice.prototype.respondToRegisterRequest = function(params) { - return this.registerCallback(params); - }; + respondToRegisterRequest(params) { + return this.registerCallback(params); + } - MockU2FDevice.prototype.respondToAuthenticateRequest = function(params) { - return this.authenticateCallback(params); - }; - - return MockU2FDevice; - })(); -}).call(window); + respondToAuthenticateRequest(params) { + return this.authenticateCallback(params); + } +} diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index f5e32c1b358..b0051f11362 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -1,77 +1,69 @@ -/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, max-len */ -/* global MockU2FDevice */ -/* global U2FRegister */ - -import '~/u2f/register'; -import '~/u2f/util'; -import '~/u2f/error'; +import U2FRegister from '~/u2f/register'; import 'vendor/u2f'; -import './mock_u2f_device'; +import MockU2FDevice from './mock_u2f_device'; + +describe('U2FRegister', () => { + preloadFixtures('u2f/register.html.raw'); -(function() { - describe('U2FRegister', function() { - preloadFixtures('u2f/register.html.raw'); + beforeEach(() => { + loadFixtures('u2f/register.html.raw'); + this.u2fDevice = new MockU2FDevice(); + this.container = $('#js-register-u2f'); + this.component = new U2FRegister(this.container, $('#js-register-u2f-templates'), {}, 'token'); + return this.component.start(); + }); - beforeEach(function() { - loadFixtures('u2f/register.html.raw'); - this.u2fDevice = new MockU2FDevice; - this.container = $("#js-register-u2f"); - this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token"); - return this.component.start(); + it('allows registering a U2F device', () => { + const setupButton = this.container.find('#js-setup-u2f-device'); + expect(setupButton.text()).toBe('Setup new U2F device'); + setupButton.trigger('click'); + const inProgressMessage = this.container.children('p'); + expect(inProgressMessage.text()).toContain('Trying to communicate with your device'); + this.u2fDevice.respondToRegisterRequest({ + deviceData: 'this is data from the device', }); - it('allows registering a U2F device', function() { - var deviceResponse, inProgressMessage, registeredMessage, setupButton; - setupButton = this.container.find("#js-setup-u2f-device"); - expect(setupButton.text()).toBe('Setup new U2F device'); - setupButton.click(); - inProgressMessage = this.container.children("p"); - expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); + const registeredMessage = this.container.find('p'); + const deviceResponse = this.container.find('#js-device-response'); + expect(registeredMessage.text()).toContain('Your device was successfully set up!'); + return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + }); + + return describe('errors', () => { + it('doesn\'t allow the same device to be registered twice (for the same user', () => { + const setupButton = this.container.find('#js-setup-u2f-device'); + setupButton.trigger('click'); this.u2fDevice.respondToRegisterRequest({ - deviceData: "this is data from the device" + errorCode: 4, }); - registeredMessage = this.container.find('p'); - deviceResponse = this.container.find('#js-device-response'); - expect(registeredMessage.text()).toContain("Your device was successfully set up!"); - return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + const errorMessage = this.container.find('p'); + return expect(errorMessage.text()).toContain('already been registered with us'); }); - return describe("errors", function() { - it("doesn't allow the same device to be registered twice (for the same user", function() { - var errorMessage, setupButton; - setupButton = this.container.find("#js-setup-u2f-device"); - setupButton.click(); - this.u2fDevice.respondToRegisterRequest({ - errorCode: 4 - }); - errorMessage = this.container.find("p"); - return expect(errorMessage.text()).toContain("already been registered with us"); + + it('displays an error message for other errors', () => { + const setupButton = this.container.find('#js-setup-u2f-device'); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: 'error!', }); - it("displays an error message for other errors", function() { - var errorMessage, setupButton; - setupButton = this.container.find("#js-setup-u2f-device"); - setupButton.click(); - this.u2fDevice.respondToRegisterRequest({ - errorCode: "error!" - }); - errorMessage = this.container.find("p"); - return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); + const errorMessage = this.container.find('p'); + return expect(errorMessage.text()).toContain('There was a problem communicating with your device'); + }); + + return it('allows retrying registration after an error', () => { + let setupButton = this.container.find('#js-setup-u2f-device'); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: 'error!', }); - return it("allows retrying registration after an error", function() { - var registeredMessage, retryButton, setupButton; - setupButton = this.container.find("#js-setup-u2f-device"); - setupButton.click(); - this.u2fDevice.respondToRegisterRequest({ - errorCode: "error!" - }); - retryButton = this.container.find("#U2FTryAgain"); - retryButton.click(); - setupButton = this.container.find("#js-setup-u2f-device"); - setupButton.click(); - this.u2fDevice.respondToRegisterRequest({ - deviceData: "this is data from the device" - }); - registeredMessage = this.container.find("p"); - return expect(registeredMessage.text()).toContain("Your device was successfully set up!"); + const retryButton = this.container.find('#U2FTryAgain'); + retryButton.trigger('click'); + setupButton = this.container.find('#js-setup-u2f-device'); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + deviceData: 'this is data from the device', }); + const registeredMessage = this.container.find('p'); + return expect(registeredMessage.text()).toContain('Your device was successfully set up!'); }); }); -}).call(window); +}); diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb index 695fd6f8573..8e524f9b05a 100644 --- a/spec/lib/gitlab/file_detector_spec.rb +++ b/spec/lib/gitlab/file_detector_spec.rb @@ -18,6 +18,10 @@ describe Gitlab::FileDetector do expect(described_class.type_of('README.md')).to eq(:readme) end + it 'returns nil for a README file in a directory' do + expect(described_class.type_of('foo/README.md')).to be_nil + end + it 'returns the type of a changelog file' do %w(CHANGELOG HISTORY CHANGES NEWS).each do |file| expect(described_class.type_of(file)).to eq(:changelog) @@ -52,6 +56,14 @@ describe Gitlab::FileDetector do end end + it 'returns the type of an issue template' do + expect(described_class.type_of('.gitlab/issue_templates/foo.md')).to eq(:issue_template) + end + + it 'returns the type of a merge request template' do + expect(described_class.type_of('.gitlab/merge_request_templates/foo.md')).to eq(:merge_request_template) + end + it 'returns nil for an unknown file' do expect(described_class.type_of('foo.txt')).to be_nil end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 1ee4acfd193..b11fa38856b 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -68,31 +68,52 @@ describe Gitlab::Git::Repository, seed_helper: true do expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository) end - context 'with no Git env stored' do - before do - expect(Gitlab::Git::Env).to receive(:all).and_return({}) - end + describe 'alternates keyword argument' do + context 'with no Git env stored' do + before do + allow(Gitlab::Git::Env).to receive(:all).and_return({}) + end - it "whitelist some variables and pass them via the alternates keyword argument" do - expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: []) + it "is passed an empty array" do + expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: []) - repository.rugged + repository.rugged + end end - end - context 'with some Git env stored' do - before do - expect(Gitlab::Git::Env).to receive(:all).and_return({ - 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar', - 'GIT_OTHER' => 'another_env' - }) + context 'with absolute and relative Git object dir envvars stored' do + before do + allow(Gitlab::Git::Env).to receive(:all).and_return({ + 'GIT_OBJECT_DIRECTORY_RELATIVE' => './objects/foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['./objects/bar', './objects/baz'], + 'GIT_OBJECT_DIRECTORY' => 'ignored', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => %w[ignored ignored], + 'GIT_OTHER' => 'another_env' + }) + end + + it "is passed the relative object dir envvars after being converted to absolute ones" do + alternates = %w[foo bar baz].map { |d| File.join(repository.path, './objects', d) } + expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: alternates) + + repository.rugged + end end - it "whitelist some variables and pass them via the alternates keyword argument" do - expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar]) + context 'with only absolute Git object dir envvars stored' do + before do + allow(Gitlab::Git::Env).to receive(:all).and_return({ + 'GIT_OBJECT_DIRECTORY' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => %w[bar baz], + 'GIT_OTHER' => 'another_env' + }) + end + + it "is passed the absolute object dir envvars as is" do + expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar baz]) - repository.rugged + repository.rugged + end end end end @@ -1489,6 +1510,21 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#fetch' do + let(:git_path) { Gitlab.config.git.bin_path } + let(:remote_name) { 'my_remote' } + + subject { repository.fetch(remote_name) } + + it 'fetches the remote and returns true if the command was successful' do + expect(repository).to receive(:popen) + .with(%W(#{git_path} fetch #{remote_name}), repository.path) + .and_return(['', 0]) + + expect(subject).to be(true) + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index fd5f984601e..cbc7ce1c1b0 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -73,4 +73,15 @@ describe Gitlab::GitalyClient::RepositoryService do client.apply_gitattributes(revision) end end + + describe '#has_local_branches?' do + it 'sends a has_local_branches message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:has_local_branches) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(value: true)) + + expect(client.has_local_branches?).to be(true) + end + end end diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb index 498f6886bee..c0c29552494 100644 --- a/spec/lib/gitlab/gitaly_client/util_spec.rb +++ b/spec/lib/gitlab/gitaly_client/util_spec.rb @@ -6,16 +6,16 @@ describe Gitlab::GitalyClient::Util do let(:relative_path) { 'my/repo.git' } let(:gl_repository) { 'project-1' } let(:git_object_directory) { '.git/objects' } - let(:git_alternate_object_directory) { '/dir/one:/dir/two' } + let(:git_alternate_object_directory) { ['/dir/one', '/dir/two'] } subject do described_class.repository(repository_storage, relative_path, gl_repository) end it 'creates a Gitaly::Repository with the given data' do - expect(Gitlab::Git::Env).to receive(:[]).with('GIT_OBJECT_DIRECTORY') + allow(Gitlab::Git::Env).to receive(:[]).with('GIT_OBJECT_DIRECTORY_RELATIVE') .and_return(git_object_directory) - expect(Gitlab::Git::Env).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES') + allow(Gitlab::Git::Env).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE') .and_return(git_alternate_object_directory) expect(subject).to be_a(Gitaly::Repository) @@ -23,7 +23,7 @@ describe Gitlab::GitalyClient::Util do expect(subject.relative_path).to eq(relative_path) expect(subject.gl_repository).to eq(gl_repository) expect(subject.git_object_directory).to eq(git_object_directory) - expect(subject.git_alternate_object_directories).to eq([git_alternate_object_directory]) + expect(subject.git_alternate_object_directories).to eq(git_alternate_object_directory) end end diff --git a/spec/lib/gitlab/import_export/project.group.json b/spec/lib/gitlab/import_export/project.group.json new file mode 100644 index 00000000000..82a1fbd2fc5 --- /dev/null +++ b/spec/lib/gitlab/import_export/project.group.json @@ -0,0 +1,188 @@ +{ + "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "visibility_level": 10, + "archived": false, + "milestones": [ + { + "id": 1, + "title": "Project milestone", + "project_id": 8, + "description": "Project-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": null + } + ], + "labels": [ + { + "id": 2, + "title": "project label", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "type": "ProjectLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] + } + ], + "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": 1, + "updated_by_id": 1, + "confidential": false, + "deleted_at": null, + "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": "Project milestone", + "project_id": 8, + "description": "Project-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": null + }, + "label_links": [ + { + "id": 11, + "label_id": 6, + "target_id": 1, + "target_type": "Issue", + "created_at": "2017-08-15T18:37:40.795Z", + "updated_at": "2017-08-15T18:37:40.795Z", + "label": { + "id": 6, + "title": "group label", + "color": "#A8D695", + "project_id": null, + "created_at": "2017-08-15T18:37:19.698Z", + "updated_at": "2017-08-15T18:37:19.698Z", + "template": false, + "description": "", + "group_id": 5, + "type": "GroupLabel", + "priorities": [] + } + }, + { + "id": 11, + "label_id": 2, + "target_id": 1, + "target_type": "Issue", + "created_at": "2017-08-15T18:37:40.795Z", + "updated_at": "2017-08-15T18:37:40.795Z", + "label": { + "id": 6, + "title": "project label", + "color": "#A8D695", + "project_id": null, + "created_at": "2017-08-15T18:37:19.698Z", + "updated_at": "2017-08-15T18:37:19.698Z", + "template": false, + "description": "", + "group_id": 5, + "type": "ProjectLabel", + "priorities": [] + } + } + ] + }, + { + "id": 2, + "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": 2, + "updated_by_id": 1, + "confidential": false, + "deleted_at": null, + "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": "A group 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": 100 + }, + "label_links": [ + { + "id": 11, + "label_id": 2, + "target_id": 1, + "target_type": "Issue", + "created_at": "2017-08-15T18:37:40.795Z", + "updated_at": "2017-08-15T18:37:40.795Z", + "label": { + "id": 2, + "title": "project label", + "color": "#A8D695", + "project_id": null, + "created_at": "2017-08-15T18:37:19.698Z", + "updated_at": "2017-08-15T18:37:19.698Z", + "template": false, + "description": "", + "group_id": 5, + "type": "ProjectLabel", + "priorities": [] + } + } + ] + } + ], + "snippets": [ + + ], + "hooks": [ + + ] +} diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json index 2d8f3d4a566..02450478a77 100644 --- a/spec/lib/gitlab/import_export/project.light.json +++ b/spec/lib/gitlab/import_export/project.light.json @@ -5,9 +5,9 @@ "milestones": [ { "id": 1, - "title": "test milestone", + "title": "Project milestone", "project_id": 8, - "description": "test milestone", + "description": "Project-level milestone", "due_date": null, "created_at": "2016-06-14T15:02:04.415Z", "updated_at": "2016-06-14T15:02:04.415Z", @@ -19,7 +19,7 @@ "labels": [ { "id": 2, - "title": "test2", + "title": "A project label", "color": "#428bca", "project_id": 8, "created_at": "2016-07-22T08:55:44.161Z", @@ -63,30 +63,21 @@ "last_edited_at": null, "last_edited_by_id": null, "group_milestone_id": null, + "milestone": { + "id": 1, + "title": "Project milestone", + "project_id": 8, + "description": "Project-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": null + }, "label_links": [ { "id": 11, - "label_id": 6, - "target_id": 1, - "target_type": "Issue", - "created_at": "2017-08-15T18:37:40.795Z", - "updated_at": "2017-08-15T18:37:40.795Z", - "label": { - "id": 6, - "title": "group label", - "color": "#A8D695", - "project_id": null, - "created_at": "2017-08-15T18:37:19.698Z", - "updated_at": "2017-08-15T18:37:19.698Z", - "template": false, - "description": "", - "group_id": 5, - "type": "GroupLabel", - "priorities": [] - } - }, - { - "id": 11, "label_id": 2, "target_id": 1, "target_type": "Issue", @@ -94,14 +85,14 @@ "updated_at": "2017-08-15T18:37:40.795Z", "label": { "id": 6, - "title": "project label", + "title": "Another project label", "color": "#A8D695", "project_id": null, "created_at": "2017-08-15T18:37:19.698Z", "updated_at": "2017-08-15T18:37:19.698Z", "template": false, "description": "", - "group_id": 5, + "group_id": null, "type": "ProjectLabel", "priorities": [] } @@ -109,10 +100,6 @@ ] } ], - "snippets": [ - - ], - "hooks": [ - - ] + "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 efe11ca794a..4301eee17dc 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do context 'JSON' do it 'restores models based on JSON' do - expect(@restored_project_json).to be true + expect(@restored_project_json).to be_truthy end it 'restore correct project features' do @@ -182,6 +182,53 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end + shared_examples 'restores project successfully' do + it 'correctly restores project' do + expect(shared.errors).to be_empty + expect(restored_project_json).to be_truthy + end + end + + shared_examples 'restores project correctly' do |**results| + it 'has labels' do + expect(project.labels.size).to eq(results.fetch(:labels, 0)) + end + + it 'has label priorities' do + expect(project.labels.first.priorities).not_to be_empty + end + + it 'has milestones' do + expect(project.milestones.size).to eq(results.fetch(:milestones, 0)) + end + + it 'has issues' 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)) + end + end + + shared_examples 'restores group correctly' do |**results| + it 'has group label' do + expect(project.group.labels.size).to eq(results.fetch(:labels, 0)) + end + + 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 let(:user) { create(:user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } @@ -190,33 +237,45 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do let(:restored_project_json) { project_tree_restorer.restore } before do - project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") - allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') end - context 'project.json file access check' do - it 'does not read a symlink' do - Dir.mktmpdir do |tmpdir| - setup_symlink(tmpdir, 'project.json') - allow(shared).to receive(:export_path).and_call_original + context 'with a simple project' do + before do + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") + + restored_project_json + end + + it_behaves_like 'restores project correctly', + issues: 1, + labels: 1, + milestones: 1, + first_issue_labels: 1 - restored_project_json + context 'project.json file access check' do + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'project.json') + allow(shared).to receive(:export_path).and_call_original - expect(shared.errors.first).to be_nil + restored_project_json + + expect(shared.errors).to be_empty + end end end - end - context 'when there is an existing build with build token' do - it 'restores project json correctly' do - create(:ci_build, token: 'abcd') + context 'when there is an existing build with build token' do + before do + create(:ci_build, token: 'abcd') + end - expect(restored_project_json).to be true + it_behaves_like 'restores project successfully' end end - context 'with group' do + context 'with a project that has a group' do let!(:project) do create(:project, :builds_disabled, @@ -227,43 +286,22 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end before do - project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.group.json") restored_project_json end - it 'correctly restores project' do - expect(restored_project_json).to be_truthy - expect(shared.errors).to be_empty - end + it_behaves_like 'restores project successfully' + it_behaves_like 'restores project correctly', + issues: 2, + labels: 1, + milestones: 1, + first_issue_labels: 1 - it 'has labels' do - expect(project.labels.count).to eq(2) - end - - it 'creates group label' do - expect(project.group.labels.count).to eq(1) - end - - it 'has label priorities' do - expect(project.labels.first.priorities).not_to be_empty - end - - it 'has milestones' do - expect(project.milestones.count).to eq(1) - end - - it 'has issue' do - expect(project.issues.count).to eq(1) - expect(project.issues.first.labels.count).to eq(2) - end - - it 'has issue with group label and project label' do - labels = project.issues.first.labels - - expect(labels.where(type: "GroupLabel").count).to eq(1) - expect(labels.where(type: "ProjectLabel").count).to eq(1) - end + it_behaves_like 'restores group correctly', + labels: 1, + milestones: 1, + first_issue_labels: 1 end end end diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index d19bd611919..57b0ef8d1ad 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -4,9 +4,9 @@ describe Gitlab::ProjectTemplate do describe '.all' do it 'returns a all templates' do expected = [ - described_class.new('rails', 'Ruby on Rails'), - described_class.new('spring', 'Spring'), - described_class.new('express', 'NodeJS Express') + described_class.new('rails', 'Ruby on Rails', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/rails'), + described_class.new('spring', 'Spring', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/spring'), + described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express') ] expect(described_class.all).to be_an(Array) @@ -31,7 +31,7 @@ describe Gitlab::ProjectTemplate do end describe 'instance methods' do - subject { described_class.new('phoenix', 'Phoenix Framework') } + subject { described_class.new('phoenix', 'Phoenix Framework', 'Phoenix description', 'link-to-template') } it { is_expected.to respond_to(:logo, :file, :archive_path) } end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 9230d58012f..80bf7986ee0 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -66,12 +66,34 @@ describe Gitlab::Workhorse do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } subject { described_class.send_git_patch(repository, diff_refs) } - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + context 'when Gitaly workhorse_send_git_patch feature is enabled' do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-format-patch") + expect(params).to eq({ + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'RawPatchRequest' => Gitaly::RawPatchRequest.new( + repository: repository.gitaly_repository, + left_commit_id: 'base', + right_commit_id: 'head' + ).to_json + }.deep_stringify_keys) + end + end + + context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) - expect(key).to eq("Gitlab-Workhorse-Send-Data") - expect(command).to eq("git-format-patch") - expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-format-patch") + expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + end end end @@ -115,14 +137,36 @@ describe Gitlab::Workhorse do describe '.send_git_diff' do let(:diff_refs) { double(base_sha: "base", head_sha: "head") } - subject { described_class.send_git_patch(repository, diff_refs) } + subject { described_class.send_git_diff(repository, diff_refs) } - it 'sets the header correctly' do - key, command, params = decode_workhorse_header(subject) + context 'when Gitaly workhorse_send_git_diff feature is enabled' do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) - expect(key).to eq("Gitlab-Workhorse-Send-Data") - expect(command).to eq("git-format-patch") - expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-diff") + expect(params).to eq({ + 'GitalyServer' => { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + }, + 'RawDiffRequest' => Gitaly::RawDiffRequest.new( + repository: repository.gitaly_repository, + left_commit_id: 'base', + right_commit_id: 'head' + ).to_json + }.deep_stringify_keys) + end + end + + context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(subject) + + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq("git-diff") + expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head") + end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a26c71e5155..cf26dbfea49 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -874,7 +874,7 @@ describe Project do let(:project) { create(:project) } context 'when avatar file is uploaded' do - let(:project) { create(:project, :with_avatar) } + let(:project) { create(:project, :public, :with_avatar) } let(:avatar_path) { "/uploads/-/system/project/avatar/#{project.id}/dk.png" } let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 5d78aed5b4f..f44693a71bb 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1509,7 +1509,9 @@ describe Repository do :gitignore, :koding, :gitlab_ci, - :avatar + :avatar, + :issue_template, + :merge_request_template ]) repository.after_change_head diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 862920ad7c3..9f3b5a809d7 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -222,13 +222,6 @@ describe API::Helpers do expect { current_user }.to raise_error /401/ end - it "returns a 401 response for a token without the appropriate scope" do - personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) - env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token - - expect { current_user }.to raise_error /401/ - end - it "leaves user as is when sudo not specified" do env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to eq(user) @@ -238,18 +231,25 @@ describe API::Helpers do expect(current_user).to eq(user) end + it "does not allow tokens without the appropriate scope" do + personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + + expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError + end + it 'does not allow revoked tokens' do personal_access_token.revoke! env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect { current_user }.to raise_error /401/ + expect { current_user }.to raise_error API::APIGuard::RevokedError end it 'does not allow expired tokens' do personal_access_token.update_attributes!(expires_at: 1.day.ago) env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token - expect { current_user }.to raise_error /401/ + expect { current_user }.to raise_error API::APIGuard::ExpiredError end end diff --git a/spec/rubocop/cop/migration/datetime_spec.rb b/spec/rubocop/cop/migration/datetime_spec.rb index 388b086ce6a..b1dfcf1b048 100644 --- a/spec/rubocop/cop/migration/datetime_spec.rb +++ b/spec/rubocop/cop/migration/datetime_spec.rb @@ -9,6 +9,7 @@ describe RuboCop::Cop::Migration::Datetime do include CopHelper subject(:cop) { described_class.new } + let(:migration_with_datetime) do %q( class Users < ActiveRecord::Migration @@ -22,6 +23,19 @@ describe RuboCop::Cop::Migration::Datetime do ) end + let(:migration_with_timestamp) do + %q( + class Users < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column(:users, :username, :text) + add_column(:users, :last_sign_in, :timestamp) + end + end + ) + end + let(:migration_without_datetime) do %q( class Users < ActiveRecord::Migration @@ -58,6 +72,17 @@ describe RuboCop::Cop::Migration::Datetime do aggregate_failures do expect(cop.offenses.size).to eq(1) expect(cop.offenses.map(&:line)).to eq([7]) + expect(cop.offenses.first.message).to include('datetime') + end + end + + it 'registers an offense when the ":timestamp" data type is used' do + inspect_source(cop, migration_with_timestamp) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([7]) + expect(cop.offenses.first.message).to include('timestamp') end end @@ -81,6 +106,7 @@ describe RuboCop::Cop::Migration::Datetime do context 'outside of migration' do it 'registers no offense' do inspect_source(cop, migration_with_datetime) + inspect_source(cop, migration_with_timestamp) inspect_source(cop, migration_without_datetime) inspect_source(cop, migration_with_datetime_with_timezone) diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb index 4aeb593da44..87832b3dca1 100644 --- a/spec/serializers/merge_request_entity_spec.rb +++ b/spec/serializers/merge_request_entity_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe MergeRequestEntity do - let(:project) { create :project } + let(:project) { create :project, :repository } let(:resource) { create(:merge_request, source_project: project, target_project: project) } let(:user) { create(:user) } diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index 9c9b0c4c4a1..a1f7dc44d31 100644 --- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -6,11 +6,7 @@ describe MergeRequests::Conflicts::ResolveService do let(:project) { create(:project, :public, :repository) } let(:forked_project) do - forked_project = fork_project(project, user) - TestEnv.copy_repo(forked_project, - bare_repo: TestEnv.forked_repo_path_bare, - refs: TestEnv::FORKED_BRANCH_SHA) - forked_project + fork_project_with_submodules(project, user) end let(:merge_request) do diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 80213d093f1..d1043f99b5a 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -185,7 +185,7 @@ describe MergeRequests::MergeService do context 'source branch removal' do context 'when the source branch is protected' do let(:service) do - described_class.new(project, user, should_remove_source_branch: '1') + described_class.new(project, user, 'should_remove_source_branch' => true) end before do @@ -200,7 +200,7 @@ describe MergeRequests::MergeService do context 'when the source branch is the default branch' do let(:service) do - described_class.new(project, user, should_remove_source_branch: '1') + described_class.new(project, user, 'should_remove_source_branch' => true) end before do @@ -215,10 +215,10 @@ describe MergeRequests::MergeService do context 'when the source branch can be removed' do context 'when MR author set the source branch to be removed' do - let(:service) do - merge_request.merge_params['force_remove_source_branch'] = '1' - merge_request.save! - described_class.new(project, user, commit_message: 'Awesome message') + let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } + + before do + merge_request.update_attribute(:merge_params, { 'force_remove_source_branch' => '1' }) end it 'removes the source branch using the author user' do @@ -227,11 +227,20 @@ describe MergeRequests::MergeService do .and_call_original service.execute(merge_request) end + + context 'when the merger set the source branch not to be removed' do + let(:service) { described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => false) } + + it 'does not delete the source branch' do + expect(DeleteBranchService).not_to receive(:new) + service.execute(merge_request) + end + end end context 'when MR merger set the source branch to be removed' do let(:service) do - described_class.new(project, user, commit_message: 'Awesome message', should_remove_source_branch: '1') + described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => true) end it 'removes the source branch using the current user' do diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 7257c359a7e..98409be4236 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -127,10 +127,10 @@ describe MergeRequests::UpdateService, :mailer do end it 'creates system note about discussion lock' do - note = find_note('locked this issue') + note = find_note('locked this merge request') expect(note).not_to be_nil - expect(note.note).to eq 'locked this issue' + expect(note.note).to eq 'locked this merge request' end context 'when not including source branch removal options' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index b64ca5be8fc..b13e12e7c94 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -731,6 +731,18 @@ describe NotificationService, :mailer do should_not_email(@u_participating) end + it "doesn't send multiple email when a user is subscribed to multiple given labels" do + subscriber_to_both = create(:user) do |user| + [label_1, label_2].each { |label| label.toggle_subscription(user, project) } + end + + notification.relabeled_issue(issue, [label_1, label_2], @u_disabled) + + should_email(subscriber_to_label_1) + should_email(subscriber_to_label_2) + should_email(subscriber_to_both) + end + context 'confidential issues' do let(:author) { create(:user) } let(:assignee) { create(:user) } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index b1241cd8d0b..cd473c1f388 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1145,4 +1145,42 @@ describe SystemNoteService do it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" } end end + + describe '.discussion_lock' do + subject { described_class.discussion_lock(noteable, author) } + + context 'discussion unlocked' do + it_behaves_like 'a system note' do + let(:action) { 'unlocked' } + end + + it 'creates the note text correctly' do + [:issue, :merge_request].each do |type| + issuable = create(type) + + expect(described_class.discussion_lock(issuable, author).note) + .to eq("unlocked this #{type.to_s.titleize.downcase}") + end + end + end + + context 'discussion locked' do + before do + noteable.update_attribute(:discussion_locked, true) + end + + it_behaves_like 'a system note' do + let(:action) { 'locked' } + end + + it 'creates the note text correctly' do + [:issue, :merge_request].each do |type| + issuable = create(type, discussion_locked: true) + + expect(described_class.discussion_lock(issuable, author).note) + .to eq("locked this #{type.to_s.titleize.downcase}") + end + end + end + end end diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb index 57e28e040d7..111534f2f26 100644 --- a/spec/support/api/scopes/read_user_shared_examples.rb +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -27,10 +27,10 @@ shared_examples_for 'allows the "read_user" scope' do stub_container_registry_config(enabled: true) end - it 'returns a "401" response' do + it 'returns a "403" response' do get api_call.call(path, user, personal_access_token: token) - expect(response).to have_http_status(401) + expect(response).to have_http_status(403) end end end @@ -74,10 +74,10 @@ shared_examples_for 'does not allow the "read_user" scope' do context 'when the requesting token has the "read_user" scope' do let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } - it 'returns a "401" response' do + it 'returns a "403" response' do post api_call.call(path, user, personal_access_token: token), attributes_for(:user, projects_limit: 3) - expect(response).to have_http_status(401) + expect(response).to have_http_status(403) end end end diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb index 3e979f2f470..b39052923dd 100644 --- a/spec/support/email_helpers.rb +++ b/spec/support/email_helpers.rb @@ -1,6 +1,6 @@ module EmailHelpers - def sent_to_user?(user, recipients = email_recipients) - recipients.include?(user.notification_email) + def sent_to_user(user, recipients: email_recipients) + recipients.count { |to| to == user.notification_email } end def reset_delivered_emails! @@ -10,17 +10,17 @@ module EmailHelpers def should_only_email(*users, kind: :to) recipients = email_recipients(kind: kind) - users.each { |user| should_email(user, recipients) } + users.each { |user| should_email(user, recipients: recipients) } expect(recipients.count).to eq(users.count) end - def should_email(user, recipients = email_recipients) - expect(sent_to_user?(user, recipients)).to be_truthy + def should_email(user, times: 1, recipients: email_recipients) + expect(sent_to_user(user, recipients: recipients)).to eq(times) end - def should_not_email(user, recipients = email_recipients) - expect(sent_to_user?(user, recipients)).to be_falsey + def should_not_email(user, recipients: email_recipients) + should_email(user, times: 0, recipients: recipients) end def should_not_email_anyone diff --git a/spec/support/project_forks_helper.rb b/spec/support/project_forks_helper.rb index 0d1c6792d13..d6680735aa1 100644 --- a/spec/support/project_forks_helper.rb +++ b/spec/support/project_forks_helper.rb @@ -52,7 +52,7 @@ module ProjectForksHelper TestEnv.copy_repo(forked_project, bare_repo: TestEnv.forked_repo_path_bare, refs: TestEnv::FORKED_BRANCH_SHA) - + forked_project.repository.after_import forked_project end end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index d9e9409840f..e881ec37ae5 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -12,6 +12,28 @@ describe RepositoryForkWorker do end describe "#perform" do + describe 'when a worker was reset without cleanup' do + let(:jid) { '12345678' } + let(:started_project) { create(:project, :repository, :import_started) } + + it 'creates a new repository from a fork' do + allow(subject).to receive(:jid).and_return(jid) + + expect(shell).to receive(:fork_repository).with( + '/test/path', + project.full_path, + project.repository_storage_path, + fork_project.namespace.full_path + ).and_return(true) + + subject.perform( + project.id, + '/test/path', + project.full_path, + fork_project.namespace.full_path) + end + end + it "creates a new repository from a fork" do expect(shell).to receive(:fork_repository).with( '/test/path', diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index 100dfc32bbe..5cff5108477 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -6,6 +6,23 @@ describe RepositoryImportWorker do subject { described_class.new } describe '#perform' do + context 'when worker was reset without cleanup' do + let(:jid) { '12345678' } + let(:started_project) { create(:project, :import_started, import_jid: jid) } + + it 'imports the project successfully' do + allow(subject).to receive(:jid).and_return(jid) + + expect_any_instance_of(Projects::ImportService).to receive(:execute) + .and_return({ status: :ok }) + + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) + expect_any_instance_of(Project).to receive(:import_finish) + + subject.perform(project.id) + end + end + context 'when the import was successful' do it 'imports a project' do expect_any_instance_of(Projects::ImportService).to receive(:execute) diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore index 520a86352f7..c79ba5080a3 100644 --- a/vendor/gitignore/Android.gitignore +++ b/vendor/gitignore/Android.gitignore @@ -41,7 +41,8 @@ captures/ .idea/libraries # Keystore files -*.jks +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore index e3923f96fce..ffa6ecc3f9b 100644 --- a/vendor/gitignore/Autotools.gitignore +++ b/vendor/gitignore/Autotools.gitignore @@ -31,3 +31,12 @@ Makefile.in # http://www.gnu.org/software/texinfo /texinfo.tex + +# http://www.gnu.org/software/m4/ + +m4/libtool.m4 +m4/ltoptions.m4 +m4/ltsugar.m4 +m4/ltversion.m4 +m4/lt~obsolete.m4 +autom4te.cache diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore index ac67aaf3243..b6d65867dac 100644 --- a/vendor/gitignore/Elixir.gitignore +++ b/vendor/gitignore/Elixir.gitignore @@ -1,6 +1,8 @@ /_build /cover /deps +/doc +/.fetch erl_crash.dump *.ez *.beam diff --git a/vendor/gitignore/ExtJs.gitignore b/vendor/gitignore/ExtJs.gitignore index c92aea0fe0c..ab97a8cc3e1 100644 --- a/vendor/gitignore/ExtJs.gitignore +++ b/vendor/gitignore/ExtJs.gitignore @@ -10,3 +10,5 @@ ext/ modern.json modern.jsonp resources/sass/.sass-cache/ +resources/.arch-internal-preview.css +.arch-internal-preview.css diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore index 09dfde64b5f..cca150a88dd 100644 --- a/vendor/gitignore/Global/Matlab.gitignore +++ b/vendor/gitignore/Global/Matlab.gitignore @@ -19,4 +19,4 @@ slprj/ octave-workspace # Simulink autosave extension -.autosave +*.autosave diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore index 37de8bb4793..cd0c7d3e45a 100644 --- a/vendor/gitignore/Global/Xcode.gitignore +++ b/vendor/gitignore/Global/Xcode.gitignore @@ -2,11 +2,17 @@ # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore -## Build generated +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ - -## Various settings +*.moved-aside *.pbxuser !default.pbxuser *.mode1v3 @@ -15,9 +21,3 @@ DerivedData/ !default.mode2v3 *.perspectivev3 !default.perspectivev3 -xcuserdata/ - -## Other -*.moved-aside -*.xccheckout -*.xcscmblueprint diff --git a/vendor/gitignore/Global/macOS.gitignore b/vendor/gitignore/Global/macOS.gitignore index 9d1061e8bc4..135767fc075 100644 --- a/vendor/gitignore/Global/macOS.gitignore +++ b/vendor/gitignore/Global/macOS.gitignore @@ -1,5 +1,5 @@ # General -*.DS_Store +.DS_Store .AppleDouble .LSOverride diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore index 53a74e74657..b6bf3a9c96a 100644 --- a/vendor/gitignore/Joomla.gitignore +++ b/vendor/gitignore/Joomla.gitignore @@ -251,7 +251,7 @@ /administrator/language/en-GB/en-GB.tpl_hathor.sys.ini /administrator/language/en-GB/en-GB.xml /administrator/language/overrides/* -/administrator/logs/index.html +/administrator/logs/* /administrator/manifests/* /administrator/modules/mod_custom/* /administrator/modules/mod_feed/* diff --git a/vendor/gitignore/OCaml.gitignore b/vendor/gitignore/OCaml.gitignore index f7817ae5c36..da0b20424a0 100644 --- a/vendor/gitignore/OCaml.gitignore +++ b/vendor/gitignore/OCaml.gitignore @@ -18,3 +18,6 @@ _build/ # oasis generated files setup.data setup.log + +# Merlin configuring file for Vim and Emacs +.merlin diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore index 113294a5f18..af2f537516d 100644 --- a/vendor/gitignore/Python.gitignore +++ b/vendor/gitignore/Python.gitignore @@ -23,6 +23,7 @@ wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -51,6 +52,8 @@ coverage.xml # Django stuff: *.log +.static_storage/ +.media/ local_settings.py # Flask stuff: @@ -84,6 +87,8 @@ celerybeat-schedule env/ venv/ ENV/ +env.bak/ +venv.bak/ # Spyder project settings .spyderproject diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore index fe67fdf1ee6..037a1e75790 100644 --- a/vendor/gitignore/Qt.gitignore +++ b/vendor/gitignore/Qt.gitignore @@ -31,11 +31,9 @@ ui_*.h Makefile* *build-* - # Qt unit tests target_wrapper.* - # QtCreator *.autosave diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index a0322dbd35a..b6418e51766 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -13,6 +13,7 @@ ## Intermediate documents: *.dvi +*.xdv *-converted-to.* # these rules might exclude image files for figures etc. # *.ps diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore index f20453be963..9b5aebb1b35 100644 --- a/vendor/gitignore/Terraform.gitignore +++ b/vendor/gitignore/Terraform.gitignore @@ -5,3 +5,6 @@ # Module directory .terraform/ + +# Variable values for development +terraform.tfvars diff --git a/vendor/gitignore/Umbraco.gitignore b/vendor/gitignore/Umbraco.gitignore index ea05e1fb2a9..b6b0743f62a 100644 --- a/vendor/gitignore/Umbraco.gitignore +++ b/vendor/gitignore/Umbraco.gitignore @@ -1,3 +1,7 @@ +## Ignore Umbraco files/folders generated for each instance +## +## Get latest from https://github.com/github/gitignore/blob/master/Umbraco.gitignore + # Note: VisualStudio gitignore rules may also be relevant # Umbraco diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index f652b45c2ee..0867ec5a7ee 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -96,6 +96,9 @@ ipch/ *.vspx *.sap +# Visual Studio Trace Files +*.e2e + # TFS 2012 Local Workspace $tf/ @@ -297,3 +300,6 @@ __pycache__/ *.btm.cs *.odx.cs *.xsd.cs + +# OpenCover UI analysis results +OpenCover/ diff --git a/vendor/gitignore/ZendFramework.gitignore b/vendor/gitignore/ZendFramework.gitignore index 80adb154900..f0b7d8585b7 100644 --- a/vendor/gitignore/ZendFramework.gitignore +++ b/vendor/gitignore/ZendFramework.gitignore @@ -19,7 +19,6 @@ temp/ data/DoctrineORMModule/Proxy/ data/DoctrineORMModule/cache/ - # Legacy ZF1 demos/ extras/documentation diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml index 8a214352d2a..86e4985d8d2 100644 --- a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml @@ -29,7 +29,7 @@ format: compile: stage: build script: - - go build -race -ldflags "-extldflags '-static'" -o mybinary + - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_DIR/mybinary artifacts: paths: - mybinary diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml index 91b096654d1..ba2efbd03a0 100644 --- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml @@ -7,8 +7,8 @@ # This template will build and test your projects as well as create the documentation. # # * Caches downloaded dependencies and plugins between invocation. -# * Does only verify merge requests but deploy built artifacts of the -# master branch. +# * Verify but don't deploy merge requests. +# * Deploy built artifacts from master branch only. # * Shows how to use multiple jobs in test stage for verifying functionality # with multiple JDKs. # * Uses site:stage to collect the documentation for multi-module projects. @@ -20,7 +20,7 @@ variables: MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true" # As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used # when running from the command line. - # `installAtEnd` and `deployAtEnd`are only effective with recent version of the corresponding plugins. + # `installAtEnd` and `deployAtEnd` are only effective with recent version of the corresponding plugins. MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true" # Cache downloaded dependencies and plugins between builds. @@ -100,4 +100,3 @@ pages: - public only: - master - diff --git a/vendor/gitlab-ci-yml/Python.gitlab-ci.yml b/vendor/gitlab-ci-yml/Python.gitlab-ci.yml new file mode 100644 index 00000000000..a2882a5407d --- /dev/null +++ b/vendor/gitlab-ci-yml/Python.gitlab-ci.yml @@ -0,0 +1,32 @@ +# This file is a template, and might need editing before it works on your project. +image: python:latest + +before_script: + - python -V # Print out python version for debugging + +test: + script: + - python setup.py test + - pip install tox flake8 # you can also use tox + - tox -e py36,flake8 + +run: + script: + - python setup.py bdist_wheel + # an alternative approach is to install and run: + - pip install dist/* + # run the command here + artifacts: + paths: + - dist/*.whl + +pages: + script: + - pip install sphinx sphinx-rtd-theme + - cd doc ; make html + - mv build/html/ ../public/ + artifacts: + paths: + - public + only: + - master diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 24623ff4c1f..9f78059986d 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -13,9 +13,9 @@ activemodel,4.2.8,MIT activerecord,4.2.8,MIT activesupport,4.2.8,MIT acts-as-taggable-on,4.0.0,MIT -addressable,2.3.8,Apache 2.0 +addressable,2.5.2,Apache 2.0 after,0.8.2,MIT -ajv,5.2.0,MIT +ajv,5.2.2,MIT ajv-keywords,2.1.0,MIT akismet,2.0.0,MIT align-text,0.1.4,MIT @@ -28,8 +28,6 @@ ansi-regex,2.1.1,MIT ansi-styles,2.2.1,MIT anymatch,1.3.2,ISC append-transform,0.4.0,MIT -aproba,1.1.1,ISC -are-we-there-yet,1.1.4,ISC arel,6.0.4,MIT argparse,1.0.9,MIT arr-diff,2.0.0,MIT @@ -46,21 +44,15 @@ arrify,1.0.1,MIT asana,0.6.0,MIT asciidoctor,1.5.3,MIT asciidoctor-plantuml,0.0.7,MIT -asn1,0.2.3,MIT asn1.js,4.9.1,MIT assert,1.4.1,MIT -assert-plus,0.2.0,MIT async,2.4.1,MIT async-each,1.0.1,MIT -asynckit,0.4.0,MIT atomic,1.1.99,Apache 2.0 attr_encrypted,3.0.3,MIT attr_required,1.0.0,MIT -autoparse,0.3.3,Apache 2.0 autoprefixer,6.7.7,MIT autoprefixer-rails,6.2.3,MIT -aws-sign2,0.6.0,Apache 2.0 -aws4,1.6.0,MIT axiom-types,0.1.1,MIT axios,0.16.2,MIT babel-code-frame,6.22.0,MIT @@ -145,19 +137,16 @@ base64-js,1.2.0,MIT base64id,1.0.0,MIT batch,0.6.1,MIT bcrypt,3.1.11,MIT -bcrypt-pbkdf,1.0.1,New BSD bcrypt_pbkdf,1.0.0,MIT better-assert,1.0.2,MIT big.js,3.1.3,MIT binary-extensions,1.10.0,MIT -bindata,2.3.5,ruby +bindata,2.4.1,ruby blob,0.0.4,unknown -block-stream,0.0.9,ISC bluebird,2.11.0,MIT bn.js,4.11.6,MIT body-parser,1.17.2,MIT bonjour,3.5.0,MIT -boom,2.10.1,New BSD bootstrap-sass,3.3.6,MIT bootstrap_form,2.7.0,MIT brace-expansion,1.1.8,MIT @@ -187,7 +176,6 @@ camelcase-keys,2.1.0,MIT caniuse-api,1.6.1,MIT caniuse-db,1.0.30000649,CC-BY-4.0 carrierwave,1.1.0,MIT -caseless,0.12.0,Apache 2.0 cause,0.1,MIT center-align,0.1.3,MIT chalk,1.1.3,MIT @@ -216,7 +204,6 @@ color-string,0.3.0,MIT colormin,1.1.2,MIT colors,1.1.2,MIT combine-lists,1.0.1,MIT -combined-stream,1.0.5,MIT commander,2.9.0,MIT commondir,1.0.1,MIT component-bind,1.0.0,unknown @@ -233,8 +220,7 @@ configstore,1.4.0,Simplified BSD connect,3.6.3,MIT connect-history-api-fallback,1.3.0,MIT connection_pool,2.2.1,MIT -console-browserify,1.1.0,MIT -console-control-strings,1.1.0,ISC +console-browserify,1.1.0,[Circular] consolidate,0.14.5,MIT constants-browserify,1.0.0,MIT contains-path,0.1.0,MIT @@ -254,7 +240,6 @@ create-hmac,1.1.4,MIT creole,0.5.0,ruby cropper,2.3.0,MIT cross-spawn,5.1.0,MIT -cryptiles,2.0.5,New BSD crypto-browserify,3.11.0,MIT css-color-names,0.0.4,MIT css-loader,0.28.0,MIT @@ -268,13 +253,14 @@ custom-event,1.0.1,MIT d,1.0.0,MIT d3,3.5.11,New BSD d3_rails,3.5.11,MIT -dashdash,1.14.1,MIT date-now,0.1.4,MIT de-indent,1.0.2,MIT debug,2.6.8,MIT debugger-ruby_core_source,1.3.8,MIT decamelize,1.2.0,MIT deckar01-task_list,2.0.0,MIT +declarative,0.0.10,MIT +declarative-option,0.1.0,MIT decompress-response,3.3.0,MIT deep-equal,1.0.1,MIT deep-extend,0.4.2,MIT @@ -283,9 +269,7 @@ default-require-extensions,1.0.0,MIT default_value_for,3.0.2,MIT defined,1.0.0,MIT del,2.2.2,MIT -delayed-stream,1.0.0,MIT delegate,3.1.2,MIT -delegates,1.0.0,MIT depd,1.1.1,MIT des.js,1.0.0,MIT descendants_tracker,0.0.4,MIT @@ -310,14 +294,13 @@ domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0" domelementtype,1.3.0,unknown domhandler,2.3.0,unknown domutils,1.5.1,unknown -doorkeeper,4.2.0,MIT -doorkeeper-openid_connect,1.1.2,MIT +doorkeeper,4.2.6,MIT +doorkeeper-openid_connect,1.2.0,MIT dropzone,4.2.0,MIT dropzonejs-rails,0.7.2,MIT -duplexer,0.1.1,MIT +duplexer,0.1.1,[Circular] duplexer3,0.1.4,New BSD duplexify,3.5.1,MIT -ecc-jsbn,0.1.1,MIT editorconfig,0.13.2,MIT ee-first,1.1.1,MIT ejs,2.5.6,Apache 2.0 @@ -362,7 +345,7 @@ eslint-plugin-import,2.2.0,MIT eslint-plugin-jasmine,2.2.0,MIT eslint-plugin-promise,3.5.0,ISC espree,3.5.0,Simplified BSD -esprima,4.0.0,Simplified BSD +esprima,2.7.3,Simplified BSD esquery,1.0.0,BSD esrecurse,4.1.0,Simplified BSD estraverse,4.1.1,Simplified BSD @@ -388,12 +371,10 @@ express,4.15.4,MIT expression_parser,0.9.0,MIT extend,3.0.1,MIT extglob,0.3.2,MIT -extlib,0.9.16,MIT -extsprintf,1.0.2,MIT -faraday,0.12.1,MIT +faraday,0.12.2,MIT faraday_middleware,0.11.0.1,MIT faraday_middleware-multi_json,0.0.6,MIT -fast-deep-equal,0.1.0,MIT +fast-deep-equal,1.0.0,MIT fast-levenshtein,2.0.6,MIT fast_gettext,1.4.0,"MIT,ruby" fastparse,1.1.1,MIT @@ -428,8 +409,6 @@ follow-redirects,1.2.3,MIT font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License" for-in,0.1.6,MIT for-own,0.1.4,MIT -forever-agent,0.6.1,Apache 2.0 -form-data,2.1.4,MIT formatador,0.2.5,MIT forwarded,0.1.0,MIT fresh,0.5.0,MIT @@ -437,11 +416,8 @@ from,0.1.7,MIT fs-access,1.0.1,MIT fs-extra,0.26.7,MIT fs.realpath,1.0.0,ISC -fsevents,1.1.2,MIT -fstream,1.0.11,ISC -fstream-ignore,1.0.5,ISC +fsevents,,unknown function-bind,1.1.0,MIT -gauge,2.7.4,ISC gemnasium-gitlab-service,0.2.6,MIT gemojione,3.3.0,MIT generate-function,2.0.0,MIT @@ -450,15 +426,15 @@ get-caller-file,1.0.2,ISC get-stdin,4.0.1,MIT get-stream,3.0.0,MIT get_process_mem,0.2.0,MIT -getpass,0.1.7,MIT gettext_i18n_rails,1.8.0,MIT gettext_i18n_rails_js,1.2.0,MIT -gitaly-proto,0.33.0,MIT +gitaly-proto,0.41.0,MIT github-linguist,4.7.6,MIT github-markup,1.6.1,MIT gitlab-flowdock-git-hook,1.0.1,MIT -gitlab-grit,2.8.1,MIT -gitlab-markup,1.5.1,MIT +gitlab-grit,2.8.2,MIT +gitlab-markup,1.6.2,MIT +gitlab-svgs,1.0.4,unknown gitlab_omniauth-ldap,2.0.4,MIT glob,6.0.4,ISC glob-base,0.3.0,MIT @@ -471,9 +447,9 @@ gollum-lib,4.2.7,MIT gollum-rugged_adapter,0.4.4,MIT gon,6.1.0,MIT good-listener,1.2.2,MIT -google-api-client,0.8.7,Apache 2.0 +google-api-client,0.13.6,Apache 2.0 google-protobuf,3.4.0.2,New BSD -googleauth,0.5.1,Apache 2.0 +googleauth,0.5.3,Apache 2.0 got,7.1.0,MIT gpgme,2.0.13,LGPL-2.1+ graceful-fs,4.1.11,ISC @@ -481,14 +457,12 @@ graceful-readlink,1.0.1,MIT grape,1.0.0,MIT grape-entity,0.6.0,MIT grape-route-helpers,2.1.0,MIT -grape_logging,1.6.0,MIT -grpc,1.4.5,New BSD +grape_logging,1.7.0,MIT +grpc,1.6.0,Apache 2.0 gzip-size,3.0.0,MIT hamlit,2.6.1,MIT handle-thing,1.2.5,MIT handlebars,4.0.6,MIT -har-schema,1.0.5,ISC -har-validator,4.2.1,ISC has,1.0.1,MIT has-ansi,2.0.0,MIT has-binary,0.1.7,MIT @@ -496,16 +470,13 @@ has-cors,1.1.0,MIT has-flag,2.0.0,MIT has-symbol-support-x,1.3.0,MIT has-to-string-tag-x,1.3.0,MIT -has-unicode,2.0.1,ISC hash-sum,1.0.2,MIT hash.js,1.0.3,MIT hashie,3.5.6,MIT hashie-forbidden_attributes,0.1.1,MIT -hawk,3.1.3,New BSD he,1.1.1,MIT health_check,2.6.0,MIT hipchat,1.5.2,MIT -hoek,2.16.3,New BSD home-or-tmp,2.0.0,MIT hosted-git-info,2.2.0,ISC hpack.js,2.1.6,MIT @@ -522,7 +493,6 @@ http-errors,1.6.2,MIT http-form_data,1.0.1,MIT http-proxy,1.16.2,MIT http-proxy-middleware,0.17.4,MIT -http-signature,1.1.1,MIT http_parser.rb,0.6.0,MIT httparty,0.13.7,MIT httpclient,2.8.2,ruby @@ -583,15 +553,13 @@ is-resolvable,1.0.0,MIT is-retry-allowed,1.1.0,MIT is-stream,1.1.0,MIT is-svg,2.1.0,MIT -is-typedarray,1.0.0,MIT is-unc-path,0.1.2,MIT is-utf8,0.2.1,MIT is-windows,0.2.0,MIT isarray,1.0.0,MIT isbinaryfile,3.0.2,MIT -isexe,1.1.2,ISC +isexe,2.0.0,ISC isobject,2.1.0,MIT -isstream,0.1.2,MIT istanbul,0.4.5,New BSD istanbul-api,1.1.1,New BSD istanbul-lib-coverage,1.0.1,New BSD @@ -605,7 +573,6 @@ jasmine-core,2.6.3,MIT jasmine-jquery,2.1.1,MIT jed,1.1.1,MIT jira-ruby,1.4.1,MIT -jodid25519,1.0.2,MIT jquery,2.2.1,MIT jquery-atwho-rails,1.3.2,MIT jquery-rails,4.1.1,MIT @@ -615,21 +582,18 @@ js-beautify,1.6.12,MIT js-cookie,2.1.3,MIT js-tokens,3.0.1,MIT js-yaml,3.7.0,MIT -jsbn,0.1.1,MIT jsesc,1.3.0,MIT json,1.8.6,ruby -json-jwt,1.7.1,MIT +json-jwt,1.7.2,MIT json-loader,0.5.7,MIT -json-schema,0.2.3,"AFLv2.1,BSD" json-schema-traverse,0.3.1,MIT json-stable-stringify,1.0.1,MIT json-stringify-safe,5.0.1,ISC -json3,3.3.2,MIT +json3,3.3.2,[Circular] json5,0.5.1,MIT jsonfile,2.4.0,MIT jsonify,0.0.0,Public Domain jsonpointer,4.0.1,MIT -jsprim,1.4.0,MIT jszip,3.1.3,(MIT OR GPL-3.0) jszip-utils,0.0.2,MIT or GPLv3 jwt,1.5.6,MIT @@ -649,7 +613,6 @@ kind-of,3.1.0,MIT klaw,1.3.1,MIT kubeclient,2.2.0,MIT latest-version,1.0.1,MIT -launchy,2.4.3,ISC lazy-cache,1.0.4,MIT lcid,1.0.0,MIT levn,0.3.0,MIT @@ -706,7 +669,7 @@ marked,0.3.6,MIT math-expression-evaluator,1.2.16,MIT media-typer,0.3.0,MIT mem,1.1.0,MIT -memoist,0.15.0,MIT +memoist,0.16.0,MIT memory-fs,0.4.1,MIT meow,3.7.0,MIT merge-descriptors,1.0.1,MIT @@ -714,13 +677,14 @@ method_source,0.8.2,MIT methods,1.1.2,MIT micromatch,2.3.11,MIT miller-rabin,4.0.0,MIT -mime,1.3.4,MIT -mime-db,1.27.0,MIT -mime-types,2.99.3,"MIT,Artistic-2.0,GPL-2.0" +mime,1.3.4,[Circular] +mime-db,1.29.0,MIT +mime-types,3.1,MIT +mime-types-data,3.2016.0521,MIT mimemagic,0.3.0,MIT mimic-fn,1.1.0,MIT mimic-response,1.0.0,MIT -mini_portile2,2.2.0,MIT +mini_portile2,2.3.0,MIT minimalistic-assert,1.0.0,ISC minimatch,3.0.3,ISC minimist,0.0.8,MIT @@ -731,7 +695,7 @@ monaco-editor,0.8.3,MIT mousetrap,1.4.6,Apache 2.0 mousetrap-rails,1.4.6,"MIT,Apache" ms,2.0.0,MIT -multi_json,1.12.1,MIT +multi_json,1.12.2,MIT multi_xml,0.6.0,MIT multicast-dns,6.1.1,MIT multicast-dns-service-types,1.1.0,MIT @@ -739,9 +703,7 @@ multipart-post,2.0.0,MIT mustermann,1.0.0,MIT mustermann-grape,1.0.0,MIT mute-stream,0.0.5,ISC -mysql2,0.4.5,MIT name-all-modules-plugin,1.0.1,MIT -nan,2.6.2,MIT natural-compare,1.4.0,MIT negotiator,0.6.1,MIT nested-error-stacks,1.0.2,MIT @@ -751,22 +713,19 @@ netrc,0.11.0,MIT node-dir,0.1.17,MIT node-forge,0.6.33,BSD node-libs-browser,2.0.0,MIT -node-pre-gyp,0.6.36,New BSD nodemon,1.11.0,MIT -nokogiri,1.8.0,MIT +nokogiri,1.8.1,MIT nopt,3.0.6,ISC -normalize-package-data,2.4.0,Simplified BSD +normalize-package-data,2.3.5,Simplified BSD normalize-path,2.1.1,MIT normalize-range,0.1.2,MIT normalize-url,1.9.1,MIT npm-run-path,2.0.2,MIT -npmlog,4.1.0,ISC null-check,1.0.0,MIT num2fraction,1.2.2,MIT number-is-nan,1.0.1,MIT numerizer,0.1.1,MIT oauth,0.5.1,MIT -oauth-sign,0.8.2,Apache 2.0 oauth2,1.4.0,MIT object-assign,4.1.1,MIT object-component,0.0.3,unknown @@ -777,7 +736,7 @@ oj,2.17.5,MIT omniauth,1.4.2,MIT omniauth-auth0,1.4.1,MIT omniauth-authentiq,0.3.1,MIT -omniauth-azure-oauth2,0.0.6,MIT +omniauth-azure-oauth2,0.0.9,MIT omniauth-cas3,1.1.4,MIT omniauth-facebook,4.0.0,MIT omniauth-github,1.1.2,MIT @@ -786,7 +745,7 @@ omniauth-google-oauth2,0.5.2,MIT omniauth-kerberos,0.3.0,MIT omniauth-multipassword,0.4.2,MIT omniauth-oauth,1.1.0,MIT -omniauth-oauth2,1.3.1,MIT +omniauth-oauth2,1.4.0,MIT omniauth-oauth2-generic,0.2.2,MIT omniauth-saml,1.7.0,MIT omniauth-shibboleth,1.2.1,MIT @@ -839,13 +798,11 @@ pbkdf2,3.0.9,MIT peek,1.0.1,MIT peek-gc,0.0.2,MIT peek-host,1.0.0,MIT -peek-mysql2,1.1.0,MIT peek-performance_bar,1.3.0,MIT peek-pg,1.3.0,MIT peek-rblineprof,0.2.0,MIT peek-redis,1.2.0,MIT peek-sidekiq,1.0.3,MIT -performance-now,0.2.0,MIT pg,0.18.4,"BSD,ruby,GPL" pify,2.3.0,MIT pikaday,1.5.1,"BSD,MIT" @@ -910,6 +867,7 @@ prr,0.0.0,MIT ps-tree,1.1.0,MIT pseudomap,1.0.2,ISC public-encrypt,4.0.0,MIT +public_suffix,3.0.0,MIT punycode,1.4.1,MIT pyu-ruby-sasl,0.0.3.3,MIT q,1.5.0,MIT @@ -917,7 +875,7 @@ qjobs,1.1.5,MIT qs,6.5.0,New BSD query-string,4.3.2,MIT querystring,0.2.0,MIT -querystring-es3,0.2.1,MIT +querystring-es3,0.2.1,[Circular] querystringify,0.0.4,MIT rack,1.6.8,MIT rack-accept,0.4.5,MIT @@ -935,7 +893,7 @@ rails-i18n,4.0.9,MIT railties,4.2.8,MIT rainbow,2.2.2,MIT raindrops,0.18.0,LGPL-2.1+ -rake,12.0.0,MIT +rake,12.1.0,MIT randomatic,1.1.6,MIT randombytes,2.0.3,MIT range-parser,1.2.0,MIT @@ -982,7 +940,7 @@ remove-trailing-separator,1.1.0,ISC repeat-element,1.1.2,MIT repeat-string,1.6.1,MIT repeating,2.0.1,MIT -request,2.81.0,Apache 2.0 +representable,3.0.4,MIT request_store,1.3.1,MIT require-directory,2.1.1,MIT require-from-string,1.2.1,MIT @@ -994,7 +952,7 @@ resolve-from,1.0.1,MIT responders,2.3.0,MIT rest-client,2.0.0,MIT restore-cursor,1.0.1,MIT -retriable,1.4.1,MIT +retriable,3.1.1,MIT right-align,0.1.3,MIT rimraf,2.6.1,ISC rinku,2.0.0,ISC @@ -1013,7 +971,7 @@ rufus-scheduler,3.4.0,MIT rugged,0.26.0,MIT run-async,0.1.0,MIT rx-lite,3.1.2,Apache 2.0 -safe-buffer,5.1.1,MIT +safe-buffer,5.0.1,MIT safe_yaml,1.0.4,MIT sanitize,2.1.0,MIT sass,3.4.22,MIT @@ -1053,7 +1011,6 @@ slack-notifier,1.5.1,MIT slash,1.0.0,MIT slice-ansi,0.0.4,MIT slide,1.1.6,ISC -sntp,1.0.9,BSD socket.io,1.7.3,MIT socket.io-adapter,0.5.0,MIT socket.io-client,1.7.3,MIT @@ -1074,7 +1031,6 @@ sprintf-js,1.0.3,New BSD sprockets,3.7.1,MIT sprockets-rails,3.2.0,MIT sql.js,0.4.0,MIT -sshpk,1.13.0,MIT state_machines,0.4.0,MIT state_machines-activemodel,0.4.0,MIT state_machines-activerecord,0.4.0,MIT @@ -1085,22 +1041,20 @@ stream-http,2.6.3,MIT stream-shift,1.0.0,MIT strict-uri-encode,1.1.0,MIT string-length,1.0.1,MIT -string-width,2.0.0,MIT -string_decoder,1.0.3,MIT +string-width,1.0.2,MIT +string_decoder,0.10.31,MIT stringex,2.7.1,MIT -stringstream,0.0.5,MIT strip-ansi,3.0.1,MIT strip-bom,3.0.0,MIT strip-eof,1.0.0,MIT strip-indent,1.0.1,MIT strip-json-comments,2.0.1,MIT -supports-color,4.2.1,MIT +supports-color,3.2.3,MIT +svg4everybody,2.1.9,CC0-1.0 svgo,0.7.2,MIT sys-filesystem,1.1.6,Artistic 2.0 table,3.8.3,New BSD tapable,0.2.8,MIT -tar,2.2.1,ISC -tar-pack,3.4.0,Simplified BSD temple,0.7.7,MIT test-exclude,4.0.0,ISC text,1.3.1,MIT @@ -1113,6 +1067,7 @@ three-stl-loader,1.0.4,MIT through,2.3.8,MIT thunky,0.1.0,unknown tilt,2.0.6,MIT +time-stamp,2.0.0,MIT timeago.js,2.0.5,MIT timed-out,4.0.1,MIT timers-browserify,2.0.4,MIT @@ -1124,31 +1079,28 @@ to-arraybuffer,1.0.1,MIT to-fast-properties,1.0.2,MIT toml-rb,0.3.15,MIT touch,1.0.0,ISC -tough-cookie,2.3.2,New BSD traverse,0.6.6,MIT trim-newlines,1.0.0,MIT trim-right,1.0.1,MIT truncato,0.7.10,MIT tryit,1.0.3,MIT tty-browserify,0.0.0,MIT -tunnel-agent,0.6.0,Apache 2.0 -tweetnacl,0.14.5,Unlicense type-check,0.3.2,MIT type-is,1.6.15,MIT typedarray,0.0.6,MIT tzinfo,1.2.3,MIT u2f,0.2.1,MIT +uber,0.1.0,MIT uglifier,2.7.2,MIT uglify-js,2.8.29,Simplified BSD uglify-to-browserify,1.0.2,MIT uglifyjs-webpack-plugin,0.4.6,MIT -uid-number,0.0.6,ISC ultron,1.1.0,MIT unc-path-regex,0.1.2,MIT undefsafe,0.0.3,MIT / http://rem.mit-license.org underscore,1.8.3,MIT unf,0.1.4,BSD -unf_ext,0.0.7.2,MIT +unf_ext,0.0.7.4,MIT unicorn,5.1.0,ruby unicorn-worker-killer,0.4.4,ruby uniq,1.0.1,MIT @@ -1166,13 +1118,12 @@ user-home,2.0.0,MIT useragent,2.2.1,MIT util,0.10.3,MIT util-deprecate,1.0.2,MIT -utils-merge,1.0.0,MIT -uuid,3.0.1,MIT +utils-merge,1.0.0,[Circular] +uuid,2.0.3,MIT validate-npm-package-license,3.0.1,Apache 2.0 validates_hostname,1.0.6,MIT vary,1.1.1,MIT vendors,1.0.1,MIT -verror,1.3.6,MIT version_sorter,2.1.0,MIT virtus,1.0.5,MIT visibilityjs,1.2.4,MIT @@ -1200,12 +1151,11 @@ webpack-stats-plugin,0.1.5,MIT websocket-driver,0.6.5,MIT websocket-extensions,0.1.1,MIT whet.extend,0.9.9,MIT -which,1.2.12,ISC +which,1.3.0,ISC which-module,2.0.0,ISC -wide-align,1.1.2,ISC wikicloth,0.8.1,MIT window-size,0.1.0,MIT -wordwrap,0.0.2,MIT/X11 +wordwrap,1.0.0,MIT wrap-ansi,2.1.0,MIT wrappy,1.0.2,ISC write,0.2.1,MIT |