diff options
357 files changed, 5146 insertions, 1849 deletions
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index 9b541aadad1..102eb7e7953 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -11,4 +11,6 @@ See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#ch - [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location. - [ ] Make sure internal links pointing to the document in question are not broken. - [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory. +- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/doc_styleguide.html#redirections-for-pages-with-disqus-comments) to the new document if there are any Disqus comments on the old document thread. - [ ] If working on CE, submit an MR to EE with the changes as well. +- [ ] Ping one of the technical writers for review. diff --git a/CHANGELOG.md b/CHANGELOG.md index c29c289310d..075790d687c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.3.5 (2018-01-18) + +- No changes. + ## 10.3.4 (2018-01-10) ### Security (7 changes, 1 of them is from the community) @@ -193,6 +197,10 @@ entry. - Clean up schema of the "merge_requests" table. +## 10.2.7 (2018-01-18) + +- No changes. + ## 10.2.6 (2018-01-11) ### Security (9 changes, 1 of them is from the community) @@ -992,6 +1000,11 @@ entry. - Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi) - [BUGIFX] Improves subgroup creation permissions. !13418 +## 9.5.10 (2017-11-08) + +- [SECURITY] Add SSRF protections for hostnames that will never resolve but will still connect to localhost +- [SECURITY] Include X-Content-Type-Options (XCTO) header into API responses + ## 9.5.9 (2017-10-16) - [SECURITY] Move project repositories between namespaces when renaming users. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 328185caaeb..534b316aef6 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.67.0 +0.70.0 @@ -115,7 +115,7 @@ gem 'google-api-client', '~> 0.13.6' gem 'unf', '~> 0.1.4' # Seed data -gem 'seed-fu', '2.3.6' # Upgrade to > 2.3.7 once https://github.com/mbleigh/seed-fu/issues/123 is solved +gem 'seed-fu', '~> 2.3.7' # Markdown and HTML processing gem 'html-pipeline', '~> 1.11.0' @@ -233,6 +233,9 @@ gem 'charlock_holmes', '~> 0.7.5' # Faster JSON gem 'oj', '~> 2.17.4' +# Faster blank +gem 'fast_blank' + # Parse time & duration gem 'chronic', '~> 0.10.2' gem 'chronic_duration', '~> 0.10.6' @@ -403,7 +406,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.69.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.73.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 8e31ac1f993..b251b8c481a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -207,6 +207,7 @@ GEM faraday_middleware-multi_json (0.0.6) faraday_middleware multi_json + fast_blank (1.0.0) fast_gettext (1.4.0) ffaker (2.4.0) ffi (1.9.18) @@ -284,7 +285,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.69.0) + gitaly-proto (0.73.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -831,7 +832,7 @@ GEM rake (>= 0.9, < 13) sass (~> 3.5.3) securecompare (1.0.0) - seed-fu (2.3.6) + seed-fu (2.3.7) activerecord (>= 3.1) activesupport (>= 3.1) select2-rails (3.5.9.3) @@ -1034,6 +1035,7 @@ DEPENDENCIES email_spec (~> 1.6.0) factory_bot_rails (~> 4.8.2) faraday (~> 0.12) + fast_blank ffaker (~> 2.4) flay (~> 2.8.0) flipper (~> 0.11.0) @@ -1054,7 +1056,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.69.0) + gitaly-proto (~> 0.73.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) @@ -1170,7 +1172,7 @@ DEPENDENCIES sanitize (~> 2.0) sass-rails (~> 5.0.6) scss_lint (~> 0.56.0) - seed-fu (= 2.3.6) + seed-fu (~> 2.3.7) select2-rails (~> 3.5.9) selenium-webdriver (~> 3.5) sentry-raven (~> 2.5.3) diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js index 2cd3c146f11..65315979df7 100644 --- a/app/assets/javascripts/boards/utils/query_data.js +++ b/app/assets/javascripts/boards/utils/query_data.js @@ -5,7 +5,7 @@ export default (path, extraData) => path.split('&').reduce((dataParam, filterPar const paramSplit = filterParam.split('='); const paramKeyNormalized = paramSplit[0].replace('[]', ''); const isArray = paramSplit[0].indexOf('[]'); - const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' '); + const value = decodeURIComponent(paramSplit[1].replace(/\+/g, ' ')); if (isArray !== -1) { if (!data[paramKeyNormalized]) { diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index a9e819b8a3c..843564ce016 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -1,11 +1,15 @@ <script> import actionBtn from './action_btn.vue'; import { getTimeago } from '../../lib/utils/datetime_utility'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { components: { actionBtn, }, + directives: { + tooltip, + }, props: { deployKey: { type: Object, @@ -32,6 +36,9 @@ isEnabled(id) { return this.store.findEnabledKey(id) !== undefined; }, + tooltipTitle(project) { + return project.can_push ? 'Write access allowed' : 'Read access only'; + }, }, }; </script> @@ -52,21 +59,23 @@ <div class="description"> {{ deployKey.fingerprint }} </div> - <div - v-if="deployKey.can_push" - class="write-access-allowed" - > - Write access allowed - </div> </div> <div class="deploy-key-content prepend-left-default deploy-key-projects"> <a - v-for="(project, i) in deployKey.projects" - class="label deploy-project-label" - :href="project.full_path" + v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects" :key="i" + class="label deploy-project-label" + :href="deployKeysProject.project.full_path" + :title="tooltipTitle(deployKeysProject)" + v-tooltip > - {{ project.full_name }} + {{ deployKeysProject.project.full_name }} + <i + v-if="!deployKeysProject.can_push" + aria-hidden="true" + class="fa fa-lock" + > + </i> </a> </div> <div class="deploy-key-content"> diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 28014946a39..b7b0162e307 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,7 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ -import { s__ } from './locale'; import projectSelect from './project_select'; -import IssuableIndex from './issuable_index'; import Milestone from './milestone'; import IssuableForm from './issuable_form'; import LabelsSelect from './labels_select'; @@ -11,34 +9,24 @@ import notificationsDropdown from './notifications_dropdown'; import groupAvatar from './group_avatar'; import GroupLabelSubscription from './group_label_subscription'; import LineHighlighter from './line_highlighter'; -import NewCommitForm from './new_commit_form'; import Project from './project'; import projectAvatar from './project_avatar'; import MergeRequest from './merge_request'; import Compare from './compare'; -import initCompareAutocomplete from './compare_autocomplete'; -import ProjectFindFile from './project_find_file'; import ProjectNew from './project_new'; -import projectImport from './project_import'; import Labels from './labels'; import LabelManager from './label_manager'; import Sidebar from './right_sidebar'; import IssuableTemplateSelectors from './templates/issuable_template_selectors'; import Flash from './flash'; -import CommitsList from './commits'; -import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import SecretValues from './behaviors/secret_values'; import Group from './group'; import ProjectsList from './projects_list'; -import setupProjectEdit from './project_edit'; -import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import UserCallout from './user_callout'; import ShortcutsWiki from './shortcuts_wiki'; import BlobViewer from './blob/viewer/index'; -import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import UsersSelect from './users_select'; -import RefSelectDropdown from './ref_select_dropdown'; import GfmAutoComplete from './gfm_auto_complete'; import Star from './star'; import TreeView from './tree'; @@ -49,25 +37,20 @@ import PerformanceBar from './performance_bar'; import initNotes from './init_notes'; import initIssuableSidebar from './init_issuable_sidebar'; import initProjectVisibilitySelector from './project_visibility'; -import GpgBadges from './gpg_badges'; -import initChangesDropdown from './init_changes_dropdown'; import NewGroupChild from './groups/new_group_child'; import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; import GlFieldErrors from './gl_field_errors'; import GLForm from './gl_form'; import Shortcuts from './shortcuts'; import ShortcutsNavigation from './shortcuts_navigation'; -import ShortcutsFindFile from './shortcuts_find_file'; import ShortcutsIssuable from './shortcuts_issuable'; import U2FAuthenticate from './u2f/authenticate'; import Members from './members'; import memberExpirationDate from './member_expiration_date'; -import DueDateSelectors from './due_date_select'; import Diff from './diff'; import ProjectLabelSubscription from './project_label_subscription'; import SearchAutocomplete from './search_autocomplete'; import Activities from './activities'; -import { fetchCommitMergeRequests } from './commit_merge_requests'; (function() { var Dispatcher; @@ -127,21 +110,16 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; shortcut_handler = true; break; case 'projects:issues:index': - if (filteredSearchEnabled) { - const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests'); - filteredSearchManager.setup(); - } - const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_'; - new IssuableIndex(pagePrefix); - - shortcut_handler = new ShortcutsNavigation(); - new UsersSelect(); + import('./pages/projects/issues/index') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:issues:show': - new Issue(); - shortcut_handler = new ShortcutsIssuable(); - new ZenMode(); - initIssuableSidebar(); + import('./pages/projects/issues/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'dashboard:milestones:index': import('./pages/dashboard/milestones/index') @@ -179,6 +157,11 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; case 'dashboard:todos:index': import('./pages/dashboard/todos/index').then(callDefault).catch(fail); break; + case 'admin:jobs:index': + import('./pages/admin/jobs/index') + .then(callDefault) + .catch(fail); + break; case 'dashboard:projects:index': case 'dashboard:projects:starred': import('./pages/dashboard/projects') @@ -198,23 +181,33 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; .catch(fail); break; case 'projects:milestones:new': + case 'projects:milestones:create': + import('./pages/projects/milestones/new') + .then(callDefault) + .catch(fail); + break; case 'projects:milestones:edit': case 'projects:milestones:update': - new ZenMode(); - new DueDateSelectors(); - new GLForm($('.milestone-form'), true); + import('./pages/projects/milestones/edit') + .then(callDefault) + .catch(fail); break; case 'groups:milestones:new': + case 'groups:milestones:create': + import('./pages/groups/milestones/new') + .then(callDefault) + .catch(fail); + break; case 'groups:milestones:edit': case 'groups:milestones:update': - new ZenMode(); - new DueDateSelectors(); - new GLForm($('.milestone-form'), false); + import('./pages/groups/milestones/edit') + .then(callDefault) + .catch(fail); break; case 'projects:compare:show': - new Diff(); - const paddingTop = 16; - initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); + import('./pages/projects/compare/show') + .then(callDefault) + .catch(fail); break; case 'projects:branches:new': import('./pages/projects/branches/new') @@ -232,13 +225,16 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; .catch(fail); break; case 'projects:issues:new': + import('./pages/projects/issues/new') + .then(callDefault) + .catch(fail); + shortcut_handler = true; + break; case 'projects:issues:edit': - shortcut_handler = new ShortcutsNavigation(); - new GLForm($('.issue-form'), true); - new IssuableForm($('.issue-form')); - new LabelsSelect(); - new MilestoneSelect(); - new IssuableTemplateSelectors(); + import('./pages/projects/issues/edit') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:merge_requests:creations:new': const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); @@ -263,12 +259,11 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; new LabelsSelect(); new MilestoneSelect(); new IssuableTemplateSelectors(); - new AutoWidthDropdownSelect($('.js-target-branch-select')).init(); break; case 'projects:tags:new': - new ZenMode(); - new GLForm($('.tag-form'), true); - new RefSelectDropdown($('.js-branch-select')); + import('./pages/projects/tags/new') + .then(callDefault) + .catch(fail); break; case 'projects:snippets:show': initNotes(); @@ -325,23 +320,15 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; .catch(fail); break; case 'projects:commit:show': - new Diff(); - new ZenMode(); - shortcut_handler = new ShortcutsNavigation(); - new MiniPipelineGraph({ - container: '.js-commit-pipeline-graph', - }).bindEvents(); - initNotes(); - const stickyBarPaddingTop = 16; - initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); - $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); - fetchCommitMergeRequests(); + import('./pages/projects/commit/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:commit:pipelines': - new MiniPipelineGraph({ - container: '.js-commit-pipeline-graph', - }).bindEvents(); - $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + import('./pages/projects/commit/pipelines') + .then(callDefault) + .catch(fail); break; case 'projects:activity': import('./pages/projects/activity') @@ -350,9 +337,10 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; shortcut_handler = true; break; case 'projects:commits:show': - CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); - shortcut_handler = new ShortcutsNavigation(); - GpgBadges.fetch(); + import('./pages/projects/commits/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:show': shortcut_handler = new ShortcutsNavigation(); @@ -370,12 +358,14 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; }); break; case 'projects:edit': - setupProjectEdit(); - // Initialize expandable settings panels - initSettingsPanels(); + import('./pages/projects/edit') + .then(callDefault) + .catch(fail); break; case 'projects:imports:show': - projectImport(); + import('./pages/projects/imports/show') + .then(callDefault) + .catch(fail); break; case 'projects:pipelines:new': case 'projects:pipelines:create': @@ -435,22 +425,15 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; groupAvatar(); break; case 'projects:tree:show': - shortcut_handler = new ShortcutsNavigation(); - new TreeView(); - new BlobViewer(); - new NewCommitForm($('.js-create-dir-form')); - $('#tree-slider').waitForImages(function() { - ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); - }); + import('./pages/projects/tree/show') + .then(callDefault) + .catch(fail); + shortcut_handler = true; break; case 'projects:find_file:show': - const findElement = document.querySelector('.js-file-finder'); - const projectFindFile = new ProjectFindFile($(".file-finder-holder"), { - url: findElement.dataset.fileFindUrl, - treeUrl: findElement.dataset.findTreeUrl, - blobUrlTemplate: findElement.dataset.blobUrlTemplate, - }); - new ShortcutsFindFile(projectFindFile); + import('./pages/projects/find_file/show') + .then(callDefault) + .catch(fail); shortcut_handler = true; break; case 'projects:blob:show': @@ -504,7 +487,7 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; shortcut_handler = true; break; case 'projects:forks:new': - import(/* webpackChunkName: 'project_fork' */ './project_fork') + import('./pages/projects/forks/new') .then(callDefault) .catch(fail); break; @@ -579,20 +562,16 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; .catch(fail); break; case 'projects:clusters:show': - import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle') - .then(cluster => new cluster.default()) // eslint-disable-line new-cap - .catch((err) => { - Flash(s__('ClusterIntegration|Problem setting up the cluster')); - throw err; - }); + case 'projects:clusters:update': + case 'projects:clusters:destroy': + import('./pages/projects/clusters/show') + .then(callDefault) + .catch(fail); break; case 'projects:clusters:index': - import(/* webpackChunkName: "clusters_index" */ './clusters/clusters_index') - .then(clusterIndex => clusterIndex.default()) - .catch((err) => { - Flash(s__('ClusterIntegration|Problem setting up the clusters list')); - throw err; - }); + import('./pages/projects/clusters/index') + .then(callDefault) + .catch(fail); break; } switch (path[0]) { @@ -672,7 +651,9 @@ import { fetchCommitMergeRequests } from './commit_merge_requests'; projectAvatar(); switch (path[1]) { case 'compare': - initCompareAutocomplete(); + import('./pages/projects/compare') + .then(callDefault) + .catch(fail); break; case 'edit': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js index 2203a56315e..14a2bfbe4e0 100644 --- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js +++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js @@ -11,6 +11,14 @@ class AutoWidthDropdownSelect { const dropdownClass = this.dropdownClass; this.$selectElement.select2({ dropdownCssClass: dropdownClass, + ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass), + }); + + return this; + } + + static selectOptions(dropdownClass) { + return { dropdownCss() { let resultantWidth = 'auto'; const $dropdown = $(`.${dropdownClass}`); @@ -29,9 +37,7 @@ class AutoWidthDropdownSelect { maxWidth: offsetParentWidth, }; }, - }); - - return this; + }; } } diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 57dcaa0e1ac..fdfad0b6a4f 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -6,6 +6,7 @@ import Autosave from './autosave'; import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; import ZenMode from './zen_mode'; +import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; export default class IssuableForm { @@ -46,6 +47,12 @@ export default class IssuableForm { }); calendar.setDate(parsePikadayDate($issuableDueDate.val())); } + + this.$targetBranchSelect = $('.js-target-branch-select', this.form); + + if (this.$targetBranchSelect.length) { + this.initTargetBranchDropdown(); + } } initAutosave() { @@ -104,4 +111,37 @@ export default class IssuableForm { addWip() { this.titleField.val(`WIP: ${(this.titleField.val())}`); } + + initTargetBranchDropdown() { + this.$targetBranchSelect.select2({ + ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'), + ajax: { + url: this.$targetBranchSelect.data('endpoint'), + dataType: 'JSON', + quietMillis: 250, + data(search) { + return { + search, + }; + }, + results(data) { + return { + // `data` keys are translated so we can't just access them with a string based key + results: data[Object.keys(data)[0]].map(name => ({ + id: name, + text: name, + })), + }; + }, + }, + initSelection(el, callback) { + const val = el.val(); + + callback({ + id: val, + text: val, + }); + }, + }); + } } diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index 8f32dcc94e2..9b5092c5e3f 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -3,7 +3,6 @@ import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; import { numberToHumanSize } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; -import { timeFor } from './lib/utils/datetime_utility'; export default class Job { constructor(options) { @@ -71,7 +70,6 @@ export default class Job { .off('resize.build') .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); - this.updateArtifactRemoveDate(); this.initAffixTopArea(); this.getBuildTrace(); @@ -261,16 +259,7 @@ export default class Job { sidebarOnClick() { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); } - // eslint-disable-next-line class-methods-use-this, consistent-return - updateArtifactRemoveDate() { - const $date = $('.js-artifacts-remove'); - if ($date.length) { - const date = $date.text(); - return $date.text( - timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3'))), - ); - } - } + // eslint-disable-next-line class-methods-use-this populateJobs(stage) { $('.build-job').hide(); diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index 321a4872ccc..357bc9aab17 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -76,6 +76,7 @@ <loading-icon v-if="isLoading" size="2" + class="prepend-top-default append-bottom-default" /> </div> </template> diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f7a1c9f1e40..664e793fc8e 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -231,7 +231,7 @@ export default class LabelsSelect { selectedClass.push('label-item'); $a.attr('data-label-id', label.id); } - $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title); + $a.addClass(selectedClass.join(' ')).html(`${colorEl} ${_.escape(label.title)}`); // Return generated html return $li.html($a).prop('outerHTML'); }, diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index cb3cdea8111..bedd50de1bb 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */ import 'vendor/jquery.waitforimages'; +import { __ } from '~/locale'; import TaskList from './task_list'; import MergeRequestTabs from './merge_request_tabs'; import IssuablesHelper from './helpers/issuables_helper'; @@ -110,22 +111,22 @@ MergeRequest.prototype.initCommitMessageListeners = function() { }); }; -MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) { +MergeRequest.setStatusBoxToMerged = function() { $('.detail-page-header .status-box') - .removeClass(classToRemove) - .addClass(classToAdd) + .removeClass('status-box-open') + .addClass('status-box-mr-merged') .find('span') - .text(newStatusText); + .text(__('Merged')); }; -MergeRequest.prototype.decreaseCounter = function(by = 1) { - const $el = $('.nav-links .js-merge-counter'); +MergeRequest.decreaseCounter = function(by = 1) { + const $el = $('.js-merge-counter'); const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); $el.text(addDelimiter(count)); }; -MergeRequest.prototype.hideCloseButton = function() { +MergeRequest.hideCloseButton = function() { const el = document.querySelector('.merge-request .js-issuable-actions'); const closeDropdownItem = el.querySelector('li.close-item'); if (closeDropdownItem) { diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index d0ec70f1fcf..3d09d24b6ab 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,6 +1,7 @@ <script> /* global katex */ import marked from 'marked'; + import sanitize from 'sanitize-html'; import Prompt from './prompt.vue'; const renderer = new marked.Renderer(); @@ -82,7 +83,12 @@ }, computed: { markdown() { - return marked(this.cell.source.join('').replace(/\\/g, '\\\\')); + return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), { + allowedTags: false, + allowedAttributes: { + '*': ['class'], + }, + }); }, }, }; diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index ebba5954de9..0535ee7afa8 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -1,4 +1,5 @@ <script> + import sanitize from 'sanitize-html'; import Prompt from '../prompt.vue'; export default { @@ -11,12 +12,24 @@ required: true, }, }, + computed: { + sanitizedOutput() { + return sanitize(this.rawCode, { + allowedTags: sanitize.defaults.allowedTags.concat([ + 'img', 'svg', + ]), + allowedAttributes: { + img: ['src'], + }, + }); + }, + }, }; </script> <template> <div class="output"> <prompt /> - <div v-html="rawCode"></div> + <div v-html="sanitizedOutput"></div> </div> </template> diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue new file mode 100644 index 00000000000..555725cbe12 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -0,0 +1,47 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + import Flash from '~/flash'; + import modal from '~/vue_shared/components/modal.vue'; + import { s__ } from '~/locale'; + import { redirectTo } from '~/lib/utils/url_utility'; + + export default { + components: { + modal, + }, + props: { + url: { + type: String, + required: true, + }, + }, + computed: { + text() { + return s__('AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.'); + }, + }, + methods: { + onSubmit() { + return axios.post(this.url) + .then((response) => { + // follow the rediect to refresh the page + redirectTo(response.request.responseURL); + }) + .catch((error) => { + Flash(s__('AdminArea|Stopping jobs failed')); + throw error; + }); + }, + }, + }; +</script> + +<template> + <modal + id="stop-jobs-modal" + :title="s__('AdminArea|Stop all jobs?')" + :text="text" + kind="danger" + :primary-button-label="s__('AdminArea|Stop jobs')" + @submit="onSubmit" /> +</template> diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js new file mode 100644 index 00000000000..0e004bd9174 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/index.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; + +import Translate from '~/vue_shared/translate'; + +import stopJobsModal from './components/stop_jobs_modal.vue'; + +Vue.use(Translate); + +export default () => { + const stopJobsButton = document.getElementById('stop-jobs-button'); + + // eslint-disable-next-line no-new + new Vue({ + el: '#stop-jobs-modal', + components: { + stopJobsModal, + }, + mounted() { + stopJobsButton.classList.remove('disabled'); + }, + render(createElement) { + return createElement('stop-jobs-modal', { + props: { + url: stopJobsButton.dataset.url, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/groups/milestones/edit/index.js b/app/assets/javascripts/pages/groups/milestones/edit/index.js new file mode 100644 index 00000000000..5c99c90e24d --- /dev/null +++ b/app/assets/javascripts/pages/groups/milestones/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(false); diff --git a/app/assets/javascripts/pages/groups/milestones/new/index.js b/app/assets/javascripts/pages/groups/milestones/new/index.js new file mode 100644 index 00000000000..5c99c90e24d --- /dev/null +++ b/app/assets/javascripts/pages/groups/milestones/new/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(false); diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js new file mode 100644 index 00000000000..d531ab81dc7 --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/index/index.js @@ -0,0 +1,5 @@ +import ClustersIndex from '~/clusters/clusters_index'; + +export default () => { + new ClustersIndex(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js new file mode 100644 index 00000000000..0458c02a66f --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/show/index.js @@ -0,0 +1,5 @@ +import ClustersBundle from '~/clusters/clusters_bundle'; + +export default () => { + new ClustersBundle(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js new file mode 100644 index 00000000000..523ad567021 --- /dev/null +++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js @@ -0,0 +1,8 @@ +import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; + +export default () => { + new MiniPipelineGraph({ + container: '.js-commit-pipeline-graph', + }).bindEvents(); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); +}; diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js new file mode 100644 index 00000000000..5ac38e6f278 --- /dev/null +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -0,0 +1,22 @@ +/* eslint-disable no-new */ +import Diff from '~/diff'; +import ZenMode from '~/zen_mode'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; +import initNotes from '~/init_notes'; +import initChangesDropdown from '~/init_changes_dropdown'; +import { fetchCommitMergeRequests } from '~/commit_merge_requests'; + +export default () => { + new Diff(); + new ZenMode(); + new ShortcutsNavigation(); + new MiniPipelineGraph({ + container: '.js-commit-pipeline-graph', + }).bindEvents(); + initNotes(); + const stickyBarPaddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop); + $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + fetchCommitMergeRequests(); +}; diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js new file mode 100644 index 00000000000..90b5882a24f --- /dev/null +++ b/app/assets/javascripts/pages/projects/commits/show/index.js @@ -0,0 +1,9 @@ +import CommitsList from '~/commits'; +import GpgBadges from '~/gpg_badges'; +import ShortcutsNavigation from '~/shortcuts_navigation'; + +export default () => { + CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); + new ShortcutsNavigation(); // eslint-disable-line no-new + GpgBadges.fetch(); +}; diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js new file mode 100644 index 00000000000..890062eeee6 --- /dev/null +++ b/app/assets/javascripts/pages/projects/compare/index.js @@ -0,0 +1,5 @@ +import initCompareAutocomplete from '~/compare_autocomplete'; + +export default () => { + initCompareAutocomplete(); +}; diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js new file mode 100644 index 00000000000..6b8d4503568 --- /dev/null +++ b/app/assets/javascripts/pages/projects/compare/show/index.js @@ -0,0 +1,8 @@ +import Diff from '~/diff'; +import initChangesDropdown from '~/init_changes_dropdown'; + +export default () => { + new Diff(); // eslint-disable-line no-new + const paddingTop = 16; + initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop); +}; diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js new file mode 100644 index 00000000000..7f662ef6b6a --- /dev/null +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -0,0 +1,8 @@ +import initSettingsPanels from '~/settings_panels'; +import setupProjectEdit from '~/project_edit'; + +export default () => { + setupProjectEdit(); + // Initialize expandable settings panels + initSettingsPanels(); +}; diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js new file mode 100644 index 00000000000..42bde0ff779 --- /dev/null +++ b/app/assets/javascripts/pages/projects/find_file/show/index.js @@ -0,0 +1,12 @@ +import ProjectFindFile from '~/project_find_file'; +import ShortcutsFindFile from '~/shortcuts_find_file'; + +export default () => { + const findElement = document.querySelector('.js-file-finder'); + const projectFindFile = new ProjectFindFile($('.file-finder-holder'), { + url: findElement.dataset.fileFindUrl, + treeUrl: findElement.dataset.findTreeUrl, + blobUrlTemplate: findElement.dataset.blobUrlTemplate, + }); + new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js new file mode 100644 index 00000000000..7825eb01949 --- /dev/null +++ b/app/assets/javascripts/pages/projects/forks/new/index.js @@ -0,0 +1,5 @@ +import ProjectFork from '~/project_fork'; + +export default () => { + new ProjectFork(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/imports/show/index.js b/app/assets/javascripts/pages/projects/imports/show/index.js new file mode 100644 index 00000000000..378f7b3f38b --- /dev/null +++ b/app/assets/javascripts/pages/projects/imports/show/index.js @@ -0,0 +1,5 @@ +import ProjectImport from '~/project_import'; + +export default () => { + new ProjectImport(); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js new file mode 100644 index 00000000000..7f27f379d8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/edit/index.js @@ -0,0 +1,5 @@ +import initForm from '../form'; + +export default () => { + initForm(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js new file mode 100644 index 00000000000..5c7daf84738 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -0,0 +1,16 @@ +/* eslint-disable no-new */ +import GLForm from '~/gl_form'; +import IssuableForm from '~/issuable_form'; +import LabelsSelect from '~/labels_select'; +import MilestoneSelect from '~/milestone_select'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; + +export default () => { + new ShortcutsNavigation(); + new GLForm($('.issue-form'), true); + new IssuableForm($('.issue-form')); + new LabelsSelect(); + new MilestoneSelect(); + new IssuableTemplateSelectors(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js new file mode 100644 index 00000000000..fd395a45f00 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -0,0 +1,18 @@ + +/* eslint-disable no-new */ + +import IssuableIndex from '~/issuable_index'; +import ShortcutsNavigation from '~/shortcuts_navigation'; +import UsersSelect from '~/users_select'; + +export default () => { + const filteredSearchEnabled = gl.FilteredSearchManager && document.querySelector('.filtered-search'); + if (filteredSearchEnabled) { + const filteredSearchManager = new gl.FilteredSearchManager('issues'); + filteredSearchManager.setup(); + } + new IssuableIndex('issue_'); + + new ShortcutsNavigation(); + new UsersSelect(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/new/index.js b/app/assets/javascripts/pages/projects/issues/new/index.js new file mode 100644 index 00000000000..7f27f379d8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/new/index.js @@ -0,0 +1,5 @@ +import initForm from '../form'; + +export default () => { + initForm(); +}; diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js new file mode 100644 index 00000000000..48ed8fb2243 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -0,0 +1,13 @@ + +/* eslint-disable no-new */ +import initIssuableSidebar from '~/init_issuable_sidebar'; +import Issue from '~/issue'; +import ShortcutsIssuable from '~/shortcuts_issuable'; +import ZenMode from '~/zen_mode'; + +export default () => { + new Issue(); + new ShortcutsIssuable(); + new ZenMode(); + initIssuableSidebar(); +}; diff --git a/app/assets/javascripts/pages/projects/milestones/edit/index.js b/app/assets/javascripts/pages/projects/milestones/edit/index.js new file mode 100644 index 00000000000..10e3979a36e --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/edit/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(); diff --git a/app/assets/javascripts/pages/projects/milestones/new/index.js b/app/assets/javascripts/pages/projects/milestones/new/index.js new file mode 100644 index 00000000000..10e3979a36e --- /dev/null +++ b/app/assets/javascripts/pages/projects/milestones/new/index.js @@ -0,0 +1,3 @@ +import initForm from '../../../../shared/milestones/form'; + +export default () => initForm(); diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js new file mode 100644 index 00000000000..dacc2875c8c --- /dev/null +++ b/app/assets/javascripts/pages/projects/tags/new/index.js @@ -0,0 +1,9 @@ +import RefSelectDropdown from '../../../../ref_select_dropdown'; +import ZenMode from '../../../../zen_mode'; +import GLForm from '../../../../gl_form'; + +export default () => { + new ZenMode(); // eslint-disable-line no-new + new GLForm($('.tag-form'), true); // eslint-disable-line no-new + new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js new file mode 100644 index 00000000000..28a0160f47d --- /dev/null +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -0,0 +1,15 @@ +import TreeView from '../../../../tree'; +import ShortcutsNavigation from '../../../../shortcuts_navigation'; +import BlobViewer from '../../../../blob/viewer'; +import NewCommitForm from '../../../../new_commit_form'; +import { ajaxGet } from '../../../../lib/utils/common_utils'; + +export default () => { + new ShortcutsNavigation(); // eslint-disable-line no-new + new TreeView(); // eslint-disable-line no-new + new BlobViewer(); // eslint-disable-line no-new + new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new + $('#tree-slider').waitForImages(() => + ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath)); +}; + diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index d44195f6b72..dc621bc87c0 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -15,6 +15,7 @@ export default class Search { $groupDropdown.glDropdown({ selectable: true, filterable: true, + filterRemote: true, fieldName: 'group_id', search: { fields: ['full_name'], @@ -43,6 +44,7 @@ export default class Search { $projectDropdown.glDropdown({ selectable: true, filterable: true, + filterRemote: true, fieldName: 'project_id', search: { fields: ['name'], diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 4ad3f66ee8c..77553ca67cc 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -3,6 +3,7 @@ import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; export default { @@ -11,6 +12,7 @@ }, components: { loadingIcon, + icon, }, props: { endpoint: { @@ -41,9 +43,6 @@ }; }, computed: { - iconClass() { - return `fa fa-${this.icon}`; - }, buttonClass() { return `btn ${this.cssClass}`; }, @@ -76,10 +75,9 @@ data-container="body" data-placement="top" :disabled="isLoading"> - <i - :class="iconClass" - aria-hidden="true"> - </i> + <icon + :name="icon" + /> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 942acc8c412..e08c2092680 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -92,6 +92,7 @@ <loading-icon v-if="isLoading" size="2" + class="prepend-top-default append-bottom-default" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index efda36c12d6..3297af7bde4 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,7 +1,7 @@ <script> - import playIconSvg from 'icons/_icon_play.svg'; import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; export default { @@ -10,6 +10,7 @@ }, components: { loadingIcon, + icon, }, props: { actions: { @@ -19,7 +20,6 @@ }, data() { return { - playIconSvg, isLoading: false, }; }, @@ -52,7 +52,10 @@ aria-label="Manual job" :disabled="isLoading" > - <span v-html="playIconSvg"></span> + <icon + name="play" + class="icon-play" + /> <i class="fa fa-caret-down" aria-hidden="true"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 670b777199c..d87e24cc8a7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -312,7 +312,7 @@ :endpoint="pipeline.cancel_path" css-class="js-pipelines-cancel-button btn-remove" title="Cancel" - icon="remove" + icon="close" confirm-action-message="Are you sure you want to cancel this pipeline?" /> </div> diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/render_mermaid.js index b7cde6fb092..31c7a772cf4 100644 --- a/app/assets/javascripts/render_mermaid.js +++ b/app/assets/javascripts/render_mermaid.js @@ -19,7 +19,11 @@ export default function renderMermaid($els) { import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => { mermaid.initialize({ - loadOnStart: false, + // mermaid core options + mermaid: { + startOnLoad: false, + }, + // mermaidAPI options theme: 'neutral', }); diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js new file mode 100644 index 00000000000..db466f722c4 --- /dev/null +++ b/app/assets/javascripts/shared/milestones/form.js @@ -0,0 +1,9 @@ +import ZenMode from '../../zen_mode'; +import DueDateSelectors from '../../due_date_select'; +import GLForm from '../../gl_form'; + +export default (initGFM = true) => { + new ZenMode(); // eslint-disable-line no-new + new DueDateSelectors(); // eslint-disable-line no-new + new GLForm($('.milestone-form'), initGFM); // eslint-disable-line no-new +}; diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index d2f0d7410da..5351873e945 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -62,7 +62,7 @@ export default class Shortcuts { e.preventDefault(); const performanceBarCookieName = 'perf_bar_enabled'; if (Cookies.get(performanceBarCookieName) === 'true') { - Cookies.remove(performanceBarCookieName, { path: '/' }); + Cookies.set(performanceBarCookieName, 'false', { path: '/' }); } else { Cookies.set(performanceBarCookieName, 'true', { path: '/' }); } 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 e82fb979162..f16414ad5c0 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,6 +1,7 @@ import successSvg from 'icons/_icon_status_success.svg'; import warningSvg from 'icons/_icon_status_warning.svg'; import simplePoll from '~/lib/utils/simple_poll'; +import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon'; import eventHub from '../../event_hub'; @@ -165,11 +166,9 @@ export default { // If state is merged we should update the widget and stop the polling eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('FetchActionsContent'); - if (window.mergeRequest) { - window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged'); - window.mergeRequest.hideCloseButton(); - window.mergeRequest.decreaseCounter(); - } + MergeRequest.setStatusBoxToMerged(); + MergeRequest.hideCloseButton(); + MergeRequest.decreaseCounter(); stopPolling(); // If user checked remove source branch and we didn't remove the branch yet diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/modal.vue index c103c45c7dd..8227428d8ba 100644 --- a/app/assets/javascripts/vue_shared/components/modal.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -122,7 +122,7 @@ > <button type="button" - class="btn pull-left" + class="btn" :class="btnCancelKindClass" @click="emitCancel($event)" data-dismiss="modal" @@ -132,7 +132,7 @@ <button v-if="primaryButtonLabel" type="button" - class="btn pull-right js-primary-button" + class="btn js-primary-button" :disabled="submitDisabled" :class="btnKindClass" @click="emitSubmit($event)" diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue new file mode 100644 index 00000000000..86f06c8d266 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -0,0 +1,127 @@ +<script> +import tooltip from '~/vue_shared/directives/tooltip'; + +export default { + directives: { + tooltip, + }, + props: { + cssClass: { + type: String, + required: false, + default: '', + }, + successLabel: { + type: String, + required: true, + }, + failureLabel: { + type: String, + required: true, + }, + neutralLabel: { + type: String, + required: true, + }, + successCount: { + type: Number, + required: true, + }, + failureCount: { + type: Number, + required: true, + }, + totalCount: { + type: Number, + required: true, + }, + }, + computed: { + neutralCount() { + return this.totalCount - this.successCount - this.failureCount; + }, + successPercent() { + return this.getPercent(this.successCount); + }, + successBarStyle() { + return this.barStyle(this.successPercent); + }, + successTooltip() { + return this.getTooltip(this.successLabel, this.successCount); + }, + failurePercent() { + return this.getPercent(this.failureCount); + }, + failureBarStyle() { + return this.barStyle(this.failurePercent); + }, + failureTooltip() { + return this.getTooltip(this.failureLabel, this.failureCount); + }, + neutralPercent() { + return this.getPercent(this.neutralCount); + }, + neutralBarStyle() { + return this.barStyle(this.neutralPercent); + }, + neutralTooltip() { + return this.getTooltip(this.neutralLabel, this.neutralCount); + }, + }, + methods: { + getPercent(count) { + return Math.ceil((count / this.totalCount) * 100); + }, + barStyle(percent) { + return `width: ${percent}%;`; + }, + getTooltip(label, count) { + return `${label}: ${count}`; + }, + }, +}; +</script> + +<template> + <div + class="stacked-progress-bar" + :class="cssClass" + > + <span + v-if="!totalCount" + class="status-unavailable" + > + {{ __("Not available") }} + </span> + <span + v-tooltip + v-if="successPercent" + class="status-green" + data-placement="bottom" + :title="successTooltip" + :style="successBarStyle" + > + {{ successPercent }}% + </span> + <span + v-tooltip + v-if="neutralPercent" + class="status-neutral" + data-placement="bottom" + :title="neutralTooltip" + :style="neutralBarStyle" + > + {{ neutralPercent }}% + </span> + <span + v-tooltip + v-if="failurePercent" + class="status-red" + data-placement="bottom" + :title="failureTooltip" + :style="failureBarStyle" + > + {{ failurePercent }}% + </span> + </div> +</template> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 43b16d3cf7d..cff47ea76ec 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -59,3 +59,4 @@ @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive_tables"; +@import "framework/stacked-progress-bar"; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 1d2303a3a2b..4c6b32630e1 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -30,7 +30,7 @@ @include set-visible; min-height: $dropdown-min-height; max-height: $dropdown-max-height; - overflow: auto; + overflow-y: auto; @media (max-width: $screen-xs-max) { width: 100%; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 1e91db5af9b..d835d49d8b2 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -124,6 +124,10 @@ &.wiki { padding: $gl-padding; + + @media (min-width: $screen-md-min) { + padding: $gl-padding * 2; + } } &.blob-no-preview { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 3b7256f3000..634593aefd0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -303,6 +303,8 @@ .projects-dropdown-menu { padding: 0; + overflow-y: initial; + max-height: initial; } .dropdown-chevron { diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 1d8bd26cf1a..d8c57a0e2d9 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -24,15 +24,13 @@ font-size: $gl-font-size; line-height: 25px; + &.status-box-closed, &.status-box-mr-closed { background-color: $gl-danger; } - &.status-box-issue-closed { - background-color: $gl-primary; - } - - &.status-box-merged { + &.status-box-issue-closed, + &.status-box-mr-merged { background-color: $gl-primary; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 1be66d0ab21..51ae09777fd 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -1,4 +1,5 @@ .modal-header { + background-color: $modal-body-bg; padding: #{3 * $grid-size} #{2 * $grid-size}; .page-title { @@ -8,8 +9,10 @@ .modal-body { background-color: $modal-body-bg; + min-height: $modal-body-height; position: relative; padding: #{3 * $grid-size} #{2 * $grid-size}; + text-align: left; .form-actions { margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size}; @@ -20,6 +23,30 @@ } } +.modal-footer { + display: flex; + flex-direction: row; + + .btn + .btn { + margin-left: $grid-size; + } + + @media (max-width: $screen-xs-max) { + flex-direction: column; + + .btn + .btn { + margin-left: 0; + margin-top: $grid-size; + } + } + + @media (min-width: $screen-sm-min) { + .btn:first-of-type { + margin-left: auto; + } + } +} + body.modal-open { overflow: hidden; } @@ -32,12 +59,6 @@ body.modal-open { } } -@media (min-width: $screen-md-min) { - .modal-dialog { - width: 860px; - } -} - @media (min-width: $screen-lg-min) { .modal-full { width: 98%; diff --git a/app/assets/stylesheets/framework/stacked-progress-bar.scss b/app/assets/stylesheets/framework/stacked-progress-bar.scss new file mode 100644 index 00000000000..4869cda73e5 --- /dev/null +++ b/app/assets/stylesheets/framework/stacked-progress-bar.scss @@ -0,0 +1,54 @@ +.stacked-progress-bar { + display: flex; + height: 16px; + border-radius: 10px; + overflow: hidden; + background-color: $theme-gray-100; + + .status-unavailable, + .status-green, + .status-neutral, + .status-red, { + height: 100%; + min-width: 25px; + padding: 0 5px; + font-size: $tooltip-font-size; + font-weight: normal; + color: $white-light; + line-height: 16px; + + &:hover { + cursor: pointer; + } + } + + .status-unavailable { + padding: 0 10px; + color: $theme-gray-700; + } + + .status-green { + background-color: $green-500; + + &:hover { + background-color: $green-600; + } + } + + .status-neutral { + background-color: $theme-gray-200; + color: $gl-gray-dark; + + &:hover { + background-color: $theme-gray-300; + } + } + + .status-red { + background-color: $red-500; + + &:hover { + background-color: $red-600; + } + } +} diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index a23131e0818..d04e555769b 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -194,6 +194,6 @@ $modal-body-bg: $white-light; //** Modal footer border color // $modal-footer-border-color: $modal-header-border-color -// $modal-lg: 900px -// $modal-md: 600px +$modal-lg: 860px; +$modal-md: 540px; // $modal-sm: 300px diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index ef1520f1f63..da18ddf78d3 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -733,3 +733,8 @@ $popup-box-shadow-color: rgba(90, 90, 90, 0.05); Multi file editor */ $border-color-settings: #e1e1e1; + +/* +Modals +*/ +$modal-body-height: 134px; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 3b35beb7695..cfef6476d4d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -117,47 +117,6 @@ top: $gl-padding-top; } - .content-list { - li { - padding: 18px $gl-padding $gl-padding; - - .container-fluid { - padding: 0; - } - } - - .title-col { - p { - margin: 0; - - &.title { - line-height: 19px; - font-size: 14px; - font-weight: $gl-font-weight-bold; - color: $gl-text-color; - } - - &.text { - color: $layout-link-gray; - - &.value-col { - color: $gl-text-color; - } - } - } - } - - .value-col { - text-align: right; - - span { - position: relative; - vertical-align: middle; - top: 3px; - } - } - } - .fa-spinner { font-size: 28px; position: relative; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 370b07663fd..766e02b12ea 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -69,13 +69,6 @@ border-color: $border-white-normal; } } - - .btn { - .icon-play { - height: 13px; - width: 12px; - } - } } .btn .text-center { diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index c7297a34ad8..7d40c61da26 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -3,22 +3,21 @@ // see also: https://gist.github.com/jasonm23/2868981 $black: #000; - $red: #cd0000; - $green: #00cd00; - $yellow: #cdcd00; - $blue: #00e; // according to wikipedia, this is the xterm standard - //$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile) - $magenta: #cd00cd; - $cyan: #00cdcd; - $white: #e5e5e5; + $red: #ea1010; + $green: #009900; + $yellow: #999900; + $blue: #0073e6; + $magenta: #d411d4; + $cyan: #009999; + $white: #ccc; $l-black: #373b41; - $l-red: #c66; - $l-green: #b5bd68; - $l-yellow: #f0c674; - $l-blue: #81a2be; - $l-magenta: #b294bb; - $l-cyan: #8abeb7; - $l-white: $gray-darkest; + $l-red: #ff6161; + $l-green: #00d600; + $l-yellow: #bdbd00; + $l-blue: #5797ff; + $l-magenta: #d96dd9; + $l-cyan: #00bdbd; + $l-white: #fff; /* * xterm colors diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index a7ab481519d..b0c4c31cffc 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -50,10 +50,10 @@ class Admin::DeployKeysController < Admin::ApplicationController end def create_params - params.require(:deploy_key).permit(:key, :title, :can_push) + params.require(:deploy_key).permit(:key, :title) end def update_params - params.require(:deploy_key).permit(:title, :can_push) + params.require(:deploy_key).permit(:title) end end diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 77e3c95d197..2b47819303e 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -59,11 +59,9 @@ class Admin::HooksController < Admin::ApplicationController def hook_params params.require(:hook).permit( :enable_ssl_verification, - :push_events, - :tag_push_events, - :repository_update_events, :token, - :url + :url, + *SystemHook.triggers.values ) end end diff --git a/app/controllers/admin/jobs_controller.rb b/app/controllers/admin/jobs_controller.rb index 5162273ef8a..ae7a7f6279c 100644 --- a/app/controllers/admin/jobs_controller.rb +++ b/app/controllers/admin/jobs_controller.rb @@ -20,6 +20,6 @@ class Admin::JobsController < Admin::ApplicationController def cancel_all Ci::Build.running_or_pending.each(&:cancel) - redirect_to admin_jobs_path + redirect_to admin_jobs_path, status: 303 end end diff --git a/app/controllers/concerns/with_performance_bar.rb b/app/controllers/concerns/with_performance_bar.rb index 230bbe4b1aa..6a8b1a4de7b 100644 --- a/app/controllers/concerns/with_performance_bar.rb +++ b/app/controllers/concerns/with_performance_bar.rb @@ -6,13 +6,22 @@ module WithPerformanceBar end def peek_enabled? - return true if Rails.env.development? return false unless Gitlab::PerformanceBar.enabled?(current_user) if RequestStore.active? - RequestStore.fetch(:peek_enabled) { cookies[:perf_bar_enabled].present? } + RequestStore.fetch(:peek_enabled) { cookie_or_default_value } else - cookies[:perf_bar_enabled].present? + cookie_or_default_value + end + end + + private + + def cookie_or_default_value + if cookies[:perf_bar_enabled].present? + cookies[:perf_bar_enabled] == 'true' + else + cookies[:perf_bar_enabled] = 'true' if Rails.env.development? end end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index f013d21275e..acf6aaf57f4 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -75,8 +75,6 @@ class Groups::MilestonesController < Groups::ApplicationController end def milestones - search_params = params.merge(group_ids: group.id) - milestones = MilestonesFinder.new(search_params).execute legacy_milestones = GroupMilestone.build_collection(group, group_projects, params) @@ -94,4 +92,8 @@ class Groups::MilestonesController < Groups::ApplicationController render_404 unless @milestone end + + def search_params + params.permit(:state).merge(group_ids: group.id) + end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 689d2e3db22..d631d09f1b8 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -112,6 +112,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController continue_login_process end + rescue Gitlab::OAuth::SigninDisabledForProviderError + handle_disabled_provider rescue Gitlab::OAuth::SignupDisabledError handle_signup_error end @@ -168,6 +170,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to new_user_session_path end + def handle_disabled_provider + label = Gitlab::OAuth::Provider.label_for(oauth['provider']) + flash[:alert] = "Signing in using #{label} has been disabled" + + redirect_to new_user_session_path + end + def log_audit_event(user, options = {}) AuditEventService.new(user, user, options) .for_authentication.security_event diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 026708169f4..0a40c67368f 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -13,31 +13,37 @@ class Projects::CommitsController < Projects::ApplicationController @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) - respond_to do |format| - format.html - format.atom { render layout: 'xml.atom' } - - format.json do - pager_json( - 'projects/commits/_commits', - @commits.size, - project: @project, - ref: @ref) + # https://gitlab.com/gitlab-org/gitaly/issues/931 + Gitlab::GitalyClient.allow_n_plus_1_calls do + respond_to do |format| + format.html + format.atom { render layout: 'xml.atom' } + + format.json do + pager_json( + 'projects/commits/_commits', + @commits.size, + project: @project, + ref: @ref) + end end end end def signatures - respond_to do |format| - format.json do - render json: { - signatures: @commits.select(&:has_signature?).map do |commit| - { - commit_sha: commit.sha, - html: view_to_html_string('projects/commit/_signature', signature: commit.signature) - } - end - } + # https://gitlab.com/gitlab-org/gitaly/issues/931 + Gitlab::GitalyClient.allow_n_plus_1_calls do + respond_to do |format| + format.json do + render json: { + signatures: @commits.select(&:has_signature?).map do |commit| + { + commit_sha: commit.sha, + html: view_to_html_string('projects/commit/_signature', signature: commit.signature) + } + end + } + end end end end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index e06dda1baa4..f43ef2e5f2f 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -24,7 +24,7 @@ class Projects::DeployKeysController < Projects::ApplicationController def create @key = DeployKeys::CreateService.new(current_user, create_params).execute - unless @key.valid? && @project.deploy_keys << @key + unless @key.valid? flash[:alert] = @key.errors.full_messages.join(', ').html_safe end @@ -71,11 +71,14 @@ class Projects::DeployKeysController < Projects::ApplicationController end def create_params - params.require(:deploy_key).permit(:key, :title, :can_push) + create_params = params.require(:deploy_key) + .permit(:key, :title, deploy_keys_projects_attributes: [:can_push]) + create_params.dig(:deploy_keys_projects_attributes, '0')&.merge!(project_id: @project.id) + create_params end def update_params - params.require(:deploy_key).permit(:title, :can_push) + params.require(:deploy_key).permit(:title, deploy_keys_projects_attributes: [:id, :can_push]) end def authorize_update_deploy_key! diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 6f51e7b9b40..dd7aa1a67b9 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -64,18 +64,10 @@ class Projects::HooksController < Projects::ApplicationController def hook_params params.require(:hook).permit( - :job_events, - :pipeline_events, :enable_ssl_verification, - :issues_events, - :confidential_issues_events, - :merge_requests_events, - :note_events, - :push_events, - :tag_push_events, :token, :url, - :wiki_page_events + *ProjectHook.triggers.values ) end end diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 3d2926d5d75..0df80fa700f 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -43,11 +43,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap end def diffs - @diffs = if @merge_request.can_be_created - @merge_request.diffs(diff_options) - else - [] - end + @diffs = @merge_request.diffs(diff_options) if @merge_request.can_be_created @diff_notes_disabled = true diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 980bbf699b6..0f70efbce40 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -92,12 +92,6 @@ class Projects::MilestonesController < Projects::ApplicationController def milestones @milestones ||= begin - if @project.group && can?(current_user, :read_group, @project.group) - group = @project.group - end - - search_params = params.merge(project_ids: @project.id, group_ids: group&.id) - MilestonesFinder.new(search_params).execute end end @@ -113,4 +107,12 @@ class Projects::MilestonesController < Projects::ApplicationController def milestone_params params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end + + def search_params + if @project.group && can?(current_user, :read_group, @project.group) + group = @project.group + end + + params.permit(:state).merge(project_ids: @project.id, group_ids: group&.id) + end end diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 0a5a0ea2f35..b4605fca193 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -46,11 +46,7 @@ class MilestonesFinder end def order(items) - if params.has_key?(:order) - items.reorder(params[:order]) - else - order_statement = Gitlab::Database.nulls_last_order('due_date', 'ASC') - items.reorder(order_statement) - end + order_statement = Gitlab::Database.nulls_last_order('due_date', 'ASC') + items.reorder(order_statement).order('title ASC') end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 5e3b2e5581c..a6e1de6ffdc 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,6 +1,8 @@ module BlobHelper def highlight(blob_name, blob_content, repository: nil, plain: false) + plain ||= blob_content.length > Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE highlighted = Gitlab::Highlight.highlight(blob_name, blob_content, plain: plain, repository: repository) + raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>) end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 0f110bd25c5..64cd3032780 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -72,7 +72,7 @@ module IssuesHelper if item.try(:expired?) 'status-box-expired' elsif item.try(:merged?) - 'status-box-merged' + 'status-box-mr-merged' elsif item.closed? 'status-box-mr-closed' elsif item.try(:upcoming?) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index e7c953e749e..ddb48371c79 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -54,8 +54,16 @@ module TodosHelper def todo_target_state_pill(todo) return unless show_todo_state?(todo) + type = + case todo.target + when MergeRequest + 'mr' + when Issue + 'issue' + end + content_tag(:span, nil, class: 'target-status') do - content_tag(:span, nil, class: "status-box status-box-#{todo.target.state.dasherize}") do + content_tag(:span, nil, class: "status-box status-box-#{type}-#{todo.target.state.dasherize}") do todo.target.state.capitalize end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 6012dbba1b9..df67fb243ad 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -8,6 +8,7 @@ module Ci MissingDependenciesError = Class.new(StandardError) + belongs_to :project, inverse_of: :builds belongs_to :runner belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d4690da3be6..d7153d7b816 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -7,7 +7,7 @@ module Ci include Presentable include Gitlab::OptimisticLocking - belongs_to :project + belongs_to :project, inverse_of: :pipelines belongs_to :user belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' diff --git a/app/models/commit.rb b/app/models/commit.rb index 21904c87f01..2d2d89af030 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -372,19 +372,19 @@ class Commit # uri_type('doc/README.md') # => :blob # uri_type('doc/logo.png') # => :raw # uri_type('doc/api') # => :tree - # uri_type('not/found') # => :nil + # uri_type('not/found') # => nil # # Returns a symbol def uri_type(path) - entry = @raw.rugged_tree_entry(path) + entry = @raw.tree_entry(path) + return unless entry + if entry[:type] == :blob blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project) blob.image? || blob.video? ? :raw : :blob else entry[:type] end - rescue Rugged::TreeError - nil end def raw_diffs(*args) diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb index 67ecf470f7e..703a72c355c 100644 --- a/app/models/concerns/sha_attribute.rb +++ b/app/models/concerns/sha_attribute.rb @@ -3,6 +3,7 @@ module ShaAttribute module ClassMethods def sha_attribute(name) + return if ENV['STATIC_VERIFICATION'] return unless table_exists? column = columns.find { |c| c.name == name.to_s } diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb new file mode 100644 index 00000000000..ec0ed3b795a --- /dev/null +++ b/app/models/concerns/triggerable_hooks.rb @@ -0,0 +1,40 @@ +module TriggerableHooks + AVAILABLE_TRIGGERS = { + repository_update_hooks: :repository_update_events, + push_hooks: :push_events, + tag_push_hooks: :tag_push_events, + issue_hooks: :issues_events, + confidential_issue_hooks: :confidential_issues_events, + note_hooks: :note_events, + merge_request_hooks: :merge_requests_events, + job_hooks: :job_events, + pipeline_hooks: :pipeline_events, + wiki_page_hooks: :wiki_page_events + }.freeze + + extend ActiveSupport::Concern + + class_methods do + attr_reader :triggerable_hooks + + attr_reader :triggers + + def hooks_for(trigger) + callable_scopes = triggers.keys + [:all] + return none unless callable_scopes.include?(trigger) + + public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend + end + + private + + def triggerable_hooks(hooks) + triggers = AVAILABLE_TRIGGERS.slice(*hooks) + @triggers = triggers + + triggers.each do |trigger, event| + scope trigger, -> { where(event => true) } + end + end + end +end diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index eae5eee4fee..c2e0a5fa126 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -1,10 +1,16 @@ class DeployKey < Key - has_many :deploy_keys_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + include IgnorableColumn + + has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :deploy_keys_projects scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) } scope :are_public, -> { where(public: true) } + ignore_column :can_push + + accepts_nested_attributes_for :deploy_keys_projects + def private? !public? end @@ -22,10 +28,18 @@ class DeployKey < Key end def has_access_to?(project) - projects.include?(project) + deploy_keys_project_for(project).present? end def can_push_to?(project) - can_push? && has_access_to?(project) + !!deploy_keys_project_for(project)&.can_push? + end + + def deploy_keys_project_for(project) + deploy_keys_projects.find_by(project: project) + end + + def projects_with_write_access + Project.preload(:route).where(id: deploy_keys_projects.with_write_access.select(:project_id)) end end diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index b37b9bfbdac..6eef12c4373 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -1,8 +1,14 @@ class DeployKeysProject < ActiveRecord::Base belongs_to :project - belongs_to :deploy_key + belongs_to :deploy_key, inverse_of: :deploy_keys_projects - validates :deploy_key_id, presence: true + scope :without_project_deleted, -> { joins(:project).where(projects: { pending_delete: false }) } + scope :in_project, ->(project) { where(project: project) } + scope :with_write_access, -> { where(can_push: true) } + + accepts_nested_attributes_for :deploy_key + + validates :deploy_key, presence: true validates :deploy_key_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :project_id, presence: true diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index c0864769314..dc2f6817190 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -44,10 +44,10 @@ class GlobalMilestone def self.group_milestones_states_count(group) return STATE_COUNT_HASH unless group - params = { group_ids: [group.id], state: 'all', order: nil } + params = { group_ids: [group.id], state: 'all' } relation = MilestonesFinder.new(params).execute - grouped_by_state = relation.group(:state).count + grouped_by_state = relation.reorder(nil).group(:state).count { opened: grouped_by_state['active'] || 0, @@ -60,10 +60,10 @@ class GlobalMilestone def self.legacy_group_milestone_states_count(projects) return STATE_COUNT_HASH unless projects - params = { project_ids: projects.map(&:id), state: 'all', order: nil } + params = { project_ids: projects.map(&:id), state: 'all' } relation = MilestonesFinder.new(params).execute - project_milestones_by_state_and_title = relation.group(:state, :title).count + project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count opened = count_by_state(project_milestones_by_state_and_title, 'active') closed = count_by_state(project_milestones_by_state_and_title, 'closed') diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index a8c424a6614..b6dd39b860b 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -1,19 +1,17 @@ class ProjectHook < WebHook - TRIGGERS = { - push_hooks: :push_events, - tag_push_hooks: :tag_push_events, - issue_hooks: :issues_events, - confidential_issue_hooks: :confidential_issues_events, - note_hooks: :note_events, - merge_request_hooks: :merge_requests_events, - job_hooks: :job_events, - pipeline_hooks: :pipeline_events, - wiki_page_hooks: :wiki_page_events - }.freeze + include TriggerableHooks - TRIGGERS.each do |trigger, event| - scope trigger, -> { where(event => true) } - end + triggerable_hooks [ + :push_hooks, + :tag_push_hooks, + :issue_hooks, + :confidential_issue_hooks, + :note_hooks, + :merge_request_hooks, + :job_hooks, + :pipeline_hooks, + :wiki_page_hooks + ] belongs_to :project validates :project, presence: true diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index 180c479c41b..0528266e5b3 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -1,14 +1,14 @@ class SystemHook < WebHook - TRIGGERS = { - repository_update_hooks: :repository_update_events, - push_hooks: :push_events, - tag_push_hooks: :tag_push_events - }.freeze + include TriggerableHooks - TRIGGERS.each do |trigger, event| - scope trigger, -> { where(event => true) } - end + triggerable_hooks [ + :repository_update_hooks, + :push_hooks, + :tag_push_hooks, + :merge_request_hooks + ] default_value_for :push_events, false default_value_for :repository_update_events, true + default_value_for :merge_requests_events, false end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 5a70e114f56..27729deeac9 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -4,6 +4,7 @@ class WebHook < ActiveRecord::Base has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :url, presence: true, url: true + validates :token, format: { without: /\n/ } def execute(data, hook_name) WebHookService.new(self, data, hook_name).execute diff --git a/app/models/project.rb b/app/models/project.rb index 7ab7df4fdcd..4017864f718 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -199,13 +199,13 @@ class Project < ActiveRecord::Base has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :commit_statuses - has_many :pipelines, class_name: 'Ci::Pipeline' + has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project # Ci::Build objects store data on the file system such as artifact files and # build traces. Currently there's no efficient way of removing this data in # bulk that doesn't involve loading the rows into memory. As a result we're # still using `dependent: :destroy` here. - has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName' has_many :runner_projects, class_name: 'Ci::RunnerProject' has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' @@ -967,10 +967,12 @@ class Project < ActiveRecord::Base def execute_hooks(data, hooks_scope = :push_hooks) run_after_commit_or_now do - hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend + hooks.hooks_for(hooks_scope).each do |hook| hook.async_execute(data, hooks_scope.to_s) end end + + SystemHooksService.new.execute_hooks(data, hooks_scope) end def execute_services(data, hooks_scope = :push_hooks) diff --git a/app/models/push_event.rb b/app/models/push_event.rb index 83ce9014094..90c085c888e 100644 --- a/app/models/push_event.rb +++ b/app/models/push_event.rb @@ -46,10 +46,11 @@ class PushEvent < Event # Returns PushEvent instances for which no merge requests have been created. def self.without_existing_merge_requests - existing_mrs = MergeRequest.except(:order) + existing_mrs = MergeRequest.except(:order, :where) .select(1) .where('merge_requests.source_project_id = events.project_id') .where('merge_requests.source_branch = push_event_payloads.ref') + .where(state: :opened) # For reasons unknown the use of #eager_load will result in the # "push_event_payload" association not being set. Because of this we're diff --git a/app/models/repository.rb b/app/models/repository.rb index 2ffd9558ebc..b4bc0f87458 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -932,15 +932,17 @@ class Repository return [] if empty? || query.blank? offset = 2 - args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) + args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) run_git(args).first.scrub.split(/^--$/) end def search_files_by_name(query, ref) - return [] if empty? || query.blank? + safe_query = Regexp.escape(query.sub(/^\/*/, "")) + + return [] if empty? || safe_query.blank? - args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) + args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{safe_query}) run_git(args).first.lines.map(&:strip) end diff --git a/app/models/service.rb b/app/models/service.rb index 7f260f7a96b..96a064697f0 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -118,6 +118,11 @@ class Service < ActiveRecord::Base nil end + def api_field_names + fields.map { |field| field[:name] } + .reject { |field_name| field_name =~ /(password|token|key)/ } + end + def global_fields fields end diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb index 229311eb6ee..c226586fba5 100644 --- a/app/presenters/projects/settings/deploy_keys_presenter.rb +++ b/app/presenters/projects/settings/deploy_keys_presenter.rb @@ -7,7 +7,7 @@ module Projects delegate :size, to: :available_public_keys, prefix: true def new_key - @key ||= DeployKey.new + @key ||= DeployKey.new.tap { |dk| dk.deploy_keys_projects.build } end def enabled_keys diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb index c75431a79ae..2678f99510c 100644 --- a/app/serializers/deploy_key_entity.rb +++ b/app/serializers/deploy_key_entity.rb @@ -3,19 +3,20 @@ class DeployKeyEntity < Grape::Entity expose :user_id expose :title expose :fingerprint - expose :can_push expose :destroyed_when_orphaned?, as: :destroyed_when_orphaned expose :almost_orphaned?, as: :almost_orphaned expose :created_at expose :updated_at - expose :projects, using: ProjectEntity do |deploy_key| - deploy_key.projects.without_deleted.select { |project| options[:user].can?(:read_project, project) } + expose :deploy_keys_projects, using: DeployKeysProjectEntity do |deploy_key| + deploy_key.deploy_keys_projects + .without_project_deleted + .select { |deploy_key_project| Ability.allowed?(options[:user], :read_project, deploy_key_project.project) } end expose :can_edit private def can_edit - options[:user].can?(:update_deploy_key, object) + Ability.allowed?(options[:user], :update_deploy_key, object) end end diff --git a/app/serializers/deploy_keys_project_entity.rb b/app/serializers/deploy_keys_project_entity.rb new file mode 100644 index 00000000000..568ef5ab75e --- /dev/null +++ b/app/serializers/deploy_keys_project_entity.rb @@ -0,0 +1,4 @@ +class DeployKeysProjectEntity < Grape::Entity + expose :can_push + expose :project, using: ProjectEntity +end diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index 997d247be46..74a85e5c9f0 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -13,6 +13,7 @@ module Labels update_issuables(new_label, batched_ids) update_issue_board_lists(new_label, batched_ids) update_priorities(new_label, batched_ids) + subscribe_users(new_label, batched_ids) # Order is important, project labels need to be last update_project_labels(batched_ids) end @@ -26,6 +27,15 @@ module Labels private + def subscribe_users(new_label, label_ids) + # users can be subscribed to multiple labels that will be merged into the group one + # we want to keep only one subscription / user + ids_to_update = Subscription.where(subscribable_id: label_ids, subscribable_type: 'Label') + .group(:user_id) + .pluck('MAX(id)') + Subscription.where(id: ids_to_update).update_all(subscribable_id: new_label.id) + end + def label_ids_for_merge(new_label) LabelsFinder .new(current_user, title: new_label.title, group_id: project.group.id) @@ -53,7 +63,7 @@ module Labels end def update_project_labels(label_ids) - Label.where(id: label_ids).delete_all + Label.where(id: label_ids).destroy_all end def clone_label_to_group_label(label) diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb index 89dab1dd028..cf687b71d16 100644 --- a/app/services/merge_requests/create_from_issue_service.rb +++ b/app/services/merge_requests/create_from_issue_service.rb @@ -54,6 +54,7 @@ module MergeRequests source_project_id: project.id, source_branch: branch_name, target_project_id: project.id, + target_branch: ref, milestone_id: issue.milestone_id } end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 49cf534dc0d..634bf3bd690 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -1,15 +1,11 @@ module MergeRequests class CreateService < MergeRequests::BaseService def execute - # @project is used to determine whether the user can set the merge request's - # assignee, milestone and labels. Whether they can depends on their - # permissions on the target project. - source_project = @project - @project = Project.find(params[:target_project_id]) if params[:target_project_id] + set_projects! merge_request = MergeRequest.new merge_request.target_project = @project - merge_request.source_project = source_project + merge_request.source_project = @source_project merge_request.source_branch = params[:source_branch] merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) @@ -58,5 +54,25 @@ module MergeRequests pipelines.order(id: :desc).first end + + def set_projects! + # @project is used to determine whether the user can set the merge request's + # assignee, milestone and labels. Whether they can depends on their + # permissions on the target project. + @source_project = @project + @project = Project.find(params[:target_project_id]) if params[:target_project_id] + + # make sure that source/target project ids are not in + # params so it can't be overridden later when updating attributes + # from params when applying quick actions + params.delete(:source_project_id) + params.delete(:target_project_id) + + unless can?(current_user, :read_project, @source_project) && + can?(current_user, :read_project, @project) + + raise Gitlab::Access::AccessDeniedError + end + end end end diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb index 4ca6414b73b..a3d7f5cbed5 100644 --- a/app/services/projects/gitlab_projects_import_service.rb +++ b/app/services/projects/gitlab_projects_import_service.rb @@ -26,7 +26,7 @@ module Projects end def tmp_filename - "#{SecureRandom.hex}_#{params[:path]}" + SecureRandom.hex end def file diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index af6d77ef5e8..a6b7a6e1416 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -8,7 +8,7 @@ class SystemHooksService end def execute_hooks(data, hooks_scope = :all) - SystemHook.public_send(hooks_scope).find_each do |hook| # rubocop:disable GitlabSecurity/PublicSend + SystemHook.hooks_for(hooks_scope).find_each do |hook| hook.async_execute(data, 'system_hooks') end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 30a5aab13bf..06b23cd7076 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -68,21 +68,14 @@ module SystemNoteService # # Returns the created Note object def change_issue_assignees(issue, project, author, old_assignees) - body = - if issue.assignees.any? && old_assignees.any? - unassigned_users = old_assignees - issue.assignees - added_users = issue.assignees.to_a - old_assignees - - text_parts = [] - text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any? - text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any? - - text_parts.join(' and ') - elsif old_assignees.any? - "removed assignee" - elsif issue.assignees.any? - "assigned to #{issue.assignees.map(&:to_reference).to_sentence}" - end + unassigned_users = old_assignees - issue.assignees + added_users = issue.assignees.to_a - old_assignees + + text_parts = [] + text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any? + text_parts << "unassigned #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any? + + body = text_parts.join(' and ') create_note(NoteSummary.new(issue, project, author, body, action: 'assignee')) end diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb index 20d90504bd2..e9aefb1fb75 100644 --- a/app/services/test_hooks/base_service.rb +++ b/app/services/test_hooks/base_service.rb @@ -9,7 +9,7 @@ module TestHooks end def execute - trigger_key = hook.class::TRIGGERS.key(trigger.to_sym) + trigger_key = hook.class.triggers.key(trigger.to_sym) trigger_data_method = "#{trigger}_data" if trigger_key.nil? || !self.respond_to?(trigger_data_method, true) diff --git a/app/services/test_hooks/system_service.rb b/app/services/test_hooks/system_service.rb index 67552edefc9..9016c77b7f0 100644 --- a/app/services/test_hooks/system_service.rb +++ b/app/services/test_hooks/system_service.rb @@ -13,5 +13,12 @@ module TestHooks def repository_update_events_data Gitlab::DataBuilder::Repository.sample_data end + + def merge_requests_events_data + merge_request = MergeRequest.of_projects(current_user.projects.select(:id)).first + throw(:validation_error, 'Ensure one of your projects has merge requests.') unless merge_request.present? + + merge_request.to_hook_data(current_user) + end end end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 6ebc7c89500..36e589d5aa8 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -113,7 +113,7 @@ class WebHookService 'Content-Type' => 'application/json', 'X-Gitlab-Event' => hook_name.singularize.titleize }.tap do |hash| - hash['X-Gitlab-Token'] = hook.token if hook.token.present? + hash['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present? end end end diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 92370034baa..1420163fd5a 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -12,7 +12,7 @@ %tr %th.col-sm-2 Title %th.col-sm-4 Fingerprint - %th.col-sm-2 Write access allowed + %th.col-sm-2 Projects with write access %th.col-sm-2 Added at %th.col-sm-2 %tbody @@ -23,10 +23,8 @@ %td %code.key-fingerprint= deploy_key.fingerprint %td - - if deploy_key.can_push? - Yes - - else - No + - deploy_key.projects_with_write_access.each do |project| + = link_to project.full_name, admin_project_path(project), class: 'label deploy-project-label' %td %span.cgray added #{time_ago_with_tooltip(deploy_key.created_at)} diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml index 645005c6deb..d8f96ed5b0d 100644 --- a/app/views/admin/hooks/_form.html.haml +++ b/app/views/admin/hooks/_form.html.haml @@ -38,6 +38,13 @@ %strong Tag push events %p.light This URL will be triggered when a new tag is pushed to the repository + %div + = form.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = form.label :merge_requests_events, class: 'list-label' do + %strong Merge request events + %p.light + This URL will be triggered when a merge request is created/updated/merged .form-group = form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox' .col-sm-10 diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml index efb15ccc8df..629b1a9940f 100644 --- a/app/views/admin/hooks/edit.html.haml +++ b/app/views/admin/hooks/edit.html.haml @@ -13,7 +13,7 @@ = render partial: 'form', locals: { form: f, hook: @hook } .form-actions = f.submit 'Save changes', class: 'btn btn-create' - = render 'shared/web_hooks/test_button', triggers: SystemHook::TRIGGERS, hook: @hook + = render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: @hook = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' } %hr diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index b6e1df5f3ac..bc02d9969d6 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -22,12 +22,12 @@ - @hooks.each do |hook| %li .controls - = render 'shared/web_hooks/test_button', triggers: SystemHook::TRIGGERS, hook: hook, button_class: 'btn-sm' + = render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: hook, button_class: 'btn-sm' = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm' = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm' .monospace= hook.url %div - - SystemHook::TRIGGERS.each_value do |event| + - SystemHook.triggers.each_value do |event| - if hook.public_send(event) %span.label.label-gray= event.to_s.titleize %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml index 7066ed12b95..a01676d82a8 100644 --- a/app/views/admin/jobs/index.html.haml +++ b/app/views/admin/jobs/index.html.haml @@ -9,7 +9,12 @@ .nav-controls - if @all_builds.running_or_pending.any? - = link_to 'Cancel all', cancel_all_admin_jobs_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post + #stop-jobs-modal + + %button#stop-jobs-button.btn.btn-danger{ data: { toggle: 'modal', + target: '#stop-jobs-modal', + url: cancel_all_admin_jobs_path } } + = s_('AdminArea|Stop all jobs') .row-content-block.second-block #{(@scope || 'all').capitalize} jobs diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index ecdf76ef5c5..7a3f3667ac1 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -2,7 +2,7 @@ %ul.nav-links %li{ class: active_when(params[:filter].nil?) }> = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do - Your Projects + Your projects %li{ class: active_when(params[:filter] == 'starred') }> = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do - Starred Projects + Starred projects diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index 7330f4cb523..a9488df07bd 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -2,10 +2,10 @@ %ul.nav-links = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do - Your Snippets + Your snippets = nav_link(page: explore_snippets_path) do = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do - Explore Snippets + Explore snippets - if current_user .nav-controls.hidden-xs diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 0c27b09f7b1..96aae06a9df 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,5 +1,5 @@ -- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute -- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute +- issues_count = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute.count +- merge_requests_count = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute.count .nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar-inner-scroll @@ -39,14 +39,14 @@ = sprite_icon('issues') %span.nav-item-name Issues - %span.badge.count= number_with_delimiter(issues.count) + %span.badge.count= number_with_delimiter(issues_count) %ul.sidebar-sub-level-items = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do = link_to issues_group_path(@group) do %strong.fly-out-top-item-name #{ _('Issues') } - %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues.count) + %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count) %li.divider.fly-out-top-item = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do = link_to issues_group_path(@group), title: 'List' do @@ -69,13 +69,13 @@ = sprite_icon('git-merge') %span.nav-item-name Merge Requests - %span.badge.count= number_with_delimiter(merge_requests.count) + %span.badge.count= number_with_delimiter(merge_requests_count) %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do = link_to merge_requests_group_path(@group) do %strong.fly-out-top-item-name #{ _('Merge Requests') } - %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests.count) + %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count) = nav_link(path: 'group_members#index') do = link_to group_group_members_path(@group) do .nav-icon-container diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml index 86ebec0179c..e44506ec9c9 100644 --- a/app/views/profiles/gpg_keys/index.html.haml +++ b/app/views/profiles/gpg_keys/index.html.haml @@ -3,12 +3,12 @@ = render 'profiles/head' .row.prepend-top-default - .col-lg-3.profile-settings-sidebar + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 = page_title %p GPG keys allow you to verify signed commits. - .col-lg-9 + .col-lg-8 %h5.prepend-top-0 Add a GPG key %p.profile-settings-content diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index bd99eb93cc8..d367bd6be7b 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -34,7 +34,7 @@ .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' } + = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' = 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 diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 03ab1bb59e4..5d48a35dc4c 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -1,5 +1,5 @@ #modal-create-new-dir.modal - .modal-dialog + .modal-dialog.modal-lg .modal-content .modal-header %a.close{ href: "#", "data-dismiss" => "modal" } × diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 05b7dfe2872..21b6aa4bad9 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -1,5 +1,5 @@ #modal-upload-blob.modal - .modal-dialog + .modal-dialog.modal-lg .modal-content .modal-header %a.close{ href: "#", "data-dismiss" => "modal" } × diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 8e8c911185a..dab94d10bb1 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -9,44 +9,27 @@ - can_create_snippet = can?(current_user, :create_snippet, @project) - if can_create_issue - %li - = link_to new_project_issue_path(@project) do - #{ _('New issue') } + %li= link_to _('New issue'), new_project_issue_path(@project) - if merge_project - %li - = link_to project_new_merge_request_path(merge_project) do - #{ _('New merge request') } + %li= link_to _('New merge request'), project_new_merge_request_path(merge_project) - if can_create_snippet - %li - = link_to new_project_snippet_path(@project) do - #{ _('New snippet') } + %li= link_to _('New snippet'), new_project_snippet_path(@project) - if can_create_issue || merge_project || can_create_snippet %li.divider - if can?(current_user, :push_code, @project) - %li - = link_to project_new_blob_path(@project, @project.default_branch || 'master') do - #{ _('New file') } + %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') - unless @project.empty_repo? - %li - = link_to new_project_branch_path(@project) do - #{ _('New branch') } - %li - = link_to new_project_tag_path(@project) do - #{ _('New tag') } + %li= link_to _('New branch'), new_project_branch_path(@project) + %li= link_to _('New tag'), new_project_tag_path(@project) - elsif current_user && current_user.already_forked?(@project) - %li - = link_to project_new_blob_path(@project, @project.default_branch || 'master') do - #{ _('New file') } + %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') - elsif can?(current_user, :fork_project, @project) - %li - - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'), - notice: edit_in_new_fork_notice, - notice_now: edit_in_new_fork_notice_now } - - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, - continue: continue_params) - = link_to fork_path, method: :post do - #{ _('New file') } + - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'), + notice: edit_in_new_fork_notice, + notice_now: edit_in_new_fork_notice_now } + - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) + %li= link_to _('New file'), fork_path, method: :post diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index edaa3a1119e..c363180d0db 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -10,13 +10,15 @@ %p.light.append-bottom-0 Paste a machine public key here. Read more about how to generate it = link_to "here", help_page_path("ssh/README") - .form-group - .checkbox - = f.label :can_push do - = f.check_box :can_push - %strong Write access allowed - .form-group - %p.light.append-bottom-0 - Allow this key to push to repository as well? (Default only allows pull access.) + + = f.fields_for :deploy_keys_projects do |deploy_keys_project_form| + .form-group + .checkbox + = deploy_keys_project_form.label :can_push do + = deploy_keys_project_form.check_box :can_push + %strong Write access allowed + .form-group + %p.light.append-bottom-0 + Allow this key to push to repository as well? (Default only allows pull access.) = f.submit "Add key", class: "btn-create btn" diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 111cbcda266..21a4702a2a9 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -31,11 +31,11 @@ - if current_user && can?(current_user, :fork_project, @project) - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do - = custom_icon('icon_fork') + = sprite_icon('fork', size: 12) %span Fork - else = link_to new_project_fork_path(@project), title: "Fork project", class: 'btn btn-new' do - = custom_icon('icon_fork') + = sprite_icon('fork', size: 12) %span Fork diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml index b1219f019d7..dcc1f0e3fbe 100644 --- a/app/views/projects/hooks/edit.html.haml +++ b/app/views/projects/hooks/edit.html.haml @@ -12,7 +12,7 @@ = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } = f.submit 'Save changes', class: 'btn btn-create' - = render 'shared/web_hooks/test_button', triggers: ProjectHook::TRIGGERS, hook: @hook + = render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: @hook = link_to 'Remove', project_hook_path(@project, @hook), method: :delete, class: 'btn btn-remove pull-right', data: { confirm: 'Are you sure?' } %hr diff --git a/app/views/projects/jobs/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml index c66313bdbf3..311934d9c33 100644 --- a/app/views/projects/jobs/_empty_state.html.haml +++ b/app/views/projects/jobs/_empty_state.html.haml @@ -1,7 +1,7 @@ - illustration = local_assigns.fetch(:illustration) - illustration_size = local_assigns.fetch(:illustration_size) - title = local_assigns.fetch(:title) -- content = local_assigns.fetch(:content) +- content = local_assigns.fetch(:content, nil) - action = local_assigns.fetch(:action, nil) .row.empty-state @@ -11,7 +11,8 @@ .col-xs-12 .text-content %h4.text-center= title - %p= content + - if content + %p= content - if action .text-center = action diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index a71333497e6..e779473c239 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -24,7 +24,7 @@ - elsif @build.has_expiring_artifacts? %p.build-detail-row The artifacts will be removed in - %span.js-artifacts-remove= @build.artifacts_expire_at + %span= time_ago_in_words @build.artifacts_expire_at - if @build.artifacts? .btn-group.btn-group-justified{ role: :group } diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 8b05440fc78..1e6d6f67e66 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -93,14 +93,13 @@ illustration: 'illustrations/manual_action.svg', illustration_size: 'svg-394', title: _('This job requires a manual action'), - content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments.'), - action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), class: 'btn btn-primary', title: _('Trigger this manual action') ) + content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments'), + action: ( link_to _('Trigger this manual action'), play_project_job_path(@project, @build), method: :post, class: 'btn btn-primary', title: _('Trigger this manual action') ) - else = render 'empty_state', illustration: 'illustrations/job_not_triggered.svg', illustration_size: 'svg-306', - title: _('This job has not been triggered yet'), - content: _('This job depends on upstream jobs that need to succeed in order for this job to be triggered.') + title: _('This job has not been triggered yet') = render "sidebar" diff --git a/app/views/projects/merge_requests/creations/_diffs.html.haml b/app/views/projects/merge_requests/creations/_diffs.html.haml index 627fc4e9671..5b70e894b39 100644 --- a/app/views/projects/merge_requests/creations/_diffs.html.haml +++ b/app/views/projects/merge_requests/creations/_diffs.html.haml @@ -1 +1,5 @@ -= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false +- if @merge_request.can_be_created + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false +- else + .nothing-here-block + This merge request cannot be created. diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index 4b5fa28078a..376ac377562 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -15,7 +15,7 @@ = f.hidden_field :source_project_id = f.hidden_field :source_branch = f.hidden_field :target_project_id - = f.hidden_field :target_branch + = f.hidden_field :target_branch, id: '' .mr-compare.merge-request.js-merge-request-new-submit{ 'data-mr-submit-action': "#{j params[:tab].presence || 'new'}" } - if @commits.empty? diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index a4e820628f3..67607e4e9c6 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -23,6 +23,3 @@ %h4.underlined-title Available shared Runners : #{@shared_runners_count} %ul.bordered-list.available-shared-runners = render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner - - if @shared_runners_count > 10 - .light - and #{@shared_runners_count - 10} more... diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml index 82516cb4bcf..cd003107d66 100644 --- a/app/views/projects/settings/integrations/_project_hook.html.haml +++ b/app/views/projects/settings/integrations/_project_hook.html.haml @@ -3,14 +3,14 @@ .col-md-8.col-lg-7 %strong.light-header= hook.url %div - - ProjectHook::TRIGGERS.each_value do |event| + - ProjectHook.triggers.each_value do |event| - if hook.public_send(event) %span.label.label-gray.deploy-project-label= event.to_s.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 %span.append-right-10.inline SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} = link_to 'Edit', edit_project_hook_path(@project, hook), class: 'btn btn-sm' - = render 'shared/web_hooks/test_button', triggers: ProjectHook::TRIGGERS, hook: hook, button_class: 'btn-sm' + = render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: hook, button_class: 'btn-sm' = link_to project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-transparent' do %span.sr-only Remove = icon('trash') diff --git a/app/views/shared/deploy_keys/_form.html.haml b/app/views/shared/deploy_keys/_form.html.haml index e6075c3ae3a..87c2965bb21 100644 --- a/app/views/shared/deploy_keys/_form.html.haml +++ b/app/views/shared/deploy_keys/_form.html.haml @@ -1,5 +1,6 @@ - form = local_assigns.fetch(:form) - deploy_key = local_assigns.fetch(:deploy_key) +- deploy_keys_project = deploy_key.deploy_keys_project_for(@project) = form_errors(deploy_key) @@ -20,11 +21,13 @@ .col-sm-10 = form.text_field :fingerprint, class: 'form-control', readonly: 'readonly' -.form-group - .control-label - .col-sm-10 - = form.label :can_push do - = form.check_box :can_push - %strong Write access allowed - %p.light.append-bottom-0 - Allow this key to push to repository as well? (Default only allows pull access.) +- if deploy_keys_project.present? + = form.fields_for :deploy_keys_projects, deploy_keys_project do |deploy_keys_project_form| + .form-group + .control-label + .col-sm-10 + = deploy_keys_project_form.label :can_push do + = deploy_keys_project_form.check_box :can_push + %strong Write access allowed + %p.light.append-bottom-0 + Allow this key to push to repository as well? (Default only allows pull access.) diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml index 203d2adc8db..9a589387255 100644 --- a/app/views/shared/issuable/form/_branch_chooser.html.haml +++ b/app/views/shared/issuable/form/_branch_chooser.html.haml @@ -15,11 +15,10 @@ = form.label :target_branch, class: 'control-label' .col-sm-10.target-branch-select-dropdown-container .issuable-form-select-holder - = form.select(:target_branch, issuable.target_branches, - { include_blank: true }, + = form.hidden_field(:target_branch, { class: 'target_branch js-target-branch-select ref-name', disabled: issuable.new_record?, - data: { placeholder: "Select branch" }}) + data: { placeholder: "Select branch", endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }}) - if issuable.new_record? = link_to 'Change branches', mr_change_branches_path(issuable) diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 63aa4e29ec9..2a75b46d376 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -52,7 +52,7 @@ = render_project_pipeline_status(project.pipeline_status) - if forks %span.prepend-left-10 - = sprite_icon('fork') + = sprite_icon('fork', size: 12) = number_with_delimiter(project.forks_count) - if stars %span.prepend-left-10 diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 1f0e7629fb4..ad4d39b4aa1 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -50,7 +50,7 @@ = form.check_box :merge_requests_events, class: 'pull-left' .prepend-left-20 = form.label :merge_requests_events, class: 'list-label' do - %strong Merge Request events + %strong Merge request events %p.light This URL will be triggered when a merge request is created/updated/merged %li diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index d1c57b82681..07584fab7c8 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -17,10 +17,7 @@ class RepositoryForkWorker project.repository_storage_path, project.disk_path) raise "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result - project.repository.after_import - raise "Project #{project_id} had an invalid repository after fork" unless project.valid_repo? - - project.import_finish + project.after_import end private diff --git a/changelogs/unreleased-ee/4378-fix-cluster-js-not-running-on-update-page.yml b/changelogs/unreleased-ee/4378-fix-cluster-js-not-running-on-update-page.yml new file mode 100644 index 00000000000..bbb6cbd05be --- /dev/null +++ b/changelogs/unreleased-ee/4378-fix-cluster-js-not-running-on-update-page.yml @@ -0,0 +1,5 @@ +--- +title: Fix JavaScript bundle running on Cluster update/destroy pages +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/16301-update-removed-assignee-note-to-include-old-assignee-reference.yml b/changelogs/unreleased/16301-update-removed-assignee-note-to-include-old-assignee-reference.yml new file mode 100644 index 00000000000..e94b4f8bb26 --- /dev/null +++ b/changelogs/unreleased/16301-update-removed-assignee-note-to-include-old-assignee-reference.yml @@ -0,0 +1,5 @@ +--- +title: "Update 'removed assignee' note to include old assignee reference" +merge_request: 16301 +author: Maurizio De Santis +type: changed diff --git a/changelogs/unreleased/16468-add-fast-blank.yml b/changelogs/unreleased/16468-add-fast-blank.yml new file mode 100644 index 00000000000..ef68888ae33 --- /dev/null +++ b/changelogs/unreleased/16468-add-fast-blank.yml @@ -0,0 +1,5 @@ +--- +title: "Add fast-blank" +merge_request: 16468 +author: +type: performance diff --git a/changelogs/unreleased/19493-fork-does-not-protect-default-branch.yml b/changelogs/unreleased/19493-fork-does-not-protect-default-branch.yml new file mode 100644 index 00000000000..962f918e9db --- /dev/null +++ b/changelogs/unreleased/19493-fork-does-not-protect-default-branch.yml @@ -0,0 +1,5 @@ +--- +title: Makes forking protect default branch on completion +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/34252-trailing-plus.yml b/changelogs/unreleased/34252-trailing-plus.yml new file mode 100644 index 00000000000..fce17cb6ab9 --- /dev/null +++ b/changelogs/unreleased/34252-trailing-plus.yml @@ -0,0 +1,5 @@ +--- +title: Allow trailing + on labels in board filters +merge_request: 16490 +author: +type: fixed diff --git a/changelogs/unreleased/36571-ignore-root-in-repo.yml b/changelogs/unreleased/36571-ignore-root-in-repo.yml new file mode 100644 index 00000000000..396e82be51b --- /dev/null +++ b/changelogs/unreleased/36571-ignore-root-in-repo.yml @@ -0,0 +1,5 @@ +--- +title: Ignore leading slashes when searching for files within context of repository. +merge_request: +author: Andrew McCallum +type: fixed diff --git a/changelogs/unreleased/37199-labels-fix.yml b/changelogs/unreleased/37199-labels-fix.yml new file mode 100644 index 00000000000..bd70babb73d --- /dev/null +++ b/changelogs/unreleased/37199-labels-fix.yml @@ -0,0 +1,5 @@ +--- +title: Keep subscribers when promoting labels to group labels +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/37898-increase-readability-of-colored-text-in-job-output-log.yml b/changelogs/unreleased/37898-increase-readability-of-colored-text-in-job-output-log.yml new file mode 100644 index 00000000000..813b9ab81fa --- /dev/null +++ b/changelogs/unreleased/37898-increase-readability-of-colored-text-in-job-output-log.yml @@ -0,0 +1,5 @@ +--- +title: increase-readability-of-colored-text-in-job-output-log +merge_request: +author: +type: other diff --git a/changelogs/unreleased/38540-ssh-env-file.yml b/changelogs/unreleased/38540-ssh-env-file.yml new file mode 100644 index 00000000000..5ada0ede76d --- /dev/null +++ b/changelogs/unreleased/38540-ssh-env-file.yml @@ -0,0 +1,6 @@ +--- +title: 'Closes #38540 - Remove .ssh/environment file that now breaks the gitlab:check + rake task' +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/40818-last-push-widget-does-not-appear-after-pushing-new-commit.yml b/changelogs/unreleased/40818-last-push-widget-does-not-appear-after-pushing-new-commit.yml new file mode 100644 index 00000000000..c57caf31d10 --- /dev/null +++ b/changelogs/unreleased/40818-last-push-widget-does-not-appear-after-pushing-new-commit.yml @@ -0,0 +1,5 @@ +--- +title: Last push widget will show banner for new pushes to previously merged branch +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/41118-add-sorting-to-deployments-api.yml b/changelogs/unreleased/41118-add-sorting-to-deployments-api.yml new file mode 100644 index 00000000000..a08f75f9fb9 --- /dev/null +++ b/changelogs/unreleased/41118-add-sorting-to-deployments-api.yml @@ -0,0 +1,5 @@ +--- +title: Adds sorting to deployments API +merge_request: !16396 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/41476-enable-project-milestons-deletion-via-api.yml b/changelogs/unreleased/41476-enable-project-milestons-deletion-via-api.yml new file mode 100644 index 00000000000..bb5c1fdf082 --- /dev/null +++ b/changelogs/unreleased/41476-enable-project-milestons-deletion-via-api.yml @@ -0,0 +1,5 @@ +--- +title: Enables Project Milestone Deletion via the API +merge_request: 16478 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/41546-count-query-for-issues-and-mrs-runs-twice-on-group-index.yml b/changelogs/unreleased/41546-count-query-for-issues-and-mrs-runs-twice-on-group-index.yml new file mode 100644 index 00000000000..7e42dc20ae8 --- /dev/null +++ b/changelogs/unreleased/41546-count-query-for-issues-and-mrs-runs-twice-on-group-index.yml @@ -0,0 +1,5 @@ +--- +title: Fix double query execution on groups page +merge_request: 16314 +author: +type: performance diff --git a/changelogs/unreleased/41666-cannot-search-with-keyword-merge-2.yml b/changelogs/unreleased/41666-cannot-search-with-keyword-merge-2.yml new file mode 100644 index 00000000000..48893862071 --- /dev/null +++ b/changelogs/unreleased/41666-cannot-search-with-keyword-merge-2.yml @@ -0,0 +1,5 @@ +--- +title: Only highlight search results under the highlighting size limit +merge_request: 16462 +author: +type: performance diff --git a/changelogs/unreleased/41666-cannot-search-with-keyword-merge.yml b/changelogs/unreleased/41666-cannot-search-with-keyword-merge.yml new file mode 100644 index 00000000000..3a6fa425c9c --- /dev/null +++ b/changelogs/unreleased/41666-cannot-search-with-keyword-merge.yml @@ -0,0 +1,6 @@ +--- +title: Fix file search results when they match file contents with a number between + two colons +merge_request: 16462 +author: +type: fixed diff --git a/changelogs/unreleased/41727-target-branch-name.yml b/changelogs/unreleased/41727-target-branch-name.yml new file mode 100644 index 00000000000..aaedf6f1d12 --- /dev/null +++ b/changelogs/unreleased/41727-target-branch-name.yml @@ -0,0 +1,5 @@ +--- +title: Set target_branch to the ref branch when creating MR from issue +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/41743-unused-selectors-for-cycle-analytics.yml b/changelogs/unreleased/41743-unused-selectors-for-cycle-analytics.yml new file mode 100644 index 00000000000..03060c357fe --- /dev/null +++ b/changelogs/unreleased/41743-unused-selectors-for-cycle-analytics.yml @@ -0,0 +1,5 @@ +--- +title: Remove unused CSS selectors for Cycle Analytics +merge_request: 16270 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/41874-closed-todo.yml b/changelogs/unreleased/41874-closed-todo.yml new file mode 100644 index 00000000000..615bd011579 --- /dev/null +++ b/changelogs/unreleased/41874-closed-todo.yml @@ -0,0 +1,5 @@ +--- +title: Fix closed text for issues on Todos page +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/42025-fix-issue-api.yml b/changelogs/unreleased/42025-fix-issue-api.yml new file mode 100644 index 00000000000..abb83bb2fad --- /dev/null +++ b/changelogs/unreleased/42025-fix-issue-api.yml @@ -0,0 +1,5 @@ +--- +title: "[API] Fix creating issue when assignee_id is empty" +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/42031-fix-links-to-uploads-in-wikis.yml b/changelogs/unreleased/42031-fix-links-to-uploads-in-wikis.yml new file mode 100644 index 00000000000..027cb414f23 --- /dev/null +++ b/changelogs/unreleased/42031-fix-links-to-uploads-in-wikis.yml @@ -0,0 +1,5 @@ +--- +title: Fix links to uploaded files on wiki pages +merge_request: 16499 +author: +type: fixed diff --git a/changelogs/unreleased/42046-fork-icon.yml b/changelogs/unreleased/42046-fork-icon.yml new file mode 100644 index 00000000000..def89ff7b08 --- /dev/null +++ b/changelogs/unreleased/42046-fork-icon.yml @@ -0,0 +1,5 @@ +--- +title: Fix giant fork icons on forks page +merge_request: 16474 +author: +type: fixed diff --git a/changelogs/unreleased/42047-pg-10-support.yml b/changelogs/unreleased/42047-pg-10-support.yml new file mode 100644 index 00000000000..f98e59329c3 --- /dev/null +++ b/changelogs/unreleased/42047-pg-10-support.yml @@ -0,0 +1,5 @@ +--- +title: Support PostgreSQL 10 +merge_request: 16471 +author: +type: added diff --git a/changelogs/unreleased/42055-update-marked-from-0-3-6-to-0-3-12.yml b/changelogs/unreleased/42055-update-marked-from-0-3-6-to-0-3-12.yml new file mode 100644 index 00000000000..2b043761856 --- /dev/null +++ b/changelogs/unreleased/42055-update-marked-from-0-3-6-to-0-3-12.yml @@ -0,0 +1,5 @@ +--- +title: Update marked from 0.3.6 to 0.3.12 +merge_request: 16480 +author: Takuya Noguchi +type: security diff --git a/changelogs/unreleased/feature-39591-visibility-level.yml b/changelogs/unreleased/feature-39591-visibility-level.yml new file mode 100644 index 00000000000..4bbc9bdbb2e --- /dev/null +++ b/changelogs/unreleased/feature-39591-visibility-level.yml @@ -0,0 +1,5 @@ +--- +title: Open visibility level help in a new tab +merge_request: +author: Jussi Räsänen +type: fixed diff --git a/changelogs/unreleased/feature-merge-request-system-hook.yml b/changelogs/unreleased/feature-merge-request-system-hook.yml new file mode 100644 index 00000000000..cfc4c4235d6 --- /dev/null +++ b/changelogs/unreleased/feature-merge-request-system-hook.yml @@ -0,0 +1,5 @@ +--- +title: System hooks for Merge Requests +merge_request: 14387 +author: Alexis Reigel +type: added diff --git a/changelogs/unreleased/file-content-large-screen-padding.yml b/changelogs/unreleased/file-content-large-screen-padding.yml new file mode 100644 index 00000000000..5691cd09b1f --- /dev/null +++ b/changelogs/unreleased/file-content-large-screen-padding.yml @@ -0,0 +1,5 @@ +--- +title: Double padding for file-content wiki class on larger screens +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/fix-gb-improve-manual-action-tooltips.yml b/changelogs/unreleased/fix-gb-improve-manual-action-tooltips.yml new file mode 100644 index 00000000000..31b4734bc79 --- /dev/null +++ b/changelogs/unreleased/fix-gb-improve-manual-action-tooltips.yml @@ -0,0 +1,5 @@ +--- +title: Fix tooltip displayed for running manual actions +merge_request: 16489 +author: +type: fixed diff --git a/changelogs/unreleased/issue_41460.yml b/changelogs/unreleased/issue_41460.yml new file mode 100644 index 00000000000..24d3eae6bf8 --- /dev/null +++ b/changelogs/unreleased/issue_41460.yml @@ -0,0 +1,5 @@ +--- +title: Fix error on changes tab when merge request cannot be created +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/jej-lfs-rev-list-handles-non-utf-paths-41627.yml b/changelogs/unreleased/jej-lfs-rev-list-handles-non-utf-paths-41627.yml new file mode 100644 index 00000000000..24f18c07ac5 --- /dev/null +++ b/changelogs/unreleased/jej-lfs-rev-list-handles-non-utf-paths-41627.yml @@ -0,0 +1,5 @@ +--- +title: Prevent RevList failing on non utf8 paths +merge_request: 16440 +author: +type: fixed diff --git a/changelogs/unreleased/merge-request-target-branch-perf.yml b/changelogs/unreleased/merge-request-target-branch-perf.yml new file mode 100644 index 00000000000..37e326bfde3 --- /dev/null +++ b/changelogs/unreleased/merge-request-target-branch-perf.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of target branch dropdown +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/mr-status-box-update.yml b/changelogs/unreleased/mr-status-box-update.yml new file mode 100644 index 00000000000..68265be16a1 --- /dev/null +++ b/changelogs/unreleased/mr-status-box-update.yml @@ -0,0 +1,5 @@ +--- +title: Fixed merge request status badge not updating after merging +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-mermaid-start-on-load-typo.yml b/changelogs/unreleased/sh-fix-mermaid-start-on-load-typo.yml new file mode 100644 index 00000000000..a2d4ade8e54 --- /dev/null +++ b/changelogs/unreleased/sh-fix-mermaid-start-on-load-typo.yml @@ -0,0 +1,5 @@ +--- +title: Fix Mermaid drawings not loading on some browsers +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-remove-shared-runners-and-more.yml b/changelogs/unreleased/sh-remove-shared-runners-and-more.yml new file mode 100644 index 00000000000..cc079617883 --- /dev/null +++ b/changelogs/unreleased/sh-remove-shared-runners-and-more.yml @@ -0,0 +1,5 @@ +--- +title: Remove erroneous text in shared runners page that suggested more runners available +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/winh-search-page-filters.yml b/changelogs/unreleased/winh-search-page-filters.yml new file mode 100644 index 00000000000..90c5cd8d818 --- /dev/null +++ b/changelogs/unreleased/winh-search-page-filters.yml @@ -0,0 +1,5 @@ +--- +title: Filter groups and projects dropdowns of search page on backend +merge_request: 16336 +author: +type: fixed diff --git a/changelogs/unreleased/winh-style-modals.yml b/changelogs/unreleased/winh-style-modals.yml new file mode 100644 index 00000000000..b7d0293960d --- /dev/null +++ b/changelogs/unreleased/winh-style-modals.yml @@ -0,0 +1,5 @@ +--- +title: Adjust modal style to new design +merge_request: 16310 +author: +type: other diff --git a/config/initializers/ar5_pg_10_support.rb b/config/initializers/ar5_pg_10_support.rb new file mode 100644 index 00000000000..6fae770015c --- /dev/null +++ b/config/initializers/ar5_pg_10_support.rb @@ -0,0 +1,57 @@ +raise "Vendored ActiveRecord 5 code! Delete #{__FILE__}!" if ActiveRecord::VERSION::MAJOR >= 5 + +require 'active_record/connection_adapters/postgresql_adapter' +require 'active_record/connection_adapters/postgresql/schema_statements' + +# +# Monkey-patch the refused Rails 4.2 patch at https://github.com/rails/rails/pull/31330 +# +# Updates sequence logic to support PostgreSQL 10. +# +# rubocop:disable all +module ActiveRecord + module ConnectionAdapters + + # We need #postgresql_version to be public as in ActiveRecord 5 for seed_fu + # to work. In ActiveRecord 4, it is protected. + # https://github.com/mbleigh/seed-fu/issues/123 + class PostgreSQLAdapter + public :postgresql_version + end + + module PostgreSQL + module SchemaStatements + # Resets the sequence of a table's primary key to the maximum value. + def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: + unless pk and sequence + default_pk, default_sequence = pk_and_sequence_for(table) + + pk ||= default_pk + sequence ||= default_sequence + end + + if @logger && pk && !sequence + @logger.warn "#{table} has primary key #{pk} with no default sequence" + end + + if pk && sequence + quoted_sequence = quote_table_name(sequence) + max_pk = select_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}") + if max_pk.nil? + if postgresql_version >= 100000 + minvalue = select_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass") + else + minvalue = select_value("SELECT min_value FROM #{quoted_sequence}") + end + end + + select_value <<-end_sql, 'SCHEMA' + SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false}) + end_sql + end + end + end + end + end +end +# rubocop:enable all diff --git a/db/migrate/20170425112128_create_pipeline_schedules_table.rb b/db/migrate/20170425112128_create_pipeline_schedules_table.rb index 57df47f5f42..4f9c56a1ad8 100644 --- a/db/migrate/20170425112128_create_pipeline_schedules_table.rb +++ b/db/migrate/20170425112128_create_pipeline_schedules_table.rb @@ -17,7 +17,7 @@ class CreatePipelineSchedulesTable < ActiveRecord::Migration t.boolean :active, default: true t.datetime :deleted_at - t.timestamps + t.timestamps null: true end add_index(:ci_pipeline_schedules, :project_id) diff --git a/db/migrate/20171207185153_add_merge_request_state_index.rb b/db/migrate/20171207185153_add_merge_request_state_index.rb new file mode 100644 index 00000000000..72f846c5c38 --- /dev/null +++ b/db/migrate/20171207185153_add_merge_request_state_index.rb @@ -0,0 +1,18 @@ +class AddMergeRequestStateIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :merge_requests, [:source_project_id, :source_branch], + where: "state = 'opened'", + name: 'index_merge_requests_on_source_project_and_branch_state_opened' + end + + def down + remove_concurrent_index_by_name :merge_requests, + 'index_merge_requests_on_source_project_and_branch_state_opened' + end +end diff --git a/db/migrate/20171211145425_add_can_push_to_deploy_keys_projects.rb b/db/migrate/20171211145425_add_can_push_to_deploy_keys_projects.rb new file mode 100644 index 00000000000..5dc723db9f9 --- /dev/null +++ b/db/migrate/20171211145425_add_can_push_to_deploy_keys_projects.rb @@ -0,0 +1,15 @@ +class AddCanPushToDeployKeysProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + disable_ddl_transaction! + + def up + add_column_with_default :deploy_keys_projects, :can_push, :boolean, default: false, allow_null: false + end + + def down + remove_column :deploy_keys_projects, :can_push + end +end diff --git a/db/migrate/20171215113714_populate_can_push_from_deploy_keys_projects.rb b/db/migrate/20171215113714_populate_can_push_from_deploy_keys_projects.rb new file mode 100644 index 00000000000..680855af945 --- /dev/null +++ b/db/migrate/20171215113714_populate_can_push_from_deploy_keys_projects.rb @@ -0,0 +1,64 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PopulateCanPushFromDeployKeysProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + DATABASE_NAME = Gitlab::Database.database_name + + disable_ddl_transaction! + + class DeploysKeyProject < ActiveRecord::Base + include EachBatch + + self.table_name = 'deploy_keys_projects' + end + + def up + DeploysKeyProject.each_batch(of: 10_000) do |batch| + start_id, end_id = batch.pluck('MIN(id), MAX(id)').first + + if Gitlab::Database.mysql? + execute <<-EOF.strip_heredoc + UPDATE deploy_keys_projects, #{DATABASE_NAME}.keys + SET deploy_keys_projects.can_push = #{DATABASE_NAME}.keys.can_push + WHERE deploy_keys_projects.deploy_key_id = #{DATABASE_NAME}.keys.id + AND deploy_keys_projects.id BETWEEN #{start_id} AND #{end_id} + EOF + else + execute <<-EOF.strip_heredoc + UPDATE deploy_keys_projects + SET can_push = keys.can_push + FROM keys + WHERE deploy_key_id = keys.id + AND deploy_keys_projects.id BETWEEN #{start_id} AND #{end_id} + EOF + end + end + end + + def down + DeploysKeyProject.each_batch(of: 10_000) do |batch| + start_id, end_id = batch.pluck('MIN(id), MAX(id)').first + + if Gitlab::Database.mysql? + execute <<-EOF.strip_heredoc + UPDATE deploy_keys_projects, #{DATABASE_NAME}.keys + SET #{DATABASE_NAME}.keys.can_push = deploy_keys_projects.can_push + WHERE deploy_keys_projects.deploy_key_id = #{DATABASE_NAME}.keys.id + AND deploy_keys_projects.id BETWEEN #{start_id} AND #{end_id} + EOF + else + execute <<-EOF.strip_heredoc + UPDATE keys + SET can_push = deploy_keys_projects.can_push + FROM deploy_keys_projects + WHERE deploy_keys_projects.deploy_key_id = keys.id + AND deploy_keys_projects.id BETWEEN #{start_id} AND #{end_id} + EOF + end + end + end +end diff --git a/db/post_migrate/20171215121205_post_populate_can_push_from_deploy_keys_projects.rb b/db/post_migrate/20171215121205_post_populate_can_push_from_deploy_keys_projects.rb new file mode 100644 index 00000000000..3a5850df3db --- /dev/null +++ b/db/post_migrate/20171215121205_post_populate_can_push_from_deploy_keys_projects.rb @@ -0,0 +1,63 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PostPopulateCanPushFromDeployKeysProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + DATABASE_NAME = Gitlab::Database.database_name + + disable_ddl_transaction! + + class DeploysKeyProject < ActiveRecord::Base + include EachBatch + + self.table_name = 'deploy_keys_projects' + end + + def up + DeploysKeyProject.each_batch(of: 10_000) do |batch| + start_id, end_id = batch.pluck('MIN(id), MAX(id)').first + + if Gitlab::Database.mysql? + execute <<-EOF.strip_heredoc + UPDATE deploy_keys_projects, #{DATABASE_NAME}.keys + SET deploy_keys_projects.can_push = #{DATABASE_NAME}.keys.can_push + WHERE deploy_keys_projects.deploy_key_id = #{DATABASE_NAME}.keys.id + AND deploy_keys_projects.id BETWEEN #{start_id} AND #{end_id} + EOF + else + execute <<-EOF.strip_heredoc + UPDATE deploy_keys_projects + SET can_push = keys.can_push + FROM keys + WHERE deploy_key_id = keys.id + AND deploy_keys_projects.id BETWEEN #{start_id} AND #{end_id} + EOF + end + end + end + + def down + DeploysKeyProject.each_batch(of: 10_000) do |batch| + start_id, end_id = batch.pluck('MIN(id), MAX(id)').first + + if Gitlab::Database.mysql? + execute <<-EOF.strip_heredoc + UPDATE deploy_keys_projects, #{DATABASE_NAME}.keys + SET #{DATABASE_NAME}.keys.can_push = deploy_keys_projects.can_push + WHERE deploy_keys_projects.deploy_key_id = #{DATABASE_NAME}.keys.id + AND deploy_keys_projects.id BETWEEN #{start_id} AND #{end_id} + EOF + else + execute <<-EOF.strip_heredoc + UPDATE keys + SET can_push = deploy_keys_projects.can_push + FROM deploy_keys_projects + WHERE deploy_keys_projects.deploy_key_id = keys.id + AND deploy_keys_projects.id BETWEEN #{start_id} AND #{end_id} + EOF + end + end + end +end diff --git a/db/post_migrate/20171215121259_remove_can_push_from_keys.rb b/db/post_migrate/20171215121259_remove_can_push_from_keys.rb new file mode 100644 index 00000000000..0599811d986 --- /dev/null +++ b/db/post_migrate/20171215121259_remove_can_push_from_keys.rb @@ -0,0 +1,17 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveCanPushFromKeys < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + disable_ddl_transaction! + + def up + remove_column :keys, :can_push + end + + def down + add_column_with_default :keys, :can_push, :boolean, default: false, allow_null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index f47accca21a..13913b49902 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -626,6 +626,7 @@ ActiveRecord::Schema.define(version: 20180105212544) do t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" + t.boolean "can_push", default: false, null: false end add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree @@ -896,7 +897,6 @@ ActiveRecord::Schema.define(version: 20180105212544) do t.string "type" t.string "fingerprint" t.boolean "public", default: false, null: false - t.boolean "can_push", default: false, null: false t.datetime "last_used_at" end @@ -1109,6 +1109,7 @@ ActiveRecord::Schema.define(version: 20180105212544) do add_index "merge_requests", ["merge_user_id"], name: "index_merge_requests_on_merge_user_id", where: "(merge_user_id IS NOT NULL)", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree + add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_and_branch_state_opened", where: "((state)::text = 'opened'::text)", using: :btree add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree diff --git a/doc/README.md b/doc/README.md index 11d52001440..330670587f5 100644 --- a/doc/README.md +++ b/doc/README.md @@ -10,9 +10,9 @@ platform for software development! GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans: -- **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/), +- **GitLab Community Edition (CE)** is an [open source product](https://gitlab.com/gitlab-org/gitlab-ce/), self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com. -- **GitLab Enterprise Edition (EE)** is an [opencore product](https://gitlab.com/gitlab-org/gitlab-ee/), +- **GitLab Enterprise Edition (EE)** is an [open-core product](https://gitlab.com/gitlab-org/gitlab-ee/), self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)**, **GitLab Enterprise Edition Premium (EEP)**, and **GitLab Enterprise Edition Ultimate (EEU)**. - **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings). @@ -148,8 +148,8 @@ Regular users don't have access to GitLab administration tools and settings. ## Contributor documentation -GitLab Community Edition is [opensource](https://gitlab.com/gitlab-org/gitlab-ce/) -and Enterprise Editions are [opencore](https://gitlab.com/gitlab-org/gitlab-ee/). +GitLab Community Edition is [open source](https://gitlab.com/gitlab-org/gitlab-ce/) +and Enterprise Editions are [open-core](https://gitlab.com/gitlab-org/gitlab-ee/). Learn how to contribute to GitLab: - [Development](development/README.md): All styleguides and explanations how to contribute. diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 273d5a56b6f..698fa22a438 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -19,15 +19,13 @@ Example response: { "id": 1, "title": "Public key", - "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", - "can_push": false, + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", "created_at": "2013-10-02T10:12:29Z" }, { "id": 3, "title": "Another Public key", - "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", - "can_push": true, + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", "created_at": "2013-10-02T11:12:29Z" } ] @@ -57,15 +55,15 @@ Example response: "id": 1, "title": "Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", - "can_push": false, - "created_at": "2013-10-02T10:12:29Z" + "created_at": "2013-10-02T10:12:29Z", + "can_push": false }, { "id": 3, "title": "Another Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", - "can_push": false, - "created_at": "2013-10-02T11:12:29Z" + "created_at": "2013-10-02T11:12:29Z", + "can_push": false } ] ``` @@ -96,8 +94,8 @@ Example response: "id": 1, "title": "Public key", "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=", - "can_push": false, - "created_at": "2013-10-02T10:12:29Z" + "created_at": "2013-10-02T10:12:29Z", + "can_push": false } ``` @@ -135,6 +133,36 @@ Example response: } ``` +## Update deploy key + +Updates a deploy key for a project. + +``` +PUT /projects/:id/deploy_keys/:key_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `title` | string | no | New deploy key's title | +| `can_push` | boolean | no | Can deploy key push to the project's repository | + +```bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "New deploy key", "can_push": true}' "https://gitlab.example.com/api/v4/projects/5/deploy_keys/11" +``` + +Example response: + +```json +{ + "id": 11, + "title": "New deploy key", + "key": "ssh-rsa AAAA...", + "created_at": "2015-08-29T12:44:31.550Z", + "can_push": true +} +``` + ## Delete deploy key Removes a deploy key from the project. If the deploy key is used only for this project, it will be deleted from the system. diff --git a/doc/api/deployments.md b/doc/api/deployments.md index ab9e63e01d3..fd11894ea8f 100644 --- a/doc/api/deployments.md +++ b/doc/api/deployments.md @@ -11,6 +11,8 @@ GET /projects/:id/deployments | Attribute | Type | Required | Description | |-----------|---------|----------|---------------------| | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `order_by`| string | no | Return deployments ordered by `id` or `iid` or `created_at` or `ref` fields. Default is `id` | +| `sort` | string | no | Return deployments sorted in `asc` or `desc` order. Default is `asc` | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/deployments" diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 84930f0bdc9..d35e940d7b1 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -93,6 +93,19 @@ Parameters: - `start_date` (optional) - The start date of the milestone - `state_event` (optional) - The state event of the milestone (close|activate) +## Delete project milestone + +Only for user with developer access to the project. + +``` +DELETE /projects/:id/milestones/:milestone_id +``` + +Parameters: + +- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +- `milestone_id` (required) - The ID of the project's milestone + ## Get all issues assigned to a single milestone Gets all issues assigned to a single project milestone. diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index ad2521230e6..cc495c5d091 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -131,12 +131,13 @@ Available only for admins. GET /projects/:id/snippets/:snippet_id/user_agent_detail ``` -| Attribute | Type | Required | Description | -|-------------|---------|----------|--------------------------------------| -| `id` | Integer | yes | The ID of a snippet | +| Attribute | Type | Required | Description | +|---------------|---------|----------|--------------------------------------| +| `id` | Integer | yes | The ID of a project | +| `snippet_id` | Integer | yes | The ID of a snippet | ```bash -curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/snippets/1/user_agent_detail +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/snippets/2/user_agent_detail ``` Example response: diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index 9750475f0a6..dd424470b67 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -33,6 +33,7 @@ Example response: "created_at":"2016-10-31T12:32:15.192Z", "push_events":true, "tag_push_events":false, + "merge_requests_events": true, "enable_ssl_verification":true } ] @@ -54,6 +55,7 @@ POST /hooks | `token` | string | no | Secret token to validate received payloads; this will not be returned in the response | | `push_events` | boolean | no | When true, the hook will fire on push events | | `tag_push_events` | boolean | no | When true, the hook will fire on new tags being pushed | +| `merge_requests_events` | boolean | no | Trigger hook on merge requests events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | Example request: @@ -72,6 +74,7 @@ Example response: "created_at":"2016-10-31T12:32:15.192Z", "push_events":true, "tag_push_events":false, + "merge_requests_events": true, "enable_ssl_verification":true } ] diff --git a/doc/articles/index.md b/doc/articles/index.md index 01fb6cdf374..9f85533ea94 100644 --- a/doc/articles/index.md +++ b/doc/articles/index.md @@ -1,74 +1,18 @@ -# Technical Articles +--- +comments: false +--- -[Technical Articles](../development/writing_documentation.md#technical-articles) are +# Technical articles list (deprecated) + +[Technical articles](../development/writing_documentation.md#technical-articles) are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives. -They are written by members of the GitLab Team and by -[Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). - -Part of the articles listed below link to the [GitLab Blog](https://about.gitlab.com/blog/), -where they were originally published. - -## GitLab Pages - -Learn how to deploy a static website with [GitLab Pages](../user/project/pages/index.md#getting-started): - -| Article title | Category | Publishing date | -| :------------ | :------: | --------------: | -| **Series: GitLab Pages from A to Z:** | -| [- Part 1: Static sites and GitLab Pages domains](../user/project/pages/getting_started_part_one.md)| User guide | 2017-02-22 | -| [- Part 2: Quick start guide - Setting up GitLab Pages](../user/project/pages/getting_started_part_two.md)| User guide | 2017-02-22 | -| [- Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](../user/project/pages/getting_started_part_three.md)| User guide | 2017-02-22 | -| [- Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](../user/project/pages/getting_started_part_four.md)| User guide | 2017-02-22 | -| [Setting up GitLab Pages with CloudFlare Certificates](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) | Tutorial | 2017-02-07 | -| [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) | Tutorial | 2016-12-07 | -| [Publish Code Coverage Report with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) | Tutorial | 2016-11-03 | -| [GitLab CI: Deployment & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) | Tutorial | 2016-08-26 | -| [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) | Tutorial | 2016-08-19 | -| **Series: Static Site Generator:** | -| [- Part 1: Dynamic vs Static Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) | Tutorial | 2016-06-03 | -| [- Part 2: Modern Static Site Generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) | Tutorial | 2016-06-10 | -| [- Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) | Tutorial | 2016-06-17 | -| [Securing your GitLab Pages with TLS and Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) | Tutorial | 2016-04-11 | -| [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) | Tutorial | 2016-04-07 | - -## Install and maintain GitLab - -[Admin](../README.md#administrator-documentation), [install](../install/README.md), -upgrade, integrate, migrate to GitLab: - -| Article title | Category | Publishing date | -| :------------ | :------: | --------------: | -| [Video Tutorial: Idea to Production on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/) | Tutorial | 2017-01-23 | -| [How to Setup a GitLab Instance on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/) | Tutorial | 2016-07-13 | -| [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/) | Tutorial | 2016-04-27 | - -## Software development - -Explore the best of GitLab's software development's capabilities: - -| Article title | Category | Publishing date | -| :------------ | :------: | --------------: | -| [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) | Concepts | 2017-07-13 | -| [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/)| Concepts | 2017-06-29 | -| [Fast and Natural Continuous Integration with GitLab CI](https://about.gitlab.com/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/) | Concepts | 2017-05-22 | -| [Demo: Auto-Deploy from GitLab to an OpenShift Container Cluster](https://about.gitlab.com/2017/05/16/devops-containers-gitlab-openshift/) | Technical overview | 2017-05-16 | -| [Demo: GitLab Service Desk](https://about.gitlab.com/2017/05/09/demo-service-desk/) | Feature highlight | 2017-05-09 | -| [Demo: Mapping Work Versus Time, With Burndown Charts](https://about.gitlab.com/2017/04/25/mapping-work-to-do-versus-time-with-burndown-charts/) | Feature highlight | 2017-04-25 | -| [Demo: Cloud Native Development with GitLab](https://about.gitlab.com/2017/04/18/cloud-native-demo/) | Feature highlight | 2017-04-18 | -| [Demo: Mastering Code Review With GitLab](https://about.gitlab.com/2017/03/17/demo-mastering-code-review-with-gitlab/) | Feature highlight | 2017-03-17 | -| [In 13 minutes from Kubernetes to a complete application development tool](https://about.gitlab.com/2016/11/14/idea-to-production/) | Technical overview | 2016-11-14 | -| [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) | Technical overview | 2016-10-25 | -| [Trends in Version Control Land: Microservices](https://about.gitlab.com/2016/08/16/trends-in-version-control-land-microservices/) | Concepts | 2016-08-16 | -| [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) | Concepts | 2016-08-05 | -| [Trends in Version Control Land: Innersourcing](https://about.gitlab.com/2016/07/07/trends-version-control-innersourcing/) | Concepts | 2016-07-07 | -| [Tutorial: It's all connected in GitLab](https://about.gitlab.com/2016/03/08/gitlab-tutorial-its-all-connected/) | Technical overview | 2016-03-08 | - -## Technologies +The list of technical articles was [deprecated](https://gitlab.com/gitlab-org/gitlab-ce/issues/41138) in favor of having them linked from their topic-related documentation: -| Article title | Category | Publishing date | -| :------------ | :------: | --------------: | -| [Why we are not leaving the cloud](https://about.gitlab.com/2017/03/02/why-we-are-not-leaving-the-cloud/) | Concepts | 2017-03-02 | -| [Why We Chose Vue.js](https://about.gitlab.com/2016/10/20/why-we-chose-vue/) | Concepts | 2016-10-20 | -| [Markdown Kramdown Tips & Tricks](https://about.gitlab.com/2016/07/19/markdown-kramdown-tips-and-tricks/) | Technical overview | 2016-07-19 | +- [Git](../topics/git/index.md) +- [GitLab administrator](../administration/index.md) +- [GitLab CI/CD](../ci/README.md) +- [GitLab Pages](../user/project/pages/index.md) +- [GitLab user](../user/index.md) +- [Install GitLab](../install/README.md) diff --git a/doc/articles/numerous_undo_possibilities_in_git/index.md b/doc/articles/numerous_undo_possibilities_in_git/index.md index 895bbccec08..3f46ee9a5e6 100644 --- a/doc/articles/numerous_undo_possibilities_in_git/index.md +++ b/doc/articles/numerous_undo_possibilities_in_git/index.md @@ -1,497 +1 @@ -# Numerous undo possibilities in Git - -> **Article [Type](../../development/writing_documentation.md#types-of-technical-articles):** tutorial || -> **Level:** intermediary || -> **Author:** [Crt Mori](https://gitlab.com/Letme) || -> **Publication date:** 2017-08-17 - -## Introduction - -In this tutorial, we will show you different ways of undoing your work in Git, for which -we will assume you have a basic working knowledge of. Check GitLab's -[Git documentation](../../topics/git/index.md#git-documentation) for reference. -Also, we will only provide some general info of the commands, which is enough -to get you started for the easy cases/examples, but for anything more advanced please refer to the [Git book](https://git-scm.com/book/en/v2). - -We will explain a few different techniques to undo your changes based on the stage -of the change in your current development. Also, keep in mind that [nothing in -Git is really deleted.][git-autoclean-ref] -This means that until Git automatically cleans detached commits (which cannot be -accessed by branch or tag) it will be possible to view them with `git reflog` command -and access them with direct commit-id. Read more about _[redoing the undo](#redoing-the-undo)_ on the section below. - -This guide is organized depending on the [stage of development][git-basics] -where you want to undo your changes from and if they were shared with other developers -or not. Because Git is tracking changes a created or edited file is in the unstaged state -(if created it is untracked by Git). After you add it to a repository (`git add`) you put -a file into the **staged** state, which is then committed (`git commit`) to your -local repository. After that, file can be shared with other developers (`git push`). -Here's what we'll cover in this tutorial: - - - [Undo local changes](#undo-local-changes) which were not pushed to remote repository - - - Before you commit, in both unstaged and staged state - - After you committed - - - Undo changes after they are pushed to remote repository - - - [Without history modification](#undo-remote-changes-without-changing-history) (preferred way) - - [With history modification](#undo-remote-changes-with-modifying-history) (requires - coordination with team and force pushes). - - - [Usecases when modifying history is generally acceptable](#where-modifying-history-is-generally-acceptable) - - [How to modify history](#how-modifying-history-is-done) - - [How to remove sensitive information from repository](#deleting-sensitive-information-from-commits) - - -### Branching strategy - -[Git][git-official] is a de-centralized version control system, which means that beside regular -versioning of the whole repository, it has possibilities to exchange changes -with other repositories. To avoid chaos with -[multiple sources of truth][git-distributed], various -development workflows have to be followed, and it depends on your internal -workflow how certain changes or commits can be undone or changed. -[GitLab Flow][gitlab-flow] provides a good -balance between developers clashing with each other while -developing the same feature and cooperating seamlessly, but it does not enable -joined development of the same feature by multiple developers by default. -When multiple developers develop the same feature on the same branch, clashing -with every synchronization is unavoidable, but a proper or chosen Git Workflow will -prevent that anything is lost or out of sync when feature is complete. You can also -read through this blog post on [Git Tips & Tricks][gitlab-git-tips-n-tricks] -to learn how to easily **do** things in Git. - - -## Undo local changes - -Until you push your changes to any remote repository, they will only affect you. -That broadens your options on how to handle undoing them. Still, local changes -can be on various stages and each stage has a different approach on how to tackle them. - - -### Unstaged local changes (before you commit) - -When a change is made, but it is not added to the staged tree, Git itself -proposes a solution to discard changes to certain file. - -Suppose you edited a file to change the content using your favorite editor: - -```shell -vim <file> -``` - -Since you did not `git add <file>` to staging, it should be under unstaged files (or -untracked if file was created). You can confirm that with: - -```shell -$ git status -On branch master -Your branch is up-to-date with 'origin/master'. -Changes not staged for commit: - (use "git add <file>..." to update what will be committed) - (use "git checkout -- <file>..." to discard changes in working directory) - - modified: <file> -no changes added to commit (use "git add" and/or "git commit -a") -``` - -At this point there are 3 options to undo the local changes you have: - - - Discard all local changes, but save them for possible re-use [later](#quickly-save-local-changes) - - ```shell - git stash - ``` - - - Discarding local changes (permanently) to a file - - ```shell - git checkout -- <file> - ``` - - - Discard all local changes to all files permanently - - ```shell - git reset --hard - ``` - - -Before executing `git reset --hard`, keep in mind that there is also a way to -just temporary store the changes without committing them using `git stash`. -This command resets the changes to all files, but it also saves them in case -you would like to apply them at some later time. You can read more about it in -[section below](#quickly-save-local-changes). - -### Quickly save local changes - -You are working on a feature when a boss drops by with an urgent task. Since your -feature is not complete, but you need to swap to another branch, you can use -`git stash` to save what you had done, swap to another branch, commit, push, -test, then get back to previous feature branch, do `git stash pop` and continue -where you left. - -The example above shows that discarding all changes is not always a preferred option, -but Git provides a way to save them for later, while resetting the repository to state without -them. This is achieved by Git stashing command `git stash`, which in fact saves your -current work and runs `git reset --hard`, but it also has various -additional options like: - - - `git stash save`, which enables including temporary commit message, which will help you identify changes, among with other options - - `git stash list`, which lists all previously stashed commits (yes, there can be more) that were not `pop`ed - - `git stash pop`, which redoes previously stashed changes and removes them from stashed list - - `git stash apply`, which redoes previously stashed changes, but keeps them on stashed list - -### Staged local changes (before you commit) - -Let's say you have added some files to staging, but you want to remove them from the -current commit, yet you want to retain those changes - just move them outside -of the staging tree. You also have an option to discard all changes with -`git reset --hard` or think about `git stash` [as described earlier.](#quickly-save-local-changes) - -Lets start the example by editing a file, with your favorite editor, to change the -content and add it to staging - -``` -vim <file> -git add <file> -``` - -The file is now added to staging as confirmed by `git status` command: - -```shell -$ git status -On branch master -Your branch is up-to-date with 'origin/master'. -Changes to be committed: - (use "git reset HEAD <file>..." to unstage) - - new file: <file> -``` - -Now you have 4 options to undo your changes: - - - Unstage the file to current commit (HEAD) - - ```shell - git reset HEAD <file> - ``` - - - Unstage everything - retain changes - - ```shell - git reset - ``` - - - Discard all local changes, but save them for [later](#quickly-save-local-changes) - - ```shell - git stash - ``` - - - Discard everything permanently - - ```shell - git reset --hard - ``` - -## Committed local changes - -Once you commit, your changes are recorded by the version control system. -Because you haven't pushed to your remote repository yet, your changes are -still not public (or shared with other developers). At this point, undoing -things is a lot easier, we have quite some workaround options. Once you push -your code, you'll have less options to troubleshoot your work. - -### Without modifying history - -Through the development process some of the previously committed changes do not -fit anymore in the end solution, or are source of the bugs. Once you find the -commit which triggered bug, or once you have a faulty commit, you can simply -revert it with `git revert commit-id`. This command inverts (swaps) the additions and -deletions in that commit, so that it does not modify history. Retaining history -can be helpful in future to notice that some changes have been tried -unsuccessfully in the past. - -In our example we will assume there are commits `A`,`B`,`C`,`D`,`E` committed in this order: `A-B-C-D-E`, -and `B` is the commit you want to undo. There are many different ways to identify commit -`B` as bad, one of them is to pass a range to `git bisect` command. The provided range includes -last known good commit (we assume `A`) and first known bad commit (where bug was detected - we will assume `E`). - -```shell -git bisect A..E -``` - -Bisect will provide us with commit-id of the middle commit to test, and then guide us -through simple bisection process. You can read more about it [in official Git Tools][git-debug] -In our example we will end up with commit `B`, that introduced bug/error. We have -4 options on how to remove it (or part of it) from our repository. - -- Undo (swap additions and deletions) changes introduced by commit `B`. - - ```shell - git revert commit-B-id - ``` - -- Undo changes on a single file or directory from commit `B`, but retain them in the staged state - - ```shell - git checkout commit-B-id <file> - ``` - -- Undo changes on a single file or directory from commit `B`, but retain them in the unstaged state - - ```shell - git reset commit-B-id <file> - ``` - - - There is one command we also must not forget: **creating a new branch** - from the point where changes are not applicable or where the development has hit a - dead end. For example you have done commits `A-B-C-D` on your feature-branch - and then you figure `C` and `D` are wrong. At this point you either reset to `B` - and do commit `F` (which will cause problems with pushing and if forced pushed also with other developers) - since branch now looks `A-B-F`, which clashes with what other developers have locally (you will - [change history](#with-history-modification)), or you simply checkout commit `B` create - a new branch and do commit `F`. In the last case, everyone else can still do their work while you - have your new way to get it right and merge it back in later. Alternatively, with GitLab, - you can [cherry-pick](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit) - that commit into a new merge request. - - ![Create a new branch to avoid clashing](img/branching.png) - - ```shell - git checkout commit-B-id - git checkout -b new-path-of-feature - # Create <commit F> - git commit -a - ``` - -### With history modification - -There is one command for history modification and that is `git rebase`. Command -provides interactive mode (`-i` flag) which enables you to: - - - **reword** commit messages (there is also `git commit --amend` for editing - last commit message) - - **edit** the commit content (changes introduced by commit) and message - - **squash** multiple commits into a single one, and have a custom or aggregated - commit message - - **drop** commits - simply delete them - - and few more options - -Let us check few examples. Again there are commits `A-B-C-D` where you want to -delete commit `B`. - -- Rebase the range from current commit D to A: - - ```shell - git rebase -i A - ``` - -- Command opens your favorite editor where you write `drop` in front of commit - `B`, but you leave default `pick` with all other commits. Save and exit the - editor to perform a rebase. Remember: if you want to cancel delete whole - file content before saving and exiting the editor - -In case you want to modify something introduced in commit `B`. - -- Rebase the range from current commit D to A: - - ```shell - git rebase -i A - ``` - -- Command opens your favorite text editor where you write `edit` in front of commit - `B`, but leave default `pick` with all other commits. Save and exit the editor to - perform a rebase - -- Now do your edits and commit changes: - - ```shell - git commit -a - ``` - -You can find some more examples in [below section where we explain how to modify -history](#how-modifying-history-is-done) - - -### Redoing the Undo - -Sometimes you realize that the changes you undid were useful and you want them -back. Well because of first paragraph you are in luck. Command `git reflog` -enables you to *recall* detached local commits by referencing or applying them -via commit-id. Although, do not expect to see really old commits in reflog, because -Git regularly [cleans the commits which are *unreachable* by branches or tags][git-autoclean-ref]. - -To view repository history and to track older commits you can use below command: - -```shell -$ git reflog show - -# Example output: -b673187 HEAD@{4}: merge 6e43d5987921bde189640cc1e37661f7f75c9c0b: Merge made by the 'recursive' strategy. -eb37e74 HEAD@{5}: rebase -i (finish): returning to refs/heads/master -eb37e74 HEAD@{6}: rebase -i (pick): Commit C -97436c6 HEAD@{7}: rebase -i (start): checkout 97436c6eec6396c63856c19b6a96372705b08b1b -... -88f1867 HEAD@{12}: commit: Commit D -97436c6 HEAD@{13}: checkout: moving from 97436c6eec6396c63856c19b6a96372705b08b1b to test -97436c6 HEAD@{14}: checkout: moving from master to 97436c6 -05cc326 HEAD@{15}: commit: Commit C -6e43d59 HEAD@{16}: commit: Commit B -``` - -Output of command shows repository history. In first column there is commit-id, -in following column, number next to `HEAD` indicates how many commits ago something -was made, after that indicator of action that was made (commit, rebase, merge, ...) -and then on end description of that action. - -## Undo remote changes without changing history - -This topic is roughly same as modifying committed local changes without modifying -history. **It should be the preferred way of undoing changes on any remote repository -or public branch.** Keep in mind that branching is the best solution when you want -to retain the history of faulty development, yet start anew from certain point. Branching -enables you to include the existing changes in new development (by merging) and -it also provides a clear timeline and development structure. - -![Use revert to keep branch flowing](img/revert.png) - -If you want to revert changes introduced in certain `commit-id` you can simply -revert that `commit-id` (swap additions and deletions) in newly created commit: -You can do this with - -```shell -git revert commit-id -``` - -or creating a new branch: - -```shell -git checkout commit-id -git checkout -b new-path-of-feature -``` - -## Undo remote changes with modifying history - -This is useful when you want to *hide* certain things - like secret keys, -passwords, SSH keys, etc. It is and should not be used to hide mistakes, as -it will make it harder to debug in case there are some other bugs. The main -reason for this is that you loose the real development progress. **Also keep in -mind that, even with modified history, commits are just detached and can still be -accessed through commit-id** - at least until all repositories perform -the cleanup of detached commits (happens automatically). - -![Modifying history causes problems on remote branch](img/rebase_reset.png) - -### Where modifying history is generally acceptable - -Modified history breaks the development chain of other developers, as changed -history does not have matching commits'ids. For that reason it should not -be used on any public branch or on branch that *might* be used by other -developers. When contributing to big open source repositories (e.g. [GitLab CE][gitlab-ce]), -it is acceptable to *squash* commits into a single one, to present -a nicer history of your contribution. -Keep in mind that this also removes the comments attached to certain commits -in merge requests, so if you need to retain traceability in GitLab, then -modifying history is not acceptable. -A feature-branch of a merge request is a public branch and might be used by -other developers, but project process and rules might allow or require -you to use `git rebase` (command that changes history) to reduce number of -displayed commits on target branch after reviews are done (for example -GitLab). There is a `git merge --squash` command which does exactly that -(squashes commits on feature-branch to a single commit on target branch -at merge). - ->**Note:** -Never modify the commit history of `master` or shared branch - -### How modifying history is done - -After you know what you want to modify (how far in history or how which range of -old commits), use `git rebase -i commit-id`. This command will then display all the commits from -current version to chosen commit-id and allow modification, squashing, deletion -of that commits. - -```shell -$ git rebase -i commit1-id..commit3-id -pick <commit1-id> <commit1-commit-message> -pick <commit2-id> <commit2-commit-message> -pick <commit3-id> <commit3-commit-message> - -# Rebase commit1-id..commit3-id onto <commit4-id> (3 command(s)) -# -# Commands: -# p, pick = use commit -# r, reword = use commit, but edit the commit message -# e, edit = use commit, but stop for amending -# s, squash = use commit, but meld into previous commit -# f, fixup = like "squash", but discard this commit's log message -# x, exec = run command (the rest of the line) using shell -# d, drop = remove commit -# -# These lines can be re-ordered; they are executed from top to bottom. -# -# If you remove a line here THAT COMMIT WILL BE LOST. -# -# However, if you remove everything, the rebase will be aborted. -# -# Note that empty commits are commented out -``` - ->**Note:** -It is important to notice that comment from the output clearly states that, if -you decide to abort, then do not just close your editor (as that will in-fact -modify history), but remove all uncommented lines and save. - -That is one of the reasons why `git rebase` should be used carefully on -shared and remote branches. But don't worry, there will be nothing broken until -you push back to the remote repository (so you can freely explore the -different outcomes locally). - -```shell -# Modify history from commit-id to HEAD (current commit) -git rebase -i commit-id -``` - -### Deleting sensitive information from commits - -Git also enables you to delete sensitive information from your past commits and -it does modify history in the progress. That is why we have included it in this -section and not as a standalone topic. To do so, you should run the -`git filter-branch`, which enables you to rewrite history with -[certain filters][git-filters-manual]. -This command uses rebase to modify history and if you want to remove certain -file from history altogether use: - -```shell -git filter-branch --tree-filter 'rm filename' HEAD -``` - -Since `git filter-branch` command might be slow on big repositories, there are -tools that can use some of Git specifics to enable faster execution of common -tasks (which is exactly what removing sensitive information file is about). -An alternative is [BFG Repo-cleaner][bfg-repo-cleaner]. Keep in mind that these -tools are faster because they do not provide a same fully feature set as `git filter-branch` -does, but focus on specific usecases. - -## Conclusion - -There are various options of undoing your work with any version control system, but -because of de-centralized nature of Git, these options are multiplied (or limited) -depending on the stage of your process. Git also enables rewriting history, but that -should be avoided as it might cause problems when multiple developers are -contributing to the same codebase. - -<!-- Identifiers, in alphabetical order --> - -[bfg-repo-cleaner]: https://rtyley.github.io/bfg-repo-cleaner/ -[git-autoclean-ref]: https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery -[git-basics]: https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository -[git-debug]: https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git -[git-distributed]: https://git-scm.com/about/distributed -[git-filters-manual]: https://git-scm.com/docs/git-filter-branch#_options -[git-official]: https://git-scm.com/ -[gitlab-ce]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria -[gitlab-flow]: https://about.gitlab.com/2014/09/29/gitlab-flow/ -[gitlab-git-tips-n-tricks]: https://about.gitlab.com/2016/12/08/git-tips-and-tricks/ +This document was moved to [another location](../../topics/git/numerous_undo_possibilities_in_git/index.md). diff --git a/doc/ci/README.md b/doc/ci/README.md index 3a10365af77..eabeb4510db 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -28,6 +28,7 @@ you don't need to set up anything to start to use them with GitLab CI/CD. - Article (2016-08-05): [Continuous Integration, Delivery, and Deployment with GitLab - Intro to CI/CD](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) - Article (2015-12-14): [Getting started with GitLab and GitLab CI - Intro to CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/) - Article (2017-07-13): [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) +- Article (2017-05-22): [Fast and Natural Continuous Integration with GitLab CI](https://about.gitlab.com/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/) - **Videos:** - Demo (Streamed live on Jul 17, 2017): [GitLab CI/CD Deep Dive](https://youtu.be/pBe4t1CD8Fc?t=195) - Demo (March, 2017): [How to get started using CI/CD with GitLab](https://about.gitlab.com/2017/03/13/ci-cd-demo/) @@ -93,17 +94,6 @@ Leverage the power of Docker to run your CI pipelines. See the documentation on [GitLab Pages](../user/project/pages/index.md). -## Special configuration (GitLab admin) - -As a GitLab administrator, you can change the default behavior of GitLab CI/CD in -your whole GitLab instance as well as in each project. - -- **Project specific:** - - [Pipelines settings](../user/project/pipelines/settings.md) - - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) -- **Affecting the whole GitLab instance:** - - [Continuous Integration admin settings](../user/admin_area/settings/continuous_integration.md) - ## Examples Check the [GitLab CI/CD examples](examples/README.md) for a collection of tutorials and guides on setting up your CI/CD pipeline for various programming languages, frameworks, @@ -115,6 +105,18 @@ and operating systems. - Article (2016-05-05): [Getting Started with GitLab and Shippable Continuous Integration](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/) - Article (2016-04-19): [GitLab Partners with DigitalOcean to make Continuous Integration faster, safer, and more affordable](https://about.gitlab.com/2016/04/19/gitlab-partners-with-digitalocean-to-make-continuous-integration-faster-safer-and-more-affordable/) +## Special configuration (GitLab admin) + +As a GitLab administrator, you can change the default behavior of GitLab CI/CD in +your whole GitLab instance as well as in each project. + +- [Continuous Integration admin settings](../administration/index.md#continuous-integration-settings) +- **Project specific:** + - [Pipelines settings](../user/project/pipelines/settings.md) + - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) +- **Affecting the whole GitLab instance:** + - [Continuous Integration admin settings](../user/admin_area/settings/continuous_integration.md) + ## Breaking changes - [CI variables renaming for GitLab 9.0](variables/README.md#9-0-renaming) Read about the diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index 474cb28b9e4..7102af5c529 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -37,6 +37,8 @@ during the deployment. We made a [simple guide](quick_start_guide.md) to using Auto Deploy with GitLab.com. +For a demonstration of GitLab Auto Deploy, read the blog post [Auto Deploy from GitLab to an OpenShift Container Cluster](https://about.gitlab.com/2017/05/16/devops-containers-gitlab-openshift/) + ## Supported templates The list of supported auto deploy templates is available in the diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index b53bd79f39e..0109e77935a 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -41,6 +41,19 @@ There's also a collection of repositories with [example projects](https://gitlab [Analyze code quality with the Code Climate CLI](code_climate.md). +### Static Application Security Testing (SAST) + +- **(EEU)** [Scan your code for vulnerabilities](https://docs.gitlab.com/ee/ci/examples/sast.html) +- [Scan your Docker images for vulnerabilities](sast_docker.md) + +### Dynamic Application Security Testing (DAST) + +Scan your app for vulnerabilities with GitLab [Dynamic Application Security Testing (DAST)](dast.md). + +### Browser Performance Testing with Sitespeed.io + +Analyze your [browser performance with Sitespeed.io](browser_performance.md). + ### GitLab CI/CD for Review Apps - [Example project](https://gitlab.com/gitlab-examples/review-apps-nginx/) that shows how to use GitLab CI/CD for [Review Apps](../review_apps/index.html). diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md new file mode 100644 index 00000000000..16ff8d5bb3e --- /dev/null +++ b/doc/ci/examples/dast.md @@ -0,0 +1,35 @@ +# Dynamic Application Security Testing with GitLab CI/CD + +This example shows how to run +[Dynamic Application Security Testing (DAST)](https://en.wikipedia.org/wiki/Dynamic_program_analysis) +on your project's source code by using GitLab CI/CD. + +DAST is using the popular open source tool +[OWASP ZAProxy](https://github.com/zaproxy/zaproxy) to perform an analysis. + +All you need is a GitLab Runner with the Docker executor (the shared Runners on +GitLab.com will work fine). You can then add a new job to `.gitlab-ci.yml`, +called `dast`: + +```yaml +dast: + image: owasp/zap2docker-stable + script: + - mkdir /zap/wrk/ + - /zap/zap-baseline.py -J gl-dast-report.json -t https://example.com || true + - cp /zap/wrk/gl-dast-report.json . + artifacts: + paths: [gl-dast-report.json] +``` + +The above example will create a `dast` job in your CI pipeline and will allow +you to download and analyze the report artifact in JSON format. + +TIP: **Tip:** +Starting with [GitLab Enterprise Edition Ultimate][ee] 10.4, this information will +be automatically extracted and shown right in the merge request widget. To do +so, the CI job must be named `dast` and the artifact path must be +`gl-dast-report.json`. +[Learn more on dynamic application security testing results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html). + +[ee]: https://about.gitlab.com/gitlab-ee/ diff --git a/doc/ci/examples/sast_docker.md b/doc/ci/examples/sast_docker.md new file mode 100644 index 00000000000..d99cfe93afa --- /dev/null +++ b/doc/ci/examples/sast_docker.md @@ -0,0 +1,55 @@ +# Static Application Security Testing for Docker containers with GitLab CI/CD + +You can check your Docker images (or more precisely the containers) for known +vulnerabilities by using [Clair](https://github.com/coreos/clair) and +[clair-scanner](https://github.com/arminc/clair-scanner), two open source tools +for Vulnerability Static Analysis for containers. + +All you need is a GitLab Runner with the Docker executor (the shared Runners on +GitLab.com will work fine). You can then add a new job to `.gitlab-ci.yml`, +called `sast:container`: + +```yaml +sast:container: + image: docker:latest + variables: + DOCKER_DRIVER: overlay2 + ## Define two new variables based on GitLab's CI/CD predefined variables + ## https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables + CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG + CI_APPLICATION_TAG: $CI_COMMIT_SHA + allow_failure: true + services: + - docker:dind + script: + - docker run -d --name db arminc/clair-db:latest + - docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1 + - apk add -U wget ca-certificates + - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} + - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64 + - mv clair-scanner_linux_amd64 clair-scanner + - chmod +x clair-scanner + - touch clair-whitelist.yml + - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-sast-container-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true + artifacts: + paths: [gl-sast-container-report.json] +``` + +The above example will create a `sast:container` job in your CI/CD pipeline, pull +the image from the [Container Registry](../../user/project/container_registry.md) +(whose name is defined from the two `CI_APPLICATION_` variables) and scan it +for possible vulnerabilities. The report will be saved as an artifact that you +can later download and analyze. + +If you want to whitelist some specific vulnerabilities, you can do so by defining +them in a [YAML file](https://github.com/arminc/clair-scanner/blob/master/README.md#example-whitelist-yaml-file), +in our case its named `clair-whitelist.yml`. + +TIP: **Tip:** +Starting with [GitLab Enterprise Edition Ultimate][ee] 10.4, this information will +be automatically extracted and shown right in the merge request widget. To do +so, the CI/CD job must be named `sast:container` and the artifact path must be +`gl-sast-container-report.json`. +[Learn more on application security testing results shown in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/sast_docker.html). + +[ee]: https://about.gitlab.com/gitlab-ee/ diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md index 10fd2616fab..7f9ab1f3a5e 100644 --- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -10,6 +10,7 @@ This is what the `.gitlab-ci.yml` file looks like for this project: ```yaml test: + stage: test script: - apt-get update -qy - apt-get install -y nodejs @@ -18,7 +19,7 @@ test: - bundle exec rake test staging: - type: deploy + stage: deploy script: - gem install dpl - dpl --provider=heroku --app=gitlab-ci-ruby-test-staging --api-key=$HEROKU_STAGING_API_KEY @@ -26,7 +27,7 @@ staging: - master production: - type: deploy + stage: deploy script: - gem install dpl - dpl --provider=heroku --app=gitlab-ci-ruby-test-prod --api-key=$HEROKU_PRODUCTION_API_KEY diff --git a/doc/ci/examples/test-phoenix-application.md b/doc/ci/examples/test-phoenix-application.md index f6c81b076bc..7e49721daf1 100644 --- a/doc/ci/examples/test-phoenix-application.md +++ b/doc/ci/examples/test-phoenix-application.md @@ -53,4 +53,3 @@ If you do not have any migrations yet, you will need to create an empty ## Sources - https://medium.com/@nahtnam/using-phoenix-on-gitlab-ci-5a51eec81142 -- https://davejlong.com/ci-with-phoenix-and-gitlab/ diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index ae0b5c0a2ba..82052cc0376 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -258,7 +258,7 @@ The `cache:key` variable can use any of the [predefined variables](../variables/ The default key is **default** across the project, therefore everything is shared between each pipelines and jobs by default, starting from GitLab 9.0. ->**Note:** The `cache:key` variable cannot contain the `/` character. +>**Note:** The `cache:key` variable cannot contain the `/` character, or the equivalent URI encoded `%2F`; a value made only of dots (`.`, `%2E`) is also forbidden. --- diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 9cb1f708a6a..f41d31797af 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -34,7 +34,6 @@ The table below shows what kind of documentation goes where. | `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). | | `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. | | `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) | -| `doc/articles/` | [Technical Articles](writing_documentation.md#technical-articles): user guides, admin guides, technical overviews, tutorials (`doc/articles/article-title/index.md`). | --- @@ -67,11 +66,10 @@ The table below shows what kind of documentation goes where. 1. The `doc/topics/` directory holds topic-related technical content. Create `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary. General user- and admin- related documentation, should be placed accordingly. -1. For technical articles, place their images under `doc/articles/article-title/img/`. --- -If you are unsure where a document should live, you can ping `@axil` in your +If you are unsure where a document should live, you can ping `@axil` or `@marcia` in your merge request. ## Text @@ -108,8 +106,8 @@ merge request. - Avoid adding things that show ephemeral statuses. For example, if a feature is considered beta or experimental, put this info in a note, not in the heading. - When introducing a new document, be careful for the headings to be - grammatically and syntactically correct. It is advised to mention one or all - of the following GitLab members for a review: `@axil`, `@rspeicher`, `@marcia`. + grammatically and syntactically correct. Mention one or all + of the following GitLab members for a review: `@axil` or `@marcia`. This is to ensure that no document with wrong heading is going live without an audit, thus preventing dead links and redirection issues when corrected @@ -203,7 +201,7 @@ You can combine one or more of the following: - Keep all file names in lower case. - Consider using PNG images instead of JPEG. - Compress all images with <https://tinypng.com/> or similar tool. -- Compress gifs with <https://ezgif.com/optimize> or similar toll. +- Compress gifs with <https://ezgif.com/optimize> or similar tool. - Images should be used (only when necessary) to _illustrate_ the description of a process, not to _replace_ it. @@ -330,6 +328,10 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to git grep -n "lfs/lfs_administration" ``` +NOTE: **Note:** +If the document being moved has any Disqus comments on it, there are extra steps +to follow documented just [below](#redirections-for-pages-with-disqus-comments). + Things to note: - Since we also use inline documentation, except for the documentation itself, @@ -342,6 +344,32 @@ Things to note: documentation, sometimes it might be useful to search a path deeper. - The `*.md` extension is not used when a document is linked to GitLab's built-in help page, that's why we omit it in `git grep`. +- Use the checklist on the documentation MR description template. + +### Redirections for pages with Disqus comments + +If the documentation page being relocated already has any Disqus comments, +we need to preserve the Disqus thread. + +Disqus uses an identifier per page, and for docs.gitlab.com, the page identifier +is configured to be the page URL. Therefore, when we change the document location, +we need to preserve the old URL as the same Disqus identifier. + +To do that, add to the frontmatter the variable `redirect_from`, +using the old URL as value. For example, let's say I moved the document +available under `https://docs.gitlab.com/my-old-location/README.html` to a new location, +`https://docs.gitlab.com/my-new-location/index.html`. + +Into the **new document** frontmatter add the following: + +```yaml +--- +redirect_from: 'https://docs.gitlab.com/my-old-location/README.html' +--- +``` + +Note: it is necessary to include the file name in the `redirect_from` URL, +even if it's `index.html` or `README.html`. ## Configuration documentation for source and Omnibus installations diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 1af839a27e1..f8cee89e650 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -87,9 +87,9 @@ still having access the class's implementation with `super`. There are a few gotchas with it: -- you should always add a `raise NotImplementedError unless defined?(super)` - guard clause in the "overrider" method to ensure that if the method gets - renamed in CE, the EE override won't be silently forgotten. +- you should always [`extend ::Gitlab::Utils::Override`] and use `override` to + guard the "overrider" method to ensure that if the method gets renamed in + CE, the EE override won't be silently forgotten. - when the "overrider" would add a line in the middle of the CE implementation, you should refactor the CE method and split it in smaller methods. Or create a "hook" method that is empty in CE, @@ -134,6 +134,9 @@ There are a few gotchas with it: guards: ``` ruby module EE::Base + extend ::Gitlab::Utils::Override + + override :do_something def do_something # Follow the above pattern to call super and extend it end @@ -174,10 +177,11 @@ implementation: ```ruby module EE - class ApplicationController - def after_sign_out_path_for(resource) - raise NotImplementedError unless defined?(super) + module ApplicationController + extend ::Gitlab::Utils::Override + override :after_sign_out_path_for + def after_sign_out_path_for(resource) if Gitlab::Geo.secondary? Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state) else @@ -188,6 +192,8 @@ module EE end ``` +[`extend ::Gitlab::Utils::Override`]: utilities.md#override + #### Use self-descriptive wrapper methods When it's not possible/logical to modify the implementation of a @@ -208,8 +214,8 @@ end In EE, the implementation `ee/app/models/ee/users.rb` would be: ```ruby +override :full_private_access? def full_private_access? - raise NotImplementedError unless defined?(super) super || auditor? end ``` diff --git a/doc/development/utilities.md b/doc/development/utilities.md index 951c3ef85ce..8f9aff1a35f 100644 --- a/doc/development/utilities.md +++ b/doc/development/utilities.md @@ -45,6 +45,51 @@ We developed a number of utilities to ease development. [:hello, "world", :this, :crushes, "an entire", "hash"] ``` +## [`Override`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/utils/override.rb) + +* This utility could help us check if a particular method would override + another method or not. It has the same idea of Java's `@Override` annotation + or Scala's `override` keyword. However we only do this check when + `ENV['STATIC_VERIFICATION']` is set to avoid production runtime overhead. + This is useful to check: + + * If we have typos in overriding methods. + * If we renamed the overridden methods, making original overriding methods + overrides nothing. + + Here's a simple example: + + ``` ruby + class Base + def execute + end + end + + class Derived < Base + extend ::Gitlab::Utils::Override + + override :execute # Override check happens here + def execute + end + end + ``` + + This also works on modules: + + ``` ruby + module Extension + extend ::Gitlab::Utils::Override + + override :execute # Modules do not check this immediately + def execute + end + end + + class Derived < Base + prepend Extension # Override check happens here, not in the module + end + ``` + ## [`StrongMemoize`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/utils/strong_memoize.rb) * Memoize the value even if it is `nil` or `false`. diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 133ac0234cf..2a1d744668b 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -25,6 +25,26 @@ them to review it for you. We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything is documented. +Whenever you submit a merge request for the documentation, use the documentation MR description template. + +### Documentation directory structure + +The documentation is structured based on the GitLab UI structure itself, +separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user), +[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development). + +To learn where to place a new document, check the [documentation style guide](doc_styleguide.md#location-and-naming-of-documents). + +In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation, +all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent. + +The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have +been deprecated and the majority their docs have been moved to their correct location +in small iterations. Please don't create new docs in these folders. + +To move a document from its location to another directory, read the section +[changing document location](doc_styleguide.md#changing-document-location) of the doc style guide. + ### Feature overview and use cases Every major feature (regardless if present in GitLab Community or Enterprise editions) diff --git a/doc/install/README.md b/doc/install/README.md index 43197351db3..87f6969b415 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -25,15 +25,19 @@ the hardware requirements. ## Install GitLab on cloud providers -- [Installing in Kubernetes](kubernetes/index.md) - Install GitLab into a Kubernetes +- [Installing in Kubernetes](kubernetes/index.md): Install GitLab into a Kubernetes Cluster using our official Helm Chart Repository. - [Install GitLab on OpenShift](openshift_and_gitlab/index.md) - [Install GitLab on DC/OS](https://mesosphere.com/blog/gitlab-dcos/) via [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/) - [Install GitLab on Azure](azure/index.md) - [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md) +- [Install GitLab on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on +the full process of installing GitLab on Google Container Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production. - [Install on AWS](https://about.gitlab.com/aws/) - _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) - Quickly test any version of GitLab on DigitalOcean using Docker Machine. +- [Getting started with GitLab and DigitalOcean](ttps://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): requirements, installation process, updates. +- [Demo: Cloud Native Development with GitLab](https://about.gitlab.com/2017/04/18/cloud-native-demo/): video demonstration on how to install GitLab on Kubernetes, build a project, create Review Apps, store Docker images in Container Registry, deploy to production on Kubernetes, and monitor with Prometheus. ## Database diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md index 8fba44aea02..448cbe1077d 100644 --- a/doc/install/openshift_and_gitlab/index.md +++ b/doc/install/openshift_and_gitlab/index.md @@ -15,6 +15,8 @@ In this tutorial, we will see how to deploy GitLab in OpenShift using GitLab's official Docker image while getting familiar with the web interface and CLI tools that will help us achieve our goal. +For a video demonstration on installing GitLab on Openshift, check the article [In 13 minutes from Kubernetes to a complete application development tool](https://about.gitlab.com/2016/11/14/idea-to-production/). + --- ## Prerequisites diff --git a/doc/install/requirements.md b/doc/install/requirements.md index baecf9455b0..4324b4ca0b8 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -7,6 +7,7 @@ - Ubuntu - Debian - CentOS +- openSUSE - Red Hat Enterprise Linux (please use the CentOS packages and instructions) - Scientific Linux (please use the CentOS packages and instructions) - Oracle Linux (please use the CentOS packages and instructions) @@ -121,7 +122,7 @@ Existing users using GitLab with MySQL/MariaDB are advised to ### PostgreSQL Requirements -As of GitLab 10.0, PostgreSQL 9.6 or newer (but less than 10) is required, and earlier versions are +As of GitLab 10.0, PostgreSQL 9.6 or newer is required, and earlier versions are not supported. We highly recommend users to use PostgreSQL 9.6 as this is the PostgreSQL version used for development and testing. diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 8c8501bcc23..9ba05c7815b 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -465,6 +465,135 @@ X-Gitlab-Event: System Hook "total_commits_count": 0 } ``` + +### Merge request events + +Triggered when a new merge request is created, an existing merge request was +updated/merged/closed or a commit is added in the source branch. + +**Request header**: + +``` +X-Gitlab-Event: System Hook +``` + +```json +{ + "object_kind": "merge_request", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon" + }, + "project": { + "name": "Example", + "description": "", + "web_url": "http://example.com/jsmith/example", + "avatar_url": null, + "git_ssh_url": "git@example.com:jsmith/example.git", + "git_http_url": "http://example.com/jsmith/example.git", + "namespace": "Jsmith", + "visibility_level": 0, + "path_with_namespace": "jsmith/example", + "default_branch": "master", + "ci_config_path": "", + "homepage": "http://example.com/jsmith/example", + "url": "git@example.com:jsmith/example.git", + "ssh_url": "git@example.com:jsmith/example.git", + "http_url": "http://example.com/jsmith/example.git" + }, + "object_attributes": { + "id": 90, + "target_branch": "master", + "source_branch": "ms-viewport", + "source_project_id": 14, + "author_id": 51, + "assignee_id": 6, + "title": "MS-Viewport", + "created_at": "2017-09-20T08:31:45.944Z", + "updated_at": "2017-09-28T12:23:42.365Z", + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 14, + "iid": 1, + "description": "", + "updated_by_id": 1, + "merge_error": null, + "merge_params": { + "force_remove_source_branch": "0" + }, + "merge_when_pipeline_succeeds": false, + "merge_user_id": null, + "merge_commit_sha": null, + "deleted_at": null, + "in_progress_merge_commit_sha": null, + "lock_version": 5, + "time_estimate": 0, + "last_edited_at": "2017-09-27T12:43:37.558Z", + "last_edited_by_id": 1, + "head_pipeline_id": 61, + "ref_fetched": true, + "merge_jid": null, + "source": { + "name": "Awesome Project", + "description": "", + "web_url": "http://example.com/awesome_space/awesome_project", + "avatar_url": null, + "git_ssh_url": "git@example.com:awesome_space/awesome_project.git", + "git_http_url": "http://example.com/awesome_space/awesome_project.git", + "namespace": "root", + "visibility_level": 0, + "path_with_namespace": "awesome_space/awesome_project", + "default_branch": "master", + "ci_config_path": "", + "homepage": "http://example.com/awesome_space/awesome_project", + "url": "http://example.com/awesome_space/awesome_project.git", + "ssh_url": "git@example.com:awesome_space/awesome_project.git", + "http_url": "http://example.com/awesome_space/awesome_project.git" + }, + "target": { + "name": "Awesome Project", + "description": "Aut reprehenderit ut est.", + "web_url": "http://example.com/awesome_space/awesome_project", + "avatar_url": null, + "git_ssh_url": "git@example.com:awesome_space/awesome_project.git", + "git_http_url": "http://example.com/awesome_space/awesome_project.git", + "namespace": "Awesome Space", + "visibility_level": 0, + "path_with_namespace": "awesome_space/awesome_project", + "default_branch": "master", + "ci_config_path": "", + "homepage": "http://example.com/awesome_space/awesome_project", + "url": "http://example.com/awesome_space/awesome_project.git", + "ssh_url": "git@example.com:awesome_space/awesome_project.git", + "http_url": "http://example.com/awesome_space/awesome_project.git" + }, + "last_commit": { + "id": "ba3e0d8ff79c80d5b0bbb4f3e2e343e0aaa662b7", + "message": "fixed readme", + "timestamp": "2017-09-26T16:12:57Z", + "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + } + }, + "work_in_progress": false, + "total_time_spent": 0, + "human_total_time_spent": null, + "human_time_estimate": null + }, + "labels": null, + "repository": { + "name": "git-gpg-test", + "url": "git@example.com:awesome_space/awesome_project.git", + "description": "", + "homepage": "http://example.com/awesome_space/awesome_project" + } +} +``` + ## Repository Update events Triggered only once when you push to the repository (including tags). diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index e23c73f46fb..6ad314647ee 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -20,6 +20,8 @@ project in an easy and automatic way: 1. [Auto Test](#auto-test) 1. [Auto Code Quality](#auto-code-quality) 1. [Auto SAST (Static Application Security Testing)](#auto-sast) +1. [Auto SAST for Docker images](#auto-sast-for-docker-images) +1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) 1. [Auto Browser Performance Testing](#auto-browser-performance-testing) 1. [Auto Review Apps](#auto-review-apps) 1. [Auto Deploy](#auto-deploy) @@ -37,6 +39,8 @@ knowledge of the following: Auto DevOps provides great defaults for all the stages; you can, however, [customize](#customizing) almost everything to your needs. +For an overview on the creation of Auto DevOps, read the blog post [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/). + ## Prerequisites TIP: **Tip:** @@ -193,8 +197,10 @@ Auto Code Quality uses the open source [`codeclimate` image](https://hub.docker.com/r/codeclimate/codeclimate/) to run static analysis and other code checks on the current code. The report is created, and is uploaded as an artifact which you can later download and check -out. In GitLab Enterprise Edition Starter, differences between the source and -target branches are +out. + +In GitLab Enterprise Edition Starter, differences between the source and +target branches are also [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). ### Auto SAST @@ -207,7 +213,34 @@ analysis on the current code and checks for potential security issues. Once the report is created, it's uploaded as an artifact which you can later download and check out. -Any security warnings are also [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html). +In GitLab Enterprise Edition Ultimate, any security warnings are also +[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html). + +### Auto SAST for Docker images + +> Introduced in GitLab 10.4. + +Vulnerability Static Analysis for containers uses +[Clair](https://github.com/coreos/clair) to run static analysis on a +Docker image and checks for potential security issues. Once the report is +created, it's uploaded as an artifact which you can later download and +check out. + +In GitLab Enterprise Edition Ultimate, any security warnings are also +[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast_docker.html). + +### Auto DAST + +> Introduced in [GitLab Enterprise Edition Ultimate][ee] 10.4. + +Dynamic Application Security Testing (DAST) uses the +popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy) +to perform an analysis on the current code and checks for potential security +issues. Once the report is created, it's uploaded as an artifact which you can +later download and check out. + +In GitLab Enterprise Edition Ultimate, any security warnings are also +[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html). ### Auto Browser Performance Testing diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md index f69e2e49f0c..2ca2bf743fb 100644 --- a/doc/topics/git/index.md +++ b/doc/topics/git/index.md @@ -44,7 +44,7 @@ We've gathered some resources to help you to get the best from Git with GitLab. ## Troubleshooting Git -- [Numerous _undo_ possibilities in Git](../../articles/numerous_undo_possibilities_in_git/index.md) +- [Numerous _undo_ possibilities in Git](numerous_undo_possibilities_in_git/index.md) - Learn a few [Git troubleshooting](troubleshooting_git.md) techniques to help you out. ## Branching strategies diff --git a/doc/articles/numerous_undo_possibilities_in_git/img/branching.png b/doc/topics/git/numerous_undo_possibilities_in_git/img/branching.png Binary files differindex 9a80c211c99..9a80c211c99 100644 --- a/doc/articles/numerous_undo_possibilities_in_git/img/branching.png +++ b/doc/topics/git/numerous_undo_possibilities_in_git/img/branching.png diff --git a/doc/articles/numerous_undo_possibilities_in_git/img/rebase_reset.png b/doc/topics/git/numerous_undo_possibilities_in_git/img/rebase_reset.png Binary files differindex ac7ea9ecddc..ac7ea9ecddc 100644 --- a/doc/articles/numerous_undo_possibilities_in_git/img/rebase_reset.png +++ b/doc/topics/git/numerous_undo_possibilities_in_git/img/rebase_reset.png diff --git a/doc/articles/numerous_undo_possibilities_in_git/img/revert.png b/doc/topics/git/numerous_undo_possibilities_in_git/img/revert.png Binary files differindex 13b3a35ca45..13b3a35ca45 100644 --- a/doc/articles/numerous_undo_possibilities_in_git/img/revert.png +++ b/doc/topics/git/numerous_undo_possibilities_in_git/img/revert.png diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md new file mode 100644 index 00000000000..6a2f7b30dd3 --- /dev/null +++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md @@ -0,0 +1,497 @@ +# Numerous undo possibilities in Git + +> **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles):** tutorial || +> **Level:** intermediary || +> **Author:** [Crt Mori](https://gitlab.com/Letme) || +> **Publication date:** 2017-08-17 + +## Introduction + +In this tutorial, we will show you different ways of undoing your work in Git, for which +we will assume you have a basic working knowledge of. Check GitLab's +[Git documentation](../index.md#git-documentation) for reference. +Also, we will only provide some general info of the commands, which is enough +to get you started for the easy cases/examples, but for anything more advanced please refer to the [Git book](https://git-scm.com/book/en/v2). + +We will explain a few different techniques to undo your changes based on the stage +of the change in your current development. Also, keep in mind that [nothing in +Git is really deleted.][git-autoclean-ref] +This means that until Git automatically cleans detached commits (which cannot be +accessed by branch or tag) it will be possible to view them with `git reflog` command +and access them with direct commit-id. Read more about _[redoing the undo](#redoing-the-undo)_ on the section below. + +This guide is organized depending on the [stage of development][git-basics] +where you want to undo your changes from and if they were shared with other developers +or not. Because Git is tracking changes a created or edited file is in the unstaged state +(if created it is untracked by Git). After you add it to a repository (`git add`) you put +a file into the **staged** state, which is then committed (`git commit`) to your +local repository. After that, file can be shared with other developers (`git push`). +Here's what we'll cover in this tutorial: + + - [Undo local changes](#undo-local-changes) which were not pushed to remote repository + + - Before you commit, in both unstaged and staged state + - After you committed + + - Undo changes after they are pushed to remote repository + + - [Without history modification](#undo-remote-changes-without-changing-history) (preferred way) + - [With history modification](#undo-remote-changes-with-modifying-history) (requires + coordination with team and force pushes). + + - [Usecases when modifying history is generally acceptable](#where-modifying-history-is-generally-acceptable) + - [How to modify history](#how-modifying-history-is-done) + - [How to remove sensitive information from repository](#deleting-sensitive-information-from-commits) + + +### Branching strategy + +[Git][git-official] is a de-centralized version control system, which means that beside regular +versioning of the whole repository, it has possibilities to exchange changes +with other repositories. To avoid chaos with +[multiple sources of truth][git-distributed], various +development workflows have to be followed, and it depends on your internal +workflow how certain changes or commits can be undone or changed. +[GitLab Flow][gitlab-flow] provides a good +balance between developers clashing with each other while +developing the same feature and cooperating seamlessly, but it does not enable +joined development of the same feature by multiple developers by default. +When multiple developers develop the same feature on the same branch, clashing +with every synchronization is unavoidable, but a proper or chosen Git Workflow will +prevent that anything is lost or out of sync when feature is complete. You can also +read through this blog post on [Git Tips & Tricks][gitlab-git-tips-n-tricks] +to learn how to easily **do** things in Git. + + +## Undo local changes + +Until you push your changes to any remote repository, they will only affect you. +That broadens your options on how to handle undoing them. Still, local changes +can be on various stages and each stage has a different approach on how to tackle them. + + +### Unstaged local changes (before you commit) + +When a change is made, but it is not added to the staged tree, Git itself +proposes a solution to discard changes to certain file. + +Suppose you edited a file to change the content using your favorite editor: + +```shell +vim <file> +``` + +Since you did not `git add <file>` to staging, it should be under unstaged files (or +untracked if file was created). You can confirm that with: + +```shell +$ git status +On branch master +Your branch is up-to-date with 'origin/master'. +Changes not staged for commit: + (use "git add <file>..." to update what will be committed) + (use "git checkout -- <file>..." to discard changes in working directory) + + modified: <file> +no changes added to commit (use "git add" and/or "git commit -a") +``` + +At this point there are 3 options to undo the local changes you have: + + - Discard all local changes, but save them for possible re-use [later](#quickly-save-local-changes) + + ```shell + git stash + ``` + + - Discarding local changes (permanently) to a file + + ```shell + git checkout -- <file> + ``` + + - Discard all local changes to all files permanently + + ```shell + git reset --hard + ``` + + +Before executing `git reset --hard`, keep in mind that there is also a way to +just temporary store the changes without committing them using `git stash`. +This command resets the changes to all files, but it also saves them in case +you would like to apply them at some later time. You can read more about it in +[section below](#quickly-save-local-changes). + +### Quickly save local changes + +You are working on a feature when a boss drops by with an urgent task. Since your +feature is not complete, but you need to swap to another branch, you can use +`git stash` to save what you had done, swap to another branch, commit, push, +test, then get back to previous feature branch, do `git stash pop` and continue +where you left. + +The example above shows that discarding all changes is not always a preferred option, +but Git provides a way to save them for later, while resetting the repository to state without +them. This is achieved by Git stashing command `git stash`, which in fact saves your +current work and runs `git reset --hard`, but it also has various +additional options like: + + - `git stash save`, which enables including temporary commit message, which will help you identify changes, among with other options + - `git stash list`, which lists all previously stashed commits (yes, there can be more) that were not `pop`ed + - `git stash pop`, which redoes previously stashed changes and removes them from stashed list + - `git stash apply`, which redoes previously stashed changes, but keeps them on stashed list + +### Staged local changes (before you commit) + +Let's say you have added some files to staging, but you want to remove them from the +current commit, yet you want to retain those changes - just move them outside +of the staging tree. You also have an option to discard all changes with +`git reset --hard` or think about `git stash` [as described earlier.](#quickly-save-local-changes) + +Lets start the example by editing a file, with your favorite editor, to change the +content and add it to staging + +``` +vim <file> +git add <file> +``` + +The file is now added to staging as confirmed by `git status` command: + +```shell +$ git status +On branch master +Your branch is up-to-date with 'origin/master'. +Changes to be committed: + (use "git reset HEAD <file>..." to unstage) + + new file: <file> +``` + +Now you have 4 options to undo your changes: + + - Unstage the file to current commit (HEAD) + + ```shell + git reset HEAD <file> + ``` + + - Unstage everything - retain changes + + ```shell + git reset + ``` + + - Discard all local changes, but save them for [later](#quickly-save-local-changes) + + ```shell + git stash + ``` + + - Discard everything permanently + + ```shell + git reset --hard + ``` + +## Committed local changes + +Once you commit, your changes are recorded by the version control system. +Because you haven't pushed to your remote repository yet, your changes are +still not public (or shared with other developers). At this point, undoing +things is a lot easier, we have quite some workaround options. Once you push +your code, you'll have less options to troubleshoot your work. + +### Without modifying history + +Through the development process some of the previously committed changes do not +fit anymore in the end solution, or are source of the bugs. Once you find the +commit which triggered bug, or once you have a faulty commit, you can simply +revert it with `git revert commit-id`. This command inverts (swaps) the additions and +deletions in that commit, so that it does not modify history. Retaining history +can be helpful in future to notice that some changes have been tried +unsuccessfully in the past. + +In our example we will assume there are commits `A`,`B`,`C`,`D`,`E` committed in this order: `A-B-C-D-E`, +and `B` is the commit you want to undo. There are many different ways to identify commit +`B` as bad, one of them is to pass a range to `git bisect` command. The provided range includes +last known good commit (we assume `A`) and first known bad commit (where bug was detected - we will assume `E`). + +```shell +git bisect A..E +``` + +Bisect will provide us with commit-id of the middle commit to test, and then guide us +through simple bisection process. You can read more about it [in official Git Tools][git-debug] +In our example we will end up with commit `B`, that introduced bug/error. We have +4 options on how to remove it (or part of it) from our repository. + +- Undo (swap additions and deletions) changes introduced by commit `B`. + + ```shell + git revert commit-B-id + ``` + +- Undo changes on a single file or directory from commit `B`, but retain them in the staged state + + ```shell + git checkout commit-B-id <file> + ``` + +- Undo changes on a single file or directory from commit `B`, but retain them in the unstaged state + + ```shell + git reset commit-B-id <file> + ``` + + - There is one command we also must not forget: **creating a new branch** + from the point where changes are not applicable or where the development has hit a + dead end. For example you have done commits `A-B-C-D` on your feature-branch + and then you figure `C` and `D` are wrong. At this point you either reset to `B` + and do commit `F` (which will cause problems with pushing and if forced pushed also with other developers) + since branch now looks `A-B-F`, which clashes with what other developers have locally (you will + [change history](#with-history-modification)), or you simply checkout commit `B` create + a new branch and do commit `F`. In the last case, everyone else can still do their work while you + have your new way to get it right and merge it back in later. Alternatively, with GitLab, + you can [cherry-pick](../../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit) + that commit into a new merge request. + + ![Create a new branch to avoid clashing](img/branching.png) + + ```shell + git checkout commit-B-id + git checkout -b new-path-of-feature + # Create <commit F> + git commit -a + ``` + +### With history modification + +There is one command for history modification and that is `git rebase`. Command +provides interactive mode (`-i` flag) which enables you to: + + - **reword** commit messages (there is also `git commit --amend` for editing + last commit message) + - **edit** the commit content (changes introduced by commit) and message + - **squash** multiple commits into a single one, and have a custom or aggregated + commit message + - **drop** commits - simply delete them + - and few more options + +Let us check few examples. Again there are commits `A-B-C-D` where you want to +delete commit `B`. + +- Rebase the range from current commit D to A: + + ```shell + git rebase -i A + ``` + +- Command opens your favorite editor where you write `drop` in front of commit + `B`, but you leave default `pick` with all other commits. Save and exit the + editor to perform a rebase. Remember: if you want to cancel delete whole + file content before saving and exiting the editor + +In case you want to modify something introduced in commit `B`. + +- Rebase the range from current commit D to A: + + ```shell + git rebase -i A + ``` + +- Command opens your favorite text editor where you write `edit` in front of commit + `B`, but leave default `pick` with all other commits. Save and exit the editor to + perform a rebase + +- Now do your edits and commit changes: + + ```shell + git commit -a + ``` + +You can find some more examples in [below section where we explain how to modify +history](#how-modifying-history-is-done) + + +### Redoing the Undo + +Sometimes you realize that the changes you undid were useful and you want them +back. Well because of first paragraph you are in luck. Command `git reflog` +enables you to *recall* detached local commits by referencing or applying them +via commit-id. Although, do not expect to see really old commits in reflog, because +Git regularly [cleans the commits which are *unreachable* by branches or tags][git-autoclean-ref]. + +To view repository history and to track older commits you can use below command: + +```shell +$ git reflog show + +# Example output: +b673187 HEAD@{4}: merge 6e43d5987921bde189640cc1e37661f7f75c9c0b: Merge made by the 'recursive' strategy. +eb37e74 HEAD@{5}: rebase -i (finish): returning to refs/heads/master +eb37e74 HEAD@{6}: rebase -i (pick): Commit C +97436c6 HEAD@{7}: rebase -i (start): checkout 97436c6eec6396c63856c19b6a96372705b08b1b +... +88f1867 HEAD@{12}: commit: Commit D +97436c6 HEAD@{13}: checkout: moving from 97436c6eec6396c63856c19b6a96372705b08b1b to test +97436c6 HEAD@{14}: checkout: moving from master to 97436c6 +05cc326 HEAD@{15}: commit: Commit C +6e43d59 HEAD@{16}: commit: Commit B +``` + +Output of command shows repository history. In first column there is commit-id, +in following column, number next to `HEAD` indicates how many commits ago something +was made, after that indicator of action that was made (commit, rebase, merge, ...) +and then on end description of that action. + +## Undo remote changes without changing history + +This topic is roughly same as modifying committed local changes without modifying +history. **It should be the preferred way of undoing changes on any remote repository +or public branch.** Keep in mind that branching is the best solution when you want +to retain the history of faulty development, yet start anew from certain point. Branching +enables you to include the existing changes in new development (by merging) and +it also provides a clear timeline and development structure. + +![Use revert to keep branch flowing](img/revert.png) + +If you want to revert changes introduced in certain `commit-id` you can simply +revert that `commit-id` (swap additions and deletions) in newly created commit: +You can do this with + +```shell +git revert commit-id +``` + +or creating a new branch: + +```shell +git checkout commit-id +git checkout -b new-path-of-feature +``` + +## Undo remote changes with modifying history + +This is useful when you want to *hide* certain things - like secret keys, +passwords, SSH keys, etc. It is and should not be used to hide mistakes, as +it will make it harder to debug in case there are some other bugs. The main +reason for this is that you loose the real development progress. **Also keep in +mind that, even with modified history, commits are just detached and can still be +accessed through commit-id** - at least until all repositories perform +the cleanup of detached commits (happens automatically). + +![Modifying history causes problems on remote branch](img/rebase_reset.png) + +### Where modifying history is generally acceptable + +Modified history breaks the development chain of other developers, as changed +history does not have matching commits'ids. For that reason it should not +be used on any public branch or on branch that *might* be used by other +developers. When contributing to big open source repositories (e.g. [GitLab CE][gitlab-ce]), +it is acceptable to *squash* commits into a single one, to present +a nicer history of your contribution. +Keep in mind that this also removes the comments attached to certain commits +in merge requests, so if you need to retain traceability in GitLab, then +modifying history is not acceptable. +A feature-branch of a merge request is a public branch and might be used by +other developers, but project process and rules might allow or require +you to use `git rebase` (command that changes history) to reduce number of +displayed commits on target branch after reviews are done (for example +GitLab). There is a `git merge --squash` command which does exactly that +(squashes commits on feature-branch to a single commit on target branch +at merge). + +>**Note:** +Never modify the commit history of `master` or shared branch + +### How modifying history is done + +After you know what you want to modify (how far in history or how which range of +old commits), use `git rebase -i commit-id`. This command will then display all the commits from +current version to chosen commit-id and allow modification, squashing, deletion +of that commits. + +```shell +$ git rebase -i commit1-id..commit3-id +pick <commit1-id> <commit1-commit-message> +pick <commit2-id> <commit2-commit-message> +pick <commit3-id> <commit3-commit-message> + +# Rebase commit1-id..commit3-id onto <commit4-id> (3 command(s)) +# +# Commands: +# p, pick = use commit +# r, reword = use commit, but edit the commit message +# e, edit = use commit, but stop for amending +# s, squash = use commit, but meld into previous commit +# f, fixup = like "squash", but discard this commit's log message +# x, exec = run command (the rest of the line) using shell +# d, drop = remove commit +# +# These lines can be re-ordered; they are executed from top to bottom. +# +# If you remove a line here THAT COMMIT WILL BE LOST. +# +# However, if you remove everything, the rebase will be aborted. +# +# Note that empty commits are commented out +``` + +>**Note:** +It is important to notice that comment from the output clearly states that, if +you decide to abort, then do not just close your editor (as that will in-fact +modify history), but remove all uncommented lines and save. + +That is one of the reasons why `git rebase` should be used carefully on +shared and remote branches. But don't worry, there will be nothing broken until +you push back to the remote repository (so you can freely explore the +different outcomes locally). + +```shell +# Modify history from commit-id to HEAD (current commit) +git rebase -i commit-id +``` + +### Deleting sensitive information from commits + +Git also enables you to delete sensitive information from your past commits and +it does modify history in the progress. That is why we have included it in this +section and not as a standalone topic. To do so, you should run the +`git filter-branch`, which enables you to rewrite history with +[certain filters][git-filters-manual]. +This command uses rebase to modify history and if you want to remove certain +file from history altogether use: + +```shell +git filter-branch --tree-filter 'rm filename' HEAD +``` + +Since `git filter-branch` command might be slow on big repositories, there are +tools that can use some of Git specifics to enable faster execution of common +tasks (which is exactly what removing sensitive information file is about). +An alternative is [BFG Repo-cleaner][bfg-repo-cleaner]. Keep in mind that these +tools are faster because they do not provide a same fully feature set as `git filter-branch` +does, but focus on specific usecases. + +## Conclusion + +There are various options of undoing your work with any version control system, but +because of de-centralized nature of Git, these options are multiplied (or limited) +depending on the stage of your process. Git also enables rewriting history, but that +should be avoided as it might cause problems when multiple developers are +contributing to the same codebase. + +<!-- Identifiers, in alphabetical order --> + +[bfg-repo-cleaner]: https://rtyley.github.io/bfg-repo-cleaner/ +[git-autoclean-ref]: https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery +[git-basics]: https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository +[git-debug]: https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git +[git-distributed]: https://git-scm.com/about/distributed +[git-filters-manual]: https://git-scm.com/docs/git-filter-branch#_options +[git-official]: https://git-scm.com/ +[gitlab-ce]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria +[gitlab-flow]: https://about.gitlab.com/2014/09/29/gitlab-flow/ +[gitlab-git-tips-n-tricks]: https://about.gitlab.com/2016/12/08/git-tips-and-tricks/ diff --git a/doc/user/admin_area/monitoring/img/convdev_index.png b/doc/user/admin_area/monitoring/img/convdev_index.png Binary files differindex ffe18d76c96..1bf1d6a83c9 100644 --- a/doc/user/admin_area/monitoring/img/convdev_index.png +++ b/doc/user/admin_area/monitoring/img/convdev_index.png diff --git a/doc/user/index.md b/doc/user/index.md index f239a15d441..01db8becc43 100644 --- a/doc/user/index.md +++ b/doc/user/index.md @@ -23,9 +23,20 @@ all the way through, from within the same platform. Please check this page for an overview on [GitLab's features](https://about.gitlab.com/features/). +### Concepts + +For an overview on concepts involved when developing code on GitLab, +read the articles on: + +- [Mastering Code Review With GitLab](https://about.gitlab.com/2017/03/17/demo-mastering-code-review-with-gitlab/). +- [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario). +- [Tutorial: It's all connected in GitLab](https://about.gitlab.com/2016/03/08/gitlab-tutorial-its-all-connected/): an overview on code collaboration with GitLab. +- [Trends in Version Control Land: Microservices](https://about.gitlab.com/2016/08/16/trends-in-version-control-land-microservices/). +- [Trends in Version Control Land: Innersourcing](https://about.gitlab.com/2016/07/07/trends-version-control-innersourcing/). + ## Use cases -GitLab is a git-based platforms that integrates a great number of essential tools for software development and deployment, and project management: +GitLab is a Git-based platform that integrates a great number of essential tools for software development and deployment, and project management: - Code hosting in repositories with version control - Track proposals for new implementations, bug reports, and feedback with a @@ -58,12 +69,6 @@ and [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board. You can also [integrate](project/integrations/project_services.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, JIRA, and a lot more. -### Articles - -For a complete workflow use case please check [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario). - -For more use cases please check our [Technical Articles](../articles/index.md). - ## Projects In GitLab, you can create [projects](project/index.md) for numerous reasons, such as, host diff --git a/doc/workflow/lfs/img/lfs-icon.png b/doc/workflow/lfs/img/lfs-icon.png Binary files differnew file mode 100644 index 00000000000..eef9a14187a --- /dev/null +++ b/doc/workflow/lfs/img/lfs-icon.png diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index f7b87dee8e1..ce7895780c3 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -4,6 +4,11 @@ Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. The general recommendation is to not have Git repositories larger than 1GB to preserve performance. +![Git LFS tracking status](img/lfs-icon.png) + +An LFS icon is shown on files tracked by Git LFS to denote if a file is stored +as a blob or as an LFS pointer. + ## How it works Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 3e2e7d0f7b6..0203757e0e1 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -65,7 +65,7 @@ Below is the table of events users can be notified of: | Group access level changed | User | Sent when user group access level is changed | | Project moved | Project members [1] | [1] not disabled | -### Issue / Merge Request events +### Issue / Merge request events In all of the below cases, the notification will be sent to: - Participants: diff --git a/lib/api/circuit_breakers.rb b/lib/api/circuit_breakers.rb index 598c76f6168..c13154dc0ec 100644 --- a/lib/api/circuit_breakers.rb +++ b/lib/api/circuit_breakers.rb @@ -17,11 +17,11 @@ module API end def storage_health - @failing_storage_health ||= Gitlab::Git::Storage::Health.for_all_storages + @storage_health ||= Gitlab::Git::Storage::Health.for_all_storages end end - desc 'Get all failing git storages' do + desc 'Get all git storages' do detail 'This feature was introduced in GitLab 9.5' success Entities::RepositoryStorageHealth end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 281269b1190..b0b7b50998f 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -4,6 +4,16 @@ module API before { authenticate! } + helpers do + def add_deploy_keys_project(project, attrs = {}) + project.deploy_keys_projects.create(attrs) + end + + def find_by_deploy_key(project, key_id) + project.deploy_keys_projects.find_by!(deploy_key: key_id) + end + end + desc 'Return all deploy keys' params do use :pagination @@ -21,28 +31,31 @@ module API before { authorize_admin_project } desc "Get a specific project's deploy keys" do - success Entities::SSHKey + success Entities::DeployKeysProject end params do use :pagination end get ":id/deploy_keys" do - present paginate(user_project.deploy_keys), with: Entities::SSHKey + keys = user_project.deploy_keys_projects.preload(:deploy_key) + + present paginate(keys), with: Entities::DeployKeysProject end desc 'Get single deploy key' do - success Entities::SSHKey + success Entities::DeployKeysProject end params do requires :key_id, type: Integer, desc: 'The ID of the deploy key' end get ":id/deploy_keys/:key_id" do - key = user_project.deploy_keys.find params[:key_id] - present key, with: Entities::SSHKey + key = find_by_deploy_key(user_project, params[:key_id]) + + present key, with: Entities::DeployKeysProject end desc 'Add new deploy key to currently authenticated user' do - success Entities::SSHKey + success Entities::DeployKeysProject end params do requires :key, type: String, desc: 'The new deploy key' @@ -53,24 +66,31 @@ module API params[:key].strip! # Check for an existing key joined to this project - key = user_project.deploy_keys.find_by(key: params[:key]) + key = user_project.deploy_keys_projects + .joins(:deploy_key) + .find_by(keys: { key: params[:key] }) + if key - present key, with: Entities::SSHKey + present key, with: Entities::DeployKeysProject break end # Check for available deploy keys in other projects key = current_user.accessible_deploy_keys.find_by(key: params[:key]) if key - user_project.deploy_keys << key - present key, with: Entities::SSHKey + added_key = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push]) + + present added_key, with: Entities::DeployKeysProject break end # Create a new deploy key - key = DeployKey.new(declared_params(include_missing: false)) - if key.valid? && user_project.deploy_keys << key - present key, with: Entities::SSHKey + key_attributes = { can_push: !!params[:can_push], + deploy_key_attributes: declared_params.except(:can_push) } + key = add_deploy_keys_project(user_project, key_attributes) + + if key.valid? + present key, with: Entities::DeployKeysProject else render_validation_error!(key) end @@ -86,14 +106,21 @@ module API at_least_one_of :title, :can_push end put ":id/deploy_keys/:key_id" do - key = DeployKey.find(params.delete(:key_id)) + deploy_keys_project = find_by_deploy_key(user_project, params[:key_id]) - authorize!(:update_deploy_key, key) + authorize!(:update_deploy_key, deploy_keys_project.deploy_key) - if key.update_attributes(declared_params(include_missing: false)) - present key, with: Entities::SSHKey + can_push = params[:can_push].nil? ? deploy_keys_project.can_push : params[:can_push] + title = params[:title] || deploy_keys_project.deploy_key.title + + result = deploy_keys_project.update_attributes(can_push: can_push, + deploy_key_attributes: { id: params[:key_id], + title: title }) + + if result + present deploy_keys_project, with: Entities::DeployKeysProject else - render_validation_error!(key) + render_validation_error!(deploy_keys_project) end end @@ -122,7 +149,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the deploy key' end delete ":id/deploy_keys/:key_id" do - key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id]) + key = user_project.deploy_keys.find(params[:key_id]) not_found!('Deploy Key') unless key destroy_conditionally!(key) diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index 1efee9a1324..184fae0eb76 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -15,11 +15,13 @@ module API end params do use :pagination + optional :order_by, type: String, values: %w[id iid created_at ref], default: 'id', desc: 'Return deployments ordered by `id` or `iid` or `created_at` or `ref`' + optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' end get ':id/deployments' do authorize! :read_deployment, user_project - present paginate(user_project.deployments), with: Entities::Deployment + present paginate(user_project.deployments.order(params[:order_by] => params[:sort])), with: Entities::Deployment end desc 'Gets a specific deployment' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c4ef2c74658..3f4b62dc1b2 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -65,12 +65,12 @@ module API end class Hook < Grape::Entity - expose :id, :url, :created_at, :push_events, :tag_push_events, :repository_update_events + expose :id, :url, :created_at, :push_events, :tag_push_events, :merge_requests_events, :repository_update_events expose :enable_ssl_verification end class ProjectHook < Hook - expose :project_id, :issues_events, :merge_requests_events + expose :project_id, :issues_events expose :note_events, :pipeline_events, :wiki_page_events expose :job_events end @@ -554,13 +554,18 @@ module API end class SSHKey < Grape::Entity - expose :id, :title, :key, :created_at, :can_push + expose :id, :title, :key, :created_at end class SSHKeyWithUser < SSHKey expose :user, using: Entities::UserPublic end + class DeployKeysProject < Grape::Entity + expose :deploy_key, merge: true, using: Entities::SSHKey + expose :can_push + end + class GPGKey < Grape::Entity expose :id, :key, :created_at end @@ -714,10 +719,7 @@ module API expose :job_events # Expose serialized properties expose :properties do |service, options| - field_names = service.fields - .select { |field| options[:include_passwords] || field[:type] != 'password' } - .map { |field| field[:name] } - service.properties.slice(*field_names) + service.properties.slice(*service.api_field_names) end end diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb index 322624c6092..9993caa5249 100644 --- a/lib/api/helpers/common_helpers.rb +++ b/lib/api/helpers/common_helpers.rb @@ -3,8 +3,10 @@ module API module CommonHelpers def convert_parameters_from_legacy_format(params) params.tap do |params| - if params[:assignee_id].present? - params[:assignee_ids] = [params.delete(:assignee_id)] + assignee_id = params.delete(:assignee_id) + + if assignee_id.present? + params[:assignee_ids] = [assignee_id] end end end diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index a116ab3c9bd..9c205514b3a 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -38,6 +38,7 @@ module API builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) + builds = builds.preload(:user, :job_artifacts_archive, :runner, pipeline: :project) present paginate(builds), with: Entities::Job end diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb index 0cb209a02d0..306dc0e63d7 100644 --- a/lib/api/project_milestones.rb +++ b/lib/api/project_milestones.rb @@ -60,6 +60,15 @@ module API update_milestone_for(user_project) end + desc 'Remove a project milestone' + delete ":id/milestones/:milestone_id" do + authorize! :admin_milestone, user_project + + user_project.milestones.find(params[:milestone_id]).destroy + + status(204) + end + desc 'Get all issues for a single project milestone' do success Entities::IssueBasic end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 5bed58c2d63..39c03c40bab 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -143,7 +143,7 @@ module API get ":id/snippets/:snippet_id/user_agent_detail" do authenticated_as_admin! - snippet = Snippet.find_by!(id: params[:id]) + snippet = Snippet.find_by!(id: params[:snippet_id], project_id: params[:id]) return not_found!('UserAgentDetail') unless snippet.user_agent_detail diff --git a/lib/api/services.rb b/lib/api/services.rb index a7f44e2869c..51e33e2c686 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -785,7 +785,7 @@ module API service_params = declared_params(include_missing: false).merge(active: true) if service.update_attributes(service_params) - present service, with: Entities::ProjectService, include_passwords: current_user.admin? + present service, with: Entities::ProjectService else render_api_error!('400 Bad Request', 400) end diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 6b6a03e3300..c7a460df46a 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -26,6 +26,7 @@ module API optional :token, type: String, desc: 'The token used to validate payloads' optional :push_events, type: Boolean, desc: "Trigger hook on push events" optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" + optional :merge_requests_events, type: Boolean, desc: "Trigger hook on tag push events" optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" end post do diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb index fa0bef39602..ac76fece931 100644 --- a/lib/api/v3/builds.rb +++ b/lib/api/v3/builds.rb @@ -36,6 +36,7 @@ module API builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) + builds = builds.preload(:user, :job_artifacts_archive, :runner, pipeline: :project) present paginate(builds), with: ::API::V3::Entities::Build end diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb index b90e2061da3..47e54ca85a5 100644 --- a/lib/api/v3/deploy_keys.rb +++ b/lib/api/v3/deploy_keys.rb @@ -3,6 +3,16 @@ module API class DeployKeys < Grape::API before { authenticate! } + helpers do + def add_deploy_keys_project(project, attrs = {}) + project.deploy_keys_projects.create(attrs) + end + + def find_by_deploy_key(project, key_id) + project.deploy_keys_projects.find_by!(deploy_key: key_id) + end + end + get "deploy_keys" do authenticated_as_admin! @@ -18,25 +28,28 @@ module API %w(keys deploy_keys).each do |path| desc "Get a specific project's deploy keys" do - success ::API::Entities::SSHKey + success ::API::Entities::DeployKeysProject end get ":id/#{path}" do - present user_project.deploy_keys, with: ::API::Entities::SSHKey + keys = user_project.deploy_keys_projects.preload(:deploy_key) + + present keys, with: ::API::Entities::DeployKeysProject end desc 'Get single deploy key' do - success ::API::Entities::SSHKey + success ::API::Entities::DeployKeysProject end params do requires :key_id, type: Integer, desc: 'The ID of the deploy key' end get ":id/#{path}/:key_id" do - key = user_project.deploy_keys.find params[:key_id] - present key, with: ::API::Entities::SSHKey + key = find_by_deploy_key(user_project, params[:key_id]) + + present key, with: ::API::Entities::DeployKeysProject end desc 'Add new deploy key to currently authenticated user' do - success ::API::Entities::SSHKey + success ::API::Entities::DeployKeysProject end params do requires :key, type: String, desc: 'The new deploy key' @@ -47,24 +60,31 @@ module API params[:key].strip! # Check for an existing key joined to this project - key = user_project.deploy_keys.find_by(key: params[:key]) + key = user_project.deploy_keys_projects + .joins(:deploy_key) + .find_by(keys: { key: params[:key] }) + if key - present key, with: ::API::Entities::SSHKey + present key, with: ::API::Entities::DeployKeysProject break end # Check for available deploy keys in other projects key = current_user.accessible_deploy_keys.find_by(key: params[:key]) if key - user_project.deploy_keys << key - present key, with: ::API::Entities::SSHKey + added_key = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push]) + + present added_key, with: ::API::Entities::DeployKeysProject break end # Create a new deploy key - key = DeployKey.new(declared_params(include_missing: false)) - if key.valid? && user_project.deploy_keys << key - present key, with: ::API::Entities::SSHKey + key_attributes = { can_push: !!params[:can_push], + deploy_key_attributes: declared_params.except(:can_push) } + key = add_deploy_keys_project(user_project, key_attributes) + + if key.valid? + present key, with: ::API::Entities::DeployKeysProject else render_validation_error!(key) end diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 64758dae7d3..2ccbb9da1c5 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -257,10 +257,7 @@ module API expose :job_events, as: :build_events # Expose serialized properties expose :properties do |service, options| - field_names = service.fields - .select { |field| options[:include_passwords] || field[:type] != 'password' } - .map { |field| field[:name] } - service.properties.slice(*field_names) + service.properties.slice(*service.api_field_names) end end diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb index 44ed94d2869..20ca1021c71 100644 --- a/lib/api/v3/services.rb +++ b/lib/api/v3/services.rb @@ -622,7 +622,7 @@ module API end get ":id/services/:service_slug" do service = user_project.find_or_initialize_service(params[:service_slug].underscore) - present service, with: Entities::ProjectService, include_passwords: current_user.admin? + present service, with: Entities::ProjectService end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 05aa79dc160..f27ce4d2b2b 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -108,7 +108,10 @@ module Backup $progress.puts "Please make sure that file name ends with #{FILE_NAME_SUFFIX}" exit 1 elsif backup_file_list.many? && ENV["BACKUP"].nil? - $progress.puts 'Found more than one backup, please specify which one you want to restore:' + $progress.puts 'Found more than one backup:' + # print list of available backups + $progress.puts " " + available_timestamps.join("\n ") + $progress.puts 'Please specify which one you want to restore:' $progress.puts 'rake gitlab:backup:restore BACKUP=timestamp_of_backup' exit 1 end @@ -169,6 +172,10 @@ module Backup @backup_file_list ||= Dir.glob("*#{FILE_NAME_SUFFIX}") end + def available_timestamps + @backup_file_list.map {|item| item.gsub("#{FILE_NAME_SUFFIX}", "")} + end + def connect_to_remote_directory(connection_settings) # our settings use string keys, but Fog expects symbols connection = ::Fog::Storage.new(connection_settings.symbolize_keys) diff --git a/lib/banzai/filter/wiki_link_filter/rewriter.rb b/lib/banzai/filter/wiki_link_filter/rewriter.rb index e7a1ec8457d..072d24e5a11 100644 --- a/lib/banzai/filter/wiki_link_filter/rewriter.rb +++ b/lib/banzai/filter/wiki_link_filter/rewriter.rb @@ -9,6 +9,10 @@ module Banzai end def apply_rules + # Special case: relative URLs beginning with `/uploads/` refer to + # user-uploaded files and will be handled elsewhere. + return @uri.to_s if @uri.relative? && @uri.path.starts_with?('/uploads/') + apply_file_link_rules! apply_hierarchical_link_rules! apply_relative_link_rules! diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index eb606b57667..55658900628 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -64,10 +64,24 @@ module Gitlab include LegacyValidationHelpers def validate_each(record, attribute, value) - unless validate_string(value) + if validate_string(value) + validate_path(record, attribute, value) + else record.errors.add(attribute, 'should be a string or symbol') end end + + private + + def validate_path(record, attribute, value) + path = CGI.unescape(value.to_s) + + if path.include?('/') + record.errors.add(attribute, 'cannot contain the "/" character') + elsif path == '.' || path == '..' + record.errors.add(attribute, 'cannot be "." or ".."') + end + end end class RegexpValidator < ActiveModel::EachValidator diff --git a/lib/gitlab/ci/status/build/action.rb b/lib/gitlab/ci/status/build/action.rb index 45fd0d4aa07..6c9125647ad 100644 --- a/lib/gitlab/ci/status/build/action.rb +++ b/lib/gitlab/ci/status/build/action.rb @@ -2,6 +2,9 @@ module Gitlab module Ci module Status module Build + ## + # Extended status for playable manual actions. + # class Action < Status::Extended def label if has_action? @@ -12,7 +15,7 @@ module Gitlab end def self.matches?(build, user) - build.action? + build.playable? end end end diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 016437b2419..768617e2cae 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -239,6 +239,24 @@ module Gitlab end end end + + def extract_signature(repository, commit_id) + repository.gitaly_migrate(:extract_commit_signature) do |is_enabled| + if is_enabled + repository.gitaly_commit_client.extract_signature(commit_id) + else + rugged_extract_signature(repository, commit_id) + end + end + end + + def rugged_extract_signature(repository, commit_id) + begin + Rugged::Commit.extract_signature(repository.rugged, commit_id) + rescue Rugged::OdbError + nil + end + end end def initialize(repository, raw_commit, head = nil) @@ -436,6 +454,16 @@ module Gitlab parent_ids.size > 1 end + def tree_entry(path) + @repository.gitaly_migrate(:commit_tree_entry) do |is_migrated| + if is_migrated + gitaly_tree_entry(path) + else + rugged_tree_entry(path) + end + end + end + def to_gitaly_commit return raw_commit if raw_commit.is_a?(Gitaly::GitCommit) @@ -450,11 +478,6 @@ module Gitlab ) end - # Is this the same as Blob.find_entry_by_path ? - def rugged_tree_entry(path) - rugged_commit.tree.path(path) - end - private def init_from_hash(hash) @@ -501,6 +524,28 @@ module Gitlab SERIALIZE_KEYS end + def gitaly_tree_entry(path) + # We're only interested in metadata, so limit actual data to 1 byte + # since Gitaly doesn't support "send no data" option. + entry = @repository.gitaly_commit_client.tree_entry(id, path, 1) + return unless entry + + # To be compatible with the rugged format + entry = entry.to_h + entry.delete(:data) + entry[:name] = File.basename(path) + entry[:type] = entry[:type].downcase + + entry + end + + # Is this the same as Blob.find_entry_by_path ? + def rugged_tree_entry(path) + rugged_commit.tree.path(path) + rescue Rugged::TreeError + nil + end + def gitaly_commit_author_from_rugged(author_or_committer) Gitaly::CommitAuthor.new( name: author_or_committer[:name].b, diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb index 976fa1ddfe6..e5a747cb987 100644 --- a/lib/gitlab/git/gitlab_projects.rb +++ b/lib/gitlab/git/gitlab_projects.rb @@ -44,29 +44,13 @@ module Gitlab # Import project via git clone --bare # URL must be publicly cloneable def import_project(source, timeout) - # Skip import if repo already exists - return false if File.exist?(repository_absolute_path) - - masked_source = mask_password_in_url(source) - - logger.info "Importing project from <#{masked_source}> to <#{repository_absolute_path}>." - cmd = %W(git clone --bare -- #{source} #{repository_absolute_path}) - - success = run_with_timeout(cmd, timeout, nil) - - unless success - logger.error("Importing project from <#{masked_source}> to <#{repository_absolute_path}> failed.") - FileUtils.rm_rf(repository_absolute_path) - return false + Gitlab::GitalyClient.migrate(:import_repository) do |is_enabled| + if is_enabled + gitaly_import_repository(source) + else + git_import_repository(source, timeout) + end end - - Gitlab::Git::Repository.create_hooks(repository_absolute_path, global_hooks_path) - - # The project was imported successfully. - # Remove the origin URL since it may contain password. - remove_origin_in_repo - - true end def fork_repository(new_shard_path, new_repository_relative_path) @@ -231,6 +215,42 @@ module Gitlab raise(ShardNameNotFoundError, "no shard found for path '#{shard_path}'") end + def git_import_repository(source, timeout) + # Skip import if repo already exists + return false if File.exist?(repository_absolute_path) + + masked_source = mask_password_in_url(source) + + logger.info "Importing project from <#{masked_source}> to <#{repository_absolute_path}>." + cmd = %W(git clone --bare -- #{source} #{repository_absolute_path}) + + success = run_with_timeout(cmd, timeout, nil) + + unless success + logger.error("Importing project from <#{masked_source}> to <#{repository_absolute_path}> failed.") + FileUtils.rm_rf(repository_absolute_path) + return false + end + + Gitlab::Git::Repository.create_hooks(repository_absolute_path, global_hooks_path) + + # The project was imported successfully. + # Remove the origin URL since it may contain password. + remove_origin_in_repo + + true + end + + def gitaly_import_repository(source) + raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil) + + Gitlab::GitalyClient::RepositoryService.new(raw_repository).import_repository(source) + true + rescue GRPC::BadStatus => e + @output << e.message + false + end + def git_fork_repository(new_shard_path, new_repository_relative_path) from_path = repository_absolute_path to_path = File.join(new_shard_path, new_repository_relative_path) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index d0467bca992..b89a38d187e 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -490,11 +490,7 @@ module Gitlab return [] end - if log_using_shell?(options) - log_by_shell(sha, options) - else - log_by_walk(sha, options) - end + log_by_shell(sha, options) end def count_commits(options) @@ -621,37 +617,6 @@ module Gitlab end end - # Returns branch names collection that contains the special commit(SHA1 - # or name) - # - # Ex. - # repo.branch_names_contains('master') - # - def branch_names_contains(commit) - branches_contains(commit).map { |c| c.name } - end - - # Returns branch collection that contains the special commit(SHA1 or name) - # - # Ex. - # repo.branch_names_contains('master') - # - def branches_contains(commit) - commit_obj = rugged.rev_parse(commit) - parent = commit_obj.parents.first unless commit_obj.parents.empty? - - walker = Rugged::Walker.new(rugged) - - rugged.branches.select do |branch| - walker.push(branch.target_id) - walker.hide(parent) if parent - result = walker.any? { |c| c.oid == commit_obj.oid } - walker.reset - - result - end - end - # Get refs hash which key is SHA1 # and value is a Rugged::Reference def refs_hash @@ -1543,27 +1508,6 @@ module Gitlab end end - def log_using_shell?(options) - options[:path].present? || - options[:disable_walk] || - options[:skip_merges] || - options[:after] || - options[:before] - end - - def log_by_walk(sha, options) - walk_options = { - show: sha, - sort: Rugged::SORT_NONE, - limit: options[:limit], - offset: options[:offset] - } - Rugged::Walker.walk(rugged, walk_options).to_a - end - - # Gitaly note: JV: although #log_by_shell shells out to Git I think the - # complexity is such that we should migrate it as Ruby before trying to - # do it in Go. def log_by_shell(sha, options) limit = options[:limit].to_i offset = options[:offset].to_i diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb index 4974205b8fd..f8b2e7e0e21 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -95,7 +95,7 @@ module Gitlab object_output.map do |output_line| sha, path = output_line.split(' ', 2) - next if require_path && path.blank? + next if require_path && path.to_s.empty? sha end.reject(&:nil?) diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index fed05bb6c64..2231371cfa7 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -177,7 +177,7 @@ module Gitlab response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout) consume_commits_response(response) - rescue GRPC::Unknown # If no repository is found, happens mainly during testing + rescue GRPC::NotFound # If no repository is found, happens mainly during testing [] end @@ -282,6 +282,23 @@ module Gitlab end end + def extract_signature(commit_id) + request = Gitaly::ExtractCommitSignatureRequest.new(repository: @gitaly_repo, commit_id: commit_id) + response = GitalyClient.call(@repository.storage, :commit_service, :extract_commit_signature, request) + + signature = ''.b + signed_text = ''.b + + response.each do |message| + signature << message.signature + signed_text << message.signed_text + end + + return if signature.blank? && signed_text.blank? + + [signature, signed_text] + end + private def call_commit_diff(request_params, options = {}) diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index 72ee92e78dc..12016aee2a6 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -100,6 +100,21 @@ module Gitlab ) end + def import_repository(source) + request = Gitaly::CreateRepositoryFromURLRequest.new( + repository: @gitaly_repo, + url: source + ) + + GitalyClient.call( + @storage, + :repository_service, + :create_repository_from_url, + request, + timeout: GitalyClient.default_timeout + ) + end + def rebase_in_progress?(rebase_id) request = Gitaly::IsRebaseInProgressRequest.new( repository: @gitaly_repo, diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 0f4ba6f83fc..672b5579dfd 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -4,12 +4,8 @@ module Gitlab def initialize(commit) @commit = commit - @signature_text, @signed_text = - begin - Rugged::Commit.extract_signature(@commit.project.repository.rugged, @commit.sha) - rescue Rugged::OdbError - nil - end + repo = commit.project.repository.raw_repository + @signature_text, @signed_text = Gitlab::Git::Commit.extract_signature(repo, commit.sha) end def has_signature? diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb index 989342389bc..5c971564a73 100644 --- a/lib/gitlab/import_export/file_importer.rb +++ b/lib/gitlab/import_export/file_importer.rb @@ -17,12 +17,16 @@ module Gitlab def import mkdir_p(@shared.export_path) + remove_symlinks! + wait_for_archived_file do decompress_archive end rescue => e @shared.error(e) false + ensure + remove_symlinks! end private @@ -43,7 +47,7 @@ module Gitlab raise Projects::ImportService::Error.new("Unable to decompress #{@archive_file} into #{@shared.export_path}") unless result - remove_symlinks! + result end def remove_symlinks! diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb index 6130c124dd1..2daeba90a51 100644 --- a/lib/gitlab/import_export/saver.rb +++ b/lib/gitlab/import_export/saver.rb @@ -37,7 +37,7 @@ module Gitlab end def archive_file - @archive_file ||= File.join(@shared.export_path, '..', Gitlab::ImportExport.export_filename(project: @project)) + @archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project)) end end end diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index 9fd0b709ef2..d03cbc880fd 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -9,7 +9,11 @@ module Gitlab end def export_path - @export_path ||= Gitlab::ImportExport.export_path(relative_path: opts[:relative_path]) + @export_path ||= Gitlab::ImportExport.export_path(relative_path: relative_path) + end + + def archive_path + @archive_path ||= Gitlab::ImportExport.export_path(relative_path: relative_archive_path) end def error(error) @@ -21,6 +25,14 @@ module Gitlab private + def relative_path + File.join(opts[:relative_path], SecureRandom.hex) + end + + def relative_archive_path + File.join(opts[:relative_path], '..') + end + def error_out(message, caller) Rails.logger.error("Import/Export error raised on #{caller}: #{message}") end diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb index 8d8c441a4b1..bf6981035f4 100644 --- a/lib/gitlab/kubernetes/helm/install_command.rb +++ b/lib/gitlab/kubernetes/helm/install_command.rb @@ -36,7 +36,11 @@ module Gitlab def complete_command(namespace_name) return unless chart - "helm install #{chart} --name #{name} --namespace #{namespace_name} >/dev/null" + if chart_values_file + "helm install #{chart} --name #{name} --namespace #{namespace_name} -f /data/helm/#{name}/config/values.yaml >/dev/null" + else + "helm install #{chart} --name #{name} --namespace #{namespace_name} >/dev/null" + end end def install_dps_command diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb index 97ad3c97e95..a3216759cae 100644 --- a/lib/gitlab/kubernetes/helm/pod.rb +++ b/lib/gitlab/kubernetes/helm/pod.rb @@ -10,9 +10,10 @@ module Gitlab def generate spec = { containers: [container_specification], restartPolicy: 'Never' } + if command.chart_values_file - generate_config_map - spec['volumes'] = volumes_specification + create_config_map + spec[:volumes] = volumes_specification end ::Kubeclient::Resource.new(metadata: metadata, spec: spec) @@ -35,19 +36,39 @@ module Gitlab end def labels - { 'gitlab.org/action': 'install', 'gitlab.org/application': command.name } + { + 'gitlab.org/action': 'install', + 'gitlab.org/application': command.name + } end def metadata - { name: command.pod_name, namespace: namespace_name, labels: labels } + { + name: command.pod_name, + namespace: namespace_name, + labels: labels + } end def volume_mounts_specification - [{ name: 'config-volume', mountPath: '/etc/config' }] + [ + { + name: 'configuration-volume', + mountPath: "/data/helm/#{command.name}/config" + } + ] end def volumes_specification - [{ name: 'config-volume', configMap: { name: 'values-config' } }] + [ + { + name: 'configuration-volume', + configMap: { + name: 'values-content-configuration', + items: [{ key: 'values', path: 'values.yaml' }] + } + } + ] end def generate_pod_env(command) @@ -58,10 +79,10 @@ module Gitlab }.map { |key, value| { name: key, value: value } } end - def generate_config_map + def create_config_map resource = ::Kubeclient::Resource.new - resource.metadata = { name: 'values-config', namespace: namespace_name } - resource.data = YAML.load_file(command.chart_values_file) + resource.metadata = { name: 'values-content-configuration', namespace: namespace_name, labels: { name: 'values-content-configuration' } } + resource.data = { values: File.read(command.chart_values_file) } kubeclient.create_config_map(resource) end end diff --git a/lib/gitlab/o_auth.rb b/lib/gitlab/o_auth.rb new file mode 100644 index 00000000000..5ad8d83bd6e --- /dev/null +++ b/lib/gitlab/o_auth.rb @@ -0,0 +1,6 @@ +module Gitlab + module OAuth + SignupDisabledError = Class.new(StandardError) + SigninDisabledForProviderError = Class.new(StandardError) + end +end diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index d33f33d192f..fff9360ea27 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -5,8 +5,6 @@ # module Gitlab module OAuth - SignupDisabledError = Class.new(StandardError) - class User attr_accessor :auth_hash, :gl_user @@ -29,7 +27,8 @@ module Gitlab end def save(provider = 'OAuth') - unauthorized_to_create unless gl_user + raise SigninDisabledForProviderError if oauth_provider_disabled? + raise SignupDisabledError unless gl_user block_after_save = needs_blocking? @@ -226,8 +225,10 @@ module Gitlab Gitlab::AppLogger end - def unauthorized_to_create - raise SignupDisabledError + def oauth_provider_disabled? + Gitlab::CurrentSettings.current_application_settings + .disabled_oauth_sign_in_sources + .include?(auth_hash.provider) end end end diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb index e73245b82c1..e29e168fc5a 100644 --- a/lib/gitlab/performance_bar.rb +++ b/lib/gitlab/performance_bar.rb @@ -6,6 +6,7 @@ module Gitlab EXPIRY_TIME = 5.minutes def self.enabled?(user = nil) + return true if Rails.env.development? return false unless user && allowed_group_id allowed_user_ids.include?(user.id) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index e2662fc362b..7771b15069b 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -44,25 +44,20 @@ module Gitlab ref = nil filename = nil basename = nil + data = "" startline = 0 - result.each_line.each_with_index do |line, index| - matches = line.match(/^(?<ref>[^:]*):(?<filename>.*):(?<startline>\d+):/) - if matches + result.strip.each_line.each_with_index do |line, index| + prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>.*)\x00(?<startline>\d+)\x00/)&.tap do |matches| ref = matches[:ref] filename = matches[:filename] startline = matches[:startline] startline = startline.to_i - index extname = Regexp.escape(File.extname(filename)) basename = filename.sub(/#{extname}$/, '') - break end - end - - data = "" - result.each_line do |line| - data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '') + data << line.sub(prefix.to_s, '') end FoundBlob.new( diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 0002c7da8f1..7ab85e1c35c 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -67,7 +67,7 @@ module Gitlab end def build_trace_section_regex - @build_trace_section_regexp ||= /section_((?:start)|(?:end)):(\d+):([^\r]+)\r\033\[0K/.freeze + @build_trace_section_regexp ||= /section_((?:start)|(?:end)):(\d+):([a-zA-Z0-9_.-]+)\r\033\[0K/.freeze end end end diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index b3baaf036d8..fa22f0e37b2 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -27,6 +27,10 @@ module Gitlab .gsub(/(\A-+|-+\z)/, '') end + def remove_line_breaks(str) + str.gsub(/\r?\n/, '') + end + def to_boolean(value) return value if [true, false].include?(value) return true if value =~ /^(true|t|yes|y|1|on)$/i diff --git a/lib/gitlab/utils/override.rb b/lib/gitlab/utils/override.rb new file mode 100644 index 00000000000..8bf6bcb1fe2 --- /dev/null +++ b/lib/gitlab/utils/override.rb @@ -0,0 +1,111 @@ +module Gitlab + module Utils + module Override + class Extension + def self.verify_class!(klass, method_name) + instance_method_defined?(klass, method_name) || + raise( + NotImplementedError.new( + "#{klass}\##{method_name} doesn't exist!")) + end + + def self.instance_method_defined?(klass, name, include_super: true) + klass.instance_methods(include_super).include?(name) || + klass.private_instance_methods(include_super).include?(name) + end + + attr_reader :subject + + def initialize(subject) + @subject = subject + end + + def add_method_name(method_name) + method_names << method_name + end + + def add_class(klass) + classes << klass + end + + def verify! + classes.each do |klass| + index = klass.ancestors.index(subject) + parents = klass.ancestors.drop(index + 1) + + method_names.each do |method_name| + parents.any? do |parent| + self.class.instance_method_defined?( + parent, method_name, include_super: false) + end || + raise( + NotImplementedError.new( + "#{klass}\##{method_name} doesn't exist!")) + end + end + end + + private + + def method_names + @method_names ||= [] + end + + def classes + @classes ||= [] + end + end + + # Instead of writing patterns like this: + # + # def f + # raise NotImplementedError unless defined?(super) + # + # true + # end + # + # We could write it like: + # + # extend ::Gitlab::Utils::Override + # + # override :f + # def f + # true + # end + # + # This would make sure we're overriding something. See: + # https://gitlab.com/gitlab-org/gitlab-ee/issues/1819 + def override(method_name) + return unless ENV['STATIC_VERIFICATION'] + + if is_a?(Class) + Extension.verify_class!(self, method_name) + else # We delay the check for modules + Override.extensions[self] ||= Extension.new(self) + Override.extensions[self].add_method_name(method_name) + end + end + + def included(base = nil) + return super if base.nil? # Rails concern, ignoring it + + super + + if base.is_a?(Class) # We could check for Class in `override` + # This could be `nil` if `override` was never called + Override.extensions[self]&.add_class(base) + end + end + + alias_method :prepended, :included + + def self.extensions + @extensions ||= {} + end + + def self.verify! + extensions.values.each(&:verify!) + end + end + end +end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index e65609d7001..4beb94eeb8e 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -7,4 +7,9 @@ namespace :dev do Rake::Task["gitlab:setup"].invoke Rake::Task["gitlab:shell:setup"].invoke end + + desc "GitLab | Eager load application" + task load: :environment do + Rails.application.eager_load! + end end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 0e6aed32c52..12ae4199b69 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -54,16 +54,6 @@ namespace :gitlab do # (Re)create hooks Rake::Task['gitlab:shell:create_hooks'].invoke - # Required for debian packaging with PKGR: Setup .ssh/environment with - # the current PATH, so that the correct ruby version gets loaded - # Requires to set "PermitUserEnvironment yes" in sshd config (should not - # be an issue since it is more than likely that there are no "normal" - # user accounts on a gitlab server). The alternative is for the admin to - # install a ruby (1.9.3+) in the global path. - File.open(File.join(user_home, ".ssh", "environment"), "w+") do |f| - f.puts "PATH=#{ENV['PATH']}" - end - Gitlab::Shell.ensure_secret_token! end diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake index 7b63e93db0e..3ab406eff2c 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -1,5 +1,17 @@ unless Rails.env.production? namespace :lint do + task :static_verification_env do + ENV['STATIC_VERIFICATION'] = 'true' + end + + desc "GitLab | lint | Static verification" + task static_verification: %w[ + lint:static_verification_env + dev:load + ] do + Gitlab::Utils::Override.verify! + end + desc "GitLab | lint | Lint JavaScript files using ESLint" task :javascript do Rake::Task['eslint'].invoke diff --git a/package.json b/package.json index 4759ae76817..c9778865e93 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "js-cookie": "^2.1.3", "jszip": "^3.1.3", "jszip-utils": "^0.0.2", - "marked": "^0.3.6", + "marked": "^0.3.12", "monaco-editor": "0.10.0", "mousetrap": "^1.4.6", "name-all-modules-plugin": "^1.0.1", @@ -64,6 +64,7 @@ "raven-js": "^3.14.0", "raw-loader": "^0.5.1", "react-dev-utils": "^0.5.2", + "sanitize-html": "^1.16.1", "select2": "3.5.2-browserify", "sql.js": "^0.4.0", "svg4everybody": "2.1.9", diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb index 5d1d5120929..b4ef07e1540 100644 --- a/qa/qa/page/project/settings/common.rb +++ b/qa/qa/page/project/settings/common.rb @@ -3,9 +3,9 @@ module QA module Project module Settings module Common - def expand(selector) + def expand(element_name) page.within('#content-body') do - find(selector).click + click_element(element_name) yield end diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index a8d6f09777c..bf42767c707 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -3,12 +3,19 @@ module QA module Project module Settings class DeployKeys < Page::Base - ## - # TODO, define all selectors required by this page object - # - # See gitlab-org/gitlab-qa#154 - # - view 'app/views/projects/deploy_keys/edit.html.haml' + view 'app/views/projects/deploy_keys/_form.html.haml' do + element :deploy_key_title, 'text_field :title' + element :deploy_key_key, 'text_area :key' + end + + view 'app/assets/javascripts/deploy_keys/components/app.vue' do + element :deploy_keys_section, /class=".*deploy\-keys.*"/ + end + + view 'app/assets/javascripts/deploy_keys/components/key.vue' do + element :key_title, /class=".*title.*"/ + element :key_title_field, '{{ deployKey.title }}' + end def fill_key_title(title) fill_in 'deploy_key_title', with: title diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index 524d87c6be9..6cc68358c8c 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -5,15 +5,12 @@ module QA class Repository < Page::Base include Common - ## - # TODO, define all selectors required by this page object - # - # See gitlab-org/gitlab-qa#154 - # - view 'app/views/projects/settings/repository/show.html.haml' + view 'app/views/projects/deploy_keys/_index.html.haml' do + element :expand_deploy_keys + end def expand_deploy_keys(&block) - expand('.qa-expand-deploy-keys') do + expand(:expand_deploy_keys) do DeployKeys.perform(&block) end end diff --git a/scripts/add-code-formatters b/scripts/add-code-formatters deleted file mode 100755 index 56bb8754d80..00000000000 --- a/scripts/add-code-formatters +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# Check if file exists with -f. Check if in in the gdk rook directory. -if [ ! -f ../GDK_ROOT ]; then - echo "Please run script from gitlab (e.g. gitlab-development-kit/gitlab) root directory." - exit 1 -fi - -PRECOMMIT=$(git rev-parse --git-dir)/hooks/pre-commit - -# Check if symlink exists with -L. Check if script was already installed. -if [ -L $PRECOMMIT ]; then - echo "Pre-commit script already installed." - exit 1 -fi - -ln -s ./pre-commit $PRECOMMIT -echo "Pre-commit script installed successfully" diff --git a/scripts/pre-commit b/scripts/pre-commit deleted file mode 100644 index 48935e90a87..00000000000 --- a/scripts/pre-commit +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -# Check if file exists with -f. Check if in in the gdk rook directory. -if [ ! -f ../GDK_ROOT ]; then - echo "Please run pre-commit from gitlab (e.g. gitlab-development-kit/gitlab) root directory." - exit 1 -fi - -jsfiles=$(git diff --cached --name-only --diff-filter=ACM "*.js" | tr '\n' ' ') -[ -z "$jsfiles" ] && exit 0 - -# Prettify all staged .js files -echo "$jsfiles" | xargs ./node_modules/.bin/prettier --write - -# Add back the modified/prettified files to staging -echo "$jsfiles" | xargs git add - -exit 0 diff --git a/scripts/static-analysis b/scripts/static-analysis index 2a2bc67800d..9690b42c788 100755 --- a/scripts/static-analysis +++ b/scripts/static-analysis @@ -10,9 +10,10 @@ tasks = [ %w[bundle exec license_finder], %w[yarn run eslint], %w[bundle exec rubocop --parallel], - %w[scripts/lint-conflicts.sh], %w[bundle exec rake gettext:lint], - %w[scripts/lint-changelog-yaml] + %w[bundle exec rake lint:static_verification], + %w[scripts/lint-changelog-yaml], + %w[scripts/lint-conflicts.sh] ] failed_tasks = tasks.reduce({}) do |failures, task| diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb index e6ba596117a..d2c1e634930 100644 --- a/spec/controllers/admin/hooks_controller_spec.rb +++ b/spec/controllers/admin/hooks_controller_spec.rb @@ -11,11 +11,13 @@ describe Admin::HooksController do it 'sets all parameters' do hook_params = { enable_ssl_verification: true, + token: "TEST TOKEN", + url: "http://example.com", + push_events: true, tag_push_events: true, repository_update_events: true, - token: "TEST TOKEN", - url: "http://example.com" + merge_requests_events: true } post :create, hook: hook_params diff --git a/spec/controllers/import/gitlab_projects_controller_spec.rb b/spec/controllers/import/gitlab_projects_controller_spec.rb new file mode 100644 index 00000000000..8759d3c0b97 --- /dev/null +++ b/spec/controllers/import/gitlab_projects_controller_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Import::GitlabProjectsController do + set(:namespace) { create(:namespace) } + set(:user) { namespace.owner } + let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + + before do + sign_in(user) + end + + describe 'POST create' do + context 'with an invalid path' do + it 'redirects with an error' do + post :create, namespace_id: namespace.id, path: '/test', file: file + + expect(flash[:alert]).to start_with('Project could not be imported') + expect(response).to have_gitlab_http_status(302) + end + + it 'redirects with an error when a relative path is used' do + post :create, namespace_id: namespace.id, path: '../test', file: file + + expect(flash[:alert]).to start_with('Project could not be imported') + expect(response).to have_gitlab_http_status(302) + end + end + + context 'with a valid path' do + it 'redirects to the new project path' do + post :create, namespace_id: namespace.id, path: 'test', file: file + + expect(flash[:notice]).to include('is being imported') + expect(response).to have_gitlab_http_status(302) + end + end + end +end diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb new file mode 100644 index 00000000000..c639ad32ec6 --- /dev/null +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe OmniauthCallbacksController do + include LoginHelpers + + let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: provider) } + let(:provider) { :github } + + before do + mock_auth_hash(provider.to_s, 'my-uid', user.email) + stub_omniauth_provider(provider, context: request) + end + + it 'allows sign in' do + post provider + + expect(request.env['warden']).to be_authenticated + end + + shared_context 'sign_up' do + let(:user) { double(email: 'new@example.com') } + + before do + stub_omniauth_setting(block_auto_created_users: false) + end + end + + context 'sign up' do + include_context 'sign_up' + + it 'is allowed' do + post provider + + expect(request.env['warden']).to be_authenticated + end + end + + context 'when OAuth is disabled' do + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + settings = Gitlab::CurrentSettings.current_application_settings + settings.update(disabled_oauth_sign_in_sources: [provider.to_s]) + end + + it 'prevents login via POST' do + post provider + + expect(request.env['warden']).not_to be_authenticated + end + + it 'shows warning when attempting login' do + post provider + + expect(response).to redirect_to new_user_session_path + expect(flash[:alert]).to eq('Signing in using GitHub has been disabled') + end + + it 'allows linking the disabled provider' do + user.identities.destroy_all + sign_in(user) + + expect { post provider }.to change { user.reload.identities.count }.by(1) + end + + context 'sign up' do + include_context 'sign_up' + + it 'is prevented' do + post provider + + expect(request.env['warden']).not_to be_authenticated + end + end + end +end diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb index aba70c6d4c1..2d473d5bf52 100644 --- a/spec/controllers/projects/hooks_controller_spec.rb +++ b/spec/controllers/projects/hooks_controller_spec.rb @@ -18,4 +18,30 @@ describe Projects::HooksController do ) end end + + describe '#create' do + it 'sets all parameters' do + hook_params = { + enable_ssl_verification: true, + token: "TEST TOKEN", + url: "http://example.com", + + push_events: true, + tag_push_events: true, + merge_requests_events: true, + issues_events: true, + confidential_issues_events: true, + note_events: true, + job_events: true, + pipeline_events: true, + wiki_page_events: true + } + + post :create, namespace_id: project.namespace, project_id: project, hook: hook_params + + expect(response).to have_http_status(302) + expect(ProjectHook.all.size).to eq(1) + expect(ProjectHook.first).to have_attributes(hook_params) + end + end end diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb index 7e2366847f4..92db7284e0e 100644 --- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb @@ -4,6 +4,16 @@ describe Projects::MergeRequests::CreationsController do let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:fork_project) { create(:forked_project_with_submodules) } + let(:get_diff_params) do + { + namespace_id: fork_project.namespace.to_param, + project_id: fork_project, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' + } + } + end before do fork_project.add_master(user) @@ -13,18 +23,23 @@ describe Projects::MergeRequests::CreationsController do describe 'GET new' do context 'merge request that removes a submodule' do - render_views - it 'renders new merge request widget template' do - get :new, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - } + get :new, get_diff_params + + expect(response).to be_success + end + end + end + + describe 'GET diffs' do + context 'when merge request cannot be created' do + it 'does not assign diffs var' do + allow_any_instance_of(MergeRequest).to receive(:can_be_created).and_return(false) + + get :diffs, get_diff_params.merge(format: 'json') expect(response).to be_success + expect(assigns[:diffs]).to be_nil end end end @@ -37,14 +52,7 @@ describe Projects::MergeRequests::CreationsController do end it 'renders JSON including serialized pipelines' do - get :pipelines, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - }, - format: :json + get :pipelines, get_diff_params.merge(format: 'json') expect(response).to be_ok expect(json_response).to have_key 'pipelines' diff --git a/spec/factories/deploy_keys_projects.rb b/spec/factories/deploy_keys_projects.rb index 30a6d468ed3..4350652fb79 100644 --- a/spec/factories/deploy_keys_projects.rb +++ b/spec/factories/deploy_keys_projects.rb @@ -2,5 +2,9 @@ FactoryBot.define do factory :deploy_keys_project do deploy_key project + + trait :write_access do + can_push true + end end end diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index 552b4b7e06e..f0c43f3d6f5 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -15,10 +15,6 @@ FactoryBot.define do factory :another_deploy_key, class: 'DeployKey' end - factory :write_access_key, class: 'DeployKey' do - can_push true - end - factory :rsa_key_2048 do key do <<~KEY.delete("\n") diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index e020579f71e..51b42d1b43b 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -21,7 +21,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_selector('.row-content-block', text: 'All jobs') expect(page.all('.build-link').size).to eq(4) - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end @@ -31,7 +31,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content 'No jobs to show' - expect(page).not_to have_link 'Cancel all' + expect(page).not_to have_button 'Stop all jobs' end end end @@ -51,7 +51,7 @@ describe 'Admin Builds' do expect(page.find('.build-link')).not_to have_content(build2.id) expect(page.find('.build-link')).not_to have_content(build3.id) expect(page.find('.build-link')).not_to have_content(build4.id) - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end @@ -63,7 +63,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_content 'No jobs to show' - expect(page).not_to have_link 'Cancel all' + expect(page).not_to have_button 'Stop all jobs' end end end @@ -83,7 +83,7 @@ describe 'Admin Builds' do expect(page.find('.build-link')).not_to have_content(build2.id) expect(page.find('.build-link')).not_to have_content(build3.id) expect(page.find('.build-link')).not_to have_content(build4.id) - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end @@ -95,7 +95,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_content 'No jobs to show' - expect(page).not_to have_link 'Cancel all' + expect(page).not_to have_button 'Stop all jobs' end end end @@ -113,7 +113,7 @@ describe 'Admin Builds' do expect(page.find('.build-link')).not_to have_content(build1.id) expect(page.find('.build-link')).not_to have_content(build2.id) expect(page.find('.build-link')).to have_content(build3.id) - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end @@ -125,7 +125,7 @@ describe 'Admin Builds' do expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page).to have_content 'No jobs to show' - expect(page).to have_link 'Cancel all' + expect(page).to have_button 'Stop all jobs' end end end diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb index 241c7cbc34e..cb96830cb7c 100644 --- a/spec/features/admin/admin_deploy_keys_spec.rb +++ b/spec/features/admin/admin_deploy_keys_spec.rb @@ -17,6 +17,16 @@ RSpec.describe 'admin deploy keys' do end end + it 'shows all the projects the deploy key has write access' do + write_key = create(:deploy_keys_project, :write_access, deploy_key: deploy_key) + + visit admin_deploy_keys_path + + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content(write_key.project.full_name) + end + end + describe 'create a new deploy key' do let(:new_ssh_key) { attributes_for(:key)[:key] } @@ -28,14 +38,12 @@ RSpec.describe 'admin deploy keys' do it 'creates a new deploy key' do fill_in 'deploy_key_title', with: 'laptop' fill_in 'deploy_key_key', with: new_ssh_key - check 'deploy_key_can_push' click_button 'Create' expect(current_path).to eq admin_deploy_keys_path page.within(find('.deploy-keys-list', match: :first)) do expect(page).to have_content('laptop') - expect(page).to have_content('Yes') end end end @@ -48,14 +56,12 @@ RSpec.describe 'admin deploy keys' do it 'updates an existing deploy key' do fill_in 'deploy_key_title', with: 'new-title' - check 'deploy_key_can_push' click_button 'Save changes' expect(current_path).to eq admin_deploy_keys_path page.within(find('.deploy-keys-list', match: :first)) do expect(page).to have_content('new-title') - expect(page).to have_content('Yes') end end end diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index eec44549a03..f266f2ecc54 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -1,11 +1,10 @@ require 'spec_helper' -describe 'Admin::Hooks', :js do - before do - @project = create(:project) - sign_in(create(:admin)) +describe 'Admin::Hooks' do + let(:user) { create(:admin) } - @system_hook = create(:system_hook) + before do + sign_in(user) end describe 'GET /admin/hooks' do @@ -13,15 +12,17 @@ describe 'Admin::Hooks', :js do visit admin_root_path page.within '.nav-sidebar' do - click_on 'Hooks' + click_on 'System Hooks', match: :first end expect(current_path).to eq(admin_hooks_path) end it 'has hooks list' do + system_hook = create(:system_hook) + visit admin_hooks_path - expect(page).to have_content(@system_hook.url) + expect(page).to have_content(system_hook.url) end end @@ -43,6 +44,10 @@ describe 'Admin::Hooks', :js do describe 'Update existing hook' do let(:new_url) { generate(:url) } + before do + create(:system_hook) + end + it 'updates existing hook' do visit admin_hooks_path @@ -57,7 +62,11 @@ describe 'Admin::Hooks', :js do end end - describe 'Remove existing hook' do + describe 'Remove existing hook', :js do + before do + create(:system_hook) + end + context 'removes existing hook' do it 'from hooks list page' do visit admin_hooks_path @@ -76,7 +85,8 @@ describe 'Admin::Hooks', :js do describe 'Test', :js do before do - WebMock.stub_request(:post, @system_hook.url) + system_hook = create(:system_hook) + WebMock.stub_request(:post, system_hook.url) visit admin_hooks_path find('.hook-test-button.dropdown').click @@ -85,4 +95,41 @@ describe 'Admin::Hooks', :js do it { expect(current_path).to eq(admin_hooks_path) } end + + context 'Merge request hook' do + describe 'New Hook' do + let(:url) { generate(:url) } + + it 'adds new hook' do + visit admin_hooks_path + + fill_in 'hook_url', with: url + uncheck 'Repository update events' + check 'Merge request events' + + expect { click_button 'Add system hook' }.to change(SystemHook, :count).by(1) + expect(current_path).to eq(admin_hooks_path) + expect(page).to have_content(url) + end + end + + describe 'Test', :js do + before do + system_hook = create(:system_hook) + WebMock.stub_request(:post, system_hook.url) + end + + it 'succeeds if the user has a repository with a merge request' do + project = create(:project, :repository) + create(:project_member, user: user, project: project) + create(:merge_request, source_project: project) + + visit admin_hooks_path + find('.hook-test-button.dropdown').click + click_link 'Merge requests events' + + expect(page).to have_content 'Hook executed successfully' + end + end + end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 3876d1c76d7..3d13f806b11 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -69,6 +69,7 @@ describe 'Issue Boards', :js do let!(:backlog) { create(:label, project: project, name: 'Backlog') } let!(:closed) { create(:label, project: project, name: 'Closed') } let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') } + let!(:a_plus) { create(:label, project: project, name: 'A+') } let!(:list1) { create(:list, board: board, label: planning, position: 0) } let!(:list2) { create(:list, board: board, label: development, position: 1) } @@ -83,6 +84,7 @@ describe 'Issue Boards', :js do let!(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) } let!(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') } let!(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) } + let!(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) } before do visit project_board_path(project, board) @@ -400,6 +402,15 @@ describe 'Issue Boards', :js do wait_for_empty_boards((3..4)) end + it 'filters by label with encoded character' do + set_filter("label", a_plus.title) + click_filter_link(a_plus.title) + submit_filter + + wait_for_board_cards(1, 1) + wait_for_empty_boards((2..4)) + end + it 'filters by label with space after reload' do set_filter("label", "\"#{accepting.title}") click_filter_link(accepting.title) diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index d36954954b6..510677ecf56 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -113,6 +113,7 @@ feature 'Cycle Analytics', :js do context "as a guest" do before do + project.add_developer(user) project.add_guest(guest) allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index a5c9d0bde5d..64b4f9e7e67 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -8,6 +8,7 @@ feature 'Issue Sidebar' do let(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} let!(:label) { create(:label, project: project, title: 'bug') } + let!(:xss_label) { create(:label, project: project, title: '<script>alert("xss");</script>') } before do sign_in(user) @@ -99,6 +100,14 @@ feature 'Issue Sidebar' do restore_window_size open_issue_sidebar end + + it 'escapes XSS when viewing issue labels' do + page.within('.block.labels') do + find('.edit-link').click + + expect(page).to have_content '<script>alert("xss");</script>' + end + end end context 'editing issue labels', :js do diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index 49d8e52f861..a5e325ee2e3 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -10,8 +10,7 @@ feature 'OAuth Login', :js, :allow_forgery_protection do def stub_omniauth_config(provider) OmniAuth.config.add_mock(provider, OmniAuth::AuthHash.new(provider: provider.to_s, uid: "12345")) - set_devise_mapping(context: Rails.application) - Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider] + stub_omniauth_provider(provider) end providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2, diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index af125e1b9d3..e8bb9c6a86c 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -32,7 +32,7 @@ feature 'Import/Export - project import integration test', :js do expect(page).to have_content('Import an exported GitLab project') expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}") - expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\h*\z/).and_call_original + expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}\z/).and_call_original attach_file('file', file) click_on 'Import project' diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 9a6b27c00f8..a5cd858b11a 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -380,9 +380,18 @@ feature 'Jobs' do it 'shows manual action empty state' do expect(page).to have_content('This job requires a manual action') - expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments.') + expect(page).to have_content('This job depends on a user to trigger its process. Often they are used to deploy code to production environments') expect(page).to have_link('Trigger this manual action') end + + it 'plays manual action', :js do + click_link 'Trigger this manual action' + + wait_for_requests + expect(page).to have_content('This job has not been triggered') + expect(page).to have_content('This job is stuck, because the project doesn\'t have any runners online assigned to it.') + expect(page).to have_content('pending') + end end context 'Non triggered job' do @@ -392,9 +401,8 @@ feature 'Jobs' do visit project_job_path(project, job) end - it 'shows manual action empty state' do + it 'shows empty state' do expect(page).to have_content('This job has not been triggered yet') - expect(page).to have_content('This job depends on upstream jobs that need to succeed in order for this job to be triggered.') end end end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 81b282502fc..14670e91006 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -43,7 +43,7 @@ feature 'Repository settings' do fill_in 'deploy_key_title', with: 'new_deploy_key' fill_in 'deploy_key_key', with: new_ssh_key - check 'deploy_key_can_push' + check 'deploy_key_deploy_keys_projects_attributes_0_can_push' click_button 'Add key' expect(page).to have_content('new_deploy_key') @@ -57,7 +57,7 @@ feature 'Repository settings' do find('li', text: private_deploy_key.title).click_link('Edit') fill_in 'deploy_key_title', with: 'updated_deploy_key' - check 'deploy_key_can_push' + check 'deploy_key_deploy_keys_projects_attributes_0_can_push' click_button 'Save changes' expect(page).to have_content('updated_deploy_key') @@ -74,11 +74,9 @@ feature 'Repository settings' do find('li', text: private_deploy_key.title).click_link('Edit') fill_in 'deploy_key_title', with: 'updated_deploy_key' - check 'deploy_key_can_push' click_button 'Save changes' expect(page).to have_content('updated_deploy_key') - expect(page).to have_content('Write access allowed') end scenario 'remove an existing deploy key' do diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb index 670e8dda916..975c157bcf5 100644 --- a/spec/features/user_can_display_performance_bar_spec.rb +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe 'User can display performance bar', :js do - shared_examples 'performance bar is disabled' do + shared_examples 'performance bar cannot be displayed' do it 'does not show the performance bar by default' do expect(page).not_to have_css('#peek') end @@ -17,7 +17,7 @@ describe 'User can display performance bar', :js do end end - shared_examples 'performance bar is enabled' do + shared_examples 'performance bar can be displayed' do it 'does not show the performance bar by default' do expect(page).not_to have_css('#peek') end @@ -33,6 +33,18 @@ describe 'User can display performance bar', :js do end end + shared_examples 'performance bar is enabled by default in development' do + before do + allow(Rails.env).to receive(:development?).and_return(true) + end + + it 'shows the performance bar by default' do + refresh # Because we're stubbing Rails.env after the 1st visit to root_path + + expect(page).to have_css('#peek') + end + end + let(:group) { create(:group) } context 'when user is logged-out' do @@ -45,7 +57,7 @@ describe 'User can display performance bar', :js do stub_application_setting(performance_bar_allowed_group_id: nil) end - it_behaves_like 'performance bar is disabled' + it_behaves_like 'performance bar cannot be displayed' end context 'when the performance_bar feature is enabled' do @@ -53,7 +65,7 @@ describe 'User can display performance bar', :js do stub_application_setting(performance_bar_allowed_group_id: group.id) end - it_behaves_like 'performance bar is disabled' + it_behaves_like 'performance bar cannot be displayed' end end @@ -72,7 +84,8 @@ describe 'User can display performance bar', :js do stub_application_setting(performance_bar_allowed_group_id: nil) end - it_behaves_like 'performance bar is disabled' + it_behaves_like 'performance bar cannot be displayed' + it_behaves_like 'performance bar is enabled by default in development' end context 'when the performance_bar feature is enabled' do @@ -80,7 +93,8 @@ describe 'User can display performance bar', :js do stub_application_setting(performance_bar_allowed_group_id: group.id) end - it_behaves_like 'performance bar is enabled' + it_behaves_like 'performance bar is enabled by default in development' + it_behaves_like 'performance bar can be displayed' end end end diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb index 8ae08656e01..0b3cf7ece5f 100644 --- a/spec/finders/milestones_finder_spec.rb +++ b/spec/finders/milestones_finder_spec.rb @@ -21,10 +21,19 @@ describe MilestonesFinder do expect(result).to contain_exactly(milestone_1, milestone_2) end - it 'returns milestones for groups and projects' do - result = described_class.new(project_ids: [project_1.id, project_2.id], group_ids: group.id, state: 'all').execute + context 'milestones for groups and project' do + let(:result) do + described_class.new(project_ids: [project_1.id, project_2.id], group_ids: group.id, state: 'all').execute + end + + it 'returns milestones for groups and projects' do + expect(result).to contain_exactly(milestone_1, milestone_2, milestone_3, milestone_4) + end - expect(result).to contain_exactly(milestone_1, milestone_2, milestone_3, milestone_4) + it 'orders milestones by due date' do + expect(result.first).to eq(milestone_1) + expect(result.second).to eq(milestone_3) + end end context 'with filters' do @@ -61,30 +70,4 @@ describe MilestonesFinder do expect(result.to_a).to contain_exactly(milestone_1) end end - - context 'with order' do - let(:params) do - { - project_ids: [project_1.id, project_2.id], - group_ids: group.id, - state: 'all' - } - end - - it "default orders by due date" do - result = described_class.new(params).execute - - expect(result.first).to eq(milestone_1) - expect(result.second).to eq(milestone_3) - end - - it "orders by parameter" do - result = described_class.new(params.merge(order: 'id DESC')).execute - - expect(result.first).to eq(milestone_4) - expect(result.second).to eq(milestone_3) - expect(result.third).to eq(milestone_2) - expect(result.fourth).to eq(milestone_1) - end - end end diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 04620f6d88c..a030796c54e 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -22,6 +22,13 @@ describe BlobHelper do expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>]) end + it 'returns plaintext for long blobs' do + stub_const('Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE', 1) + result = helper.highlight(blob_name, blob_content) + + expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem"))</span></code></pre>]) + end + it 'highlights single block' do expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> <span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>] diff --git a/spec/javascripts/boards/utils/query_data_spec.js b/spec/javascripts/boards/utils/query_data_spec.js new file mode 100644 index 00000000000..922215ffc1d --- /dev/null +++ b/spec/javascripts/boards/utils/query_data_spec.js @@ -0,0 +1,27 @@ +import queryData from '~/boards/utils/query_data'; + +describe('queryData', () => { + it('parses path for label with trailing +', () => { + expect( + queryData('label_name[]=label%2B', {}), + ).toEqual({ + label_name: ['label+'], + }); + }); + + it('parses path for milestone with trailing +', () => { + expect( + queryData('milestone_title=A%2B', {}), + ).toEqual({ + milestone_title: 'A+', + }); + }); + + it('parses path for search terms with spaces', () => { + expect( + queryData('search=two+words', {}), + ).toEqual({ + search: 'two words', + }); + }); +}); diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 2f28c5bbf01..b7aadf604a4 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -53,18 +53,24 @@ describe('Deploy keys key', () => { ).toBe('Remove'); }); - it('shows write access text when key has write access', (done) => { - vm.deployKey.can_push = true; + it('shows write access title when key has write access', (done) => { + vm.deployKey.deploy_keys_projects[0].can_push = true; Vue.nextTick(() => { expect( - vm.$el.querySelector('.write-access-allowed'), - ).not.toBeNull(); - - expect( - vm.$el.querySelector('.write-access-allowed').textContent.trim(), + vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), ).toBe('Write access allowed'); + done(); + }); + }); + + it('does not show write access title when key has write access', (done) => { + vm.deployKey.deploy_keys_projects[0].can_push = false; + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), + ).toBe('Read access only'); done(); }); }); diff --git a/spec/javascripts/fixtures/search.rb b/spec/javascripts/fixtures/search.rb new file mode 100644 index 00000000000..703cd3d49fa --- /dev/null +++ b/spec/javascripts/fixtures/search.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe SearchController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + render_views + + before(:all) do + clean_frontend_fixtures('search/') + end + + it 'search/show.html.raw' do |example| + get :show + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index b740c9ed893..feb341d22e6 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -52,11 +52,6 @@ describe('Job', () => { expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false); expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false); }); - - it('displays the remove date correctly', () => { - const removeDateElement = document.querySelector('.js-artifacts-remove'); - expect(removeDateElement.innerText.trim()).toBe('1 year remaining'); - }); }); describe('running build', () => { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 9d6ea3781bc..bae3219b043 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -70,8 +70,8 @@ import IssuablesHelper from '~/helpers/issuables_helper'; beforeEach(() => { loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.el = document.querySelector('.js-issuable-actions'); - const merge = new MergeRequest(); - merge.hideCloseButton(); + new MergeRequest(); // eslint-disable-line no-new + MergeRequest.hideCloseButton(); }); it('hides the dropdown close item and selects the next item', () => { @@ -90,8 +90,7 @@ import IssuablesHelper from '~/helpers/issuables_helper'; beforeEach(() => { loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); this.el = document.querySelector('.js-issuable-actions'); - const merge = new MergeRequest(); - merge.hideCloseButton(); + MergeRequest.hideCloseButton(); }); it('hides the close button', () => { diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js index a88e9ed3d99..02304bf5d7d 100644 --- a/spec/javascripts/notebook/cells/markdown_spec.js +++ b/spec/javascripts/notebook/cells/markdown_spec.js @@ -42,6 +42,18 @@ describe('Markdown component', () => { expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); }); + it('sanitizes output', (done) => { + Object.assign(cell, { + source: ['[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n'], + }); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('a')).toBeNull(); + + done(); + }); + }); + describe('katex', () => { beforeEach(() => { json = getJSONFixture('blob/notebook/math.json'); diff --git a/spec/javascripts/notebook/cells/output/html_sanitize_tests.js b/spec/javascripts/notebook/cells/output/html_sanitize_tests.js new file mode 100644 index 00000000000..d587573fc9e --- /dev/null +++ b/spec/javascripts/notebook/cells/output/html_sanitize_tests.js @@ -0,0 +1,66 @@ +export default { + 'protocol-based JS injection: simple, no spaces': { + input: '<a href="javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before': { + input: '<a href="javascript :alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces after': { + input: '<a href="javascript: alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: simple, spaces before and after': { + input: '<a href="javascript : alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: preceding colon': { + input: '<a href=":javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long UTF-8 encoding without semicolons': { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: long hex encoding': { + input: '<a href="javascript:">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: hex encoding without semicolons': { + input: '<a href=javascript:alert('XSS')>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: null char': { + input: '<a href=java\0script:alert("XSS")>foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: invalid URL char': { + input: '<img src=java\script:alert("XSS")>', // eslint-disable-line no-useless-escape + output: '<img>', + }, + 'protocol-based JS injection: Unicode': { + input: '<a href="\u0001java\u0003script:alert(\'XSS\')">foo</a>', + output: '<a>foo</a>', + }, + 'protocol-based JS injection: spaces and entities': { + input: '<a href="  javascript:alert(\'XSS\');">foo</a>', + output: '<a>foo</a>', + }, + 'img on error': { + input: '<img src="x" onerror="alert(document.domain)" />', + output: '<img src="x">', + }, +}; diff --git a/spec/javascripts/notebook/cells/output/html_spec.js b/spec/javascripts/notebook/cells/output/html_spec.js new file mode 100644 index 00000000000..9c5385f2922 --- /dev/null +++ b/spec/javascripts/notebook/cells/output/html_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import htmlOutput from '~/notebook/cells/output/html.vue'; +import sanitizeTests from './html_sanitize_tests'; + +describe('html output cell', () => { + function createComponent(rawCode) { + const Component = Vue.extend(htmlOutput); + + return new Component({ + propsData: { + rawCode, + }, + }).$mount(); + } + + describe('sanitizes output', () => { + Object.keys(sanitizeTests).forEach((key) => { + it(key, () => { + const test = sanitizeTests[key]; + const vm = createComponent(test.input); + const outputEl = [...vm.$el.querySelectorAll('div')].pop(); + + expect(outputEl.innerHTML).toEqual(test.output); + + vm.$destroy(); + }); + }); + }); +}); diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js new file mode 100644 index 00000000000..440a6585d57 --- /dev/null +++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; + +import axios from '~/lib/utils/axios_utils'; +import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue'; +import * as urlUtility from '~/lib/utils/url_utility'; + +import mountComponent from '../../../../../helpers/vue_mount_component_helper'; + +describe('stop_jobs_modal.vue', () => { + const props = { + url: `${gl.TEST_HOST}/stop_jobs_modal.vue/stopAll`, + }; + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + beforeEach(() => { + const Component = Vue.extend(stopJobsModal); + vm = mountComponent(Component, props); + }); + + describe('onSubmit', () => { + it('stops jobs and redirects to overview page', (done) => { + const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`; + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + spyOn(axios, 'post').and.callFake((url) => { + expect(url).toBe(props.url); + return Promise.resolve({ + request: { + responseURL, + }, + }); + }); + + vm.onSubmit() + .then(() => { + expect(redirectSpy).toHaveBeenCalledWith(responseURL); + }) + .then(done) + .catch(done.fail); + }); + + it('displays error if stopping jobs failed', (done) => { + const dummyError = new Error('stopping jobs failed'); + const redirectSpy = spyOn(urlUtility, 'redirectTo'); + spyOn(axios, 'post').and.callFake((url) => { + expect(url).toBe(props.url); + return Promise.reject(dummyError); + }); + + vm.onSubmit() + .then(done.fail) + .catch((error) => { + expect(error).toBe(dummyError); + expect(redirectSpy).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 48620898357..d010d897642 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -13,7 +13,7 @@ describe('Pipelines Async Button', () => { propsData: { endpoint: '/foo', title: 'Foo', - icon: 'fa fa-foo', + icon: 'repeat', cssClass: 'bar', }, }).$mount(); @@ -23,8 +23,8 @@ describe('Pipelines Async Button', () => { expect(component.$el.tagName).toEqual('BUTTON'); }); - it('should render the provided icon', () => { - expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo'); + it('should render svg icon', () => { + expect(component.$el.querySelector('svg')).not.toBeNull(); }); it('should render the provided title', () => { diff --git a/spec/javascripts/search_spec.js b/spec/javascripts/search_spec.js new file mode 100644 index 00000000000..38e94d45e55 --- /dev/null +++ b/spec/javascripts/search_spec.js @@ -0,0 +1,40 @@ +import Api from '~/api'; +import Search from '~/pages/search/show/search'; + +describe('Search', () => { + const fixturePath = 'search/show.html.raw'; + const searchTerm = 'some search'; + const fillDropdownInput = (dropdownSelector) => { + const dropdownElement = document.querySelector(dropdownSelector).parentNode; + const inputElement = dropdownElement.querySelector('.dropdown-input-field'); + inputElement.value = searchTerm; + return inputElement; + }; + + preloadFixtures(fixturePath); + + beforeEach(() => { + loadFixtures(fixturePath); + new Search(); // eslint-disable-line no-new + }); + + it('requests groups from backend when filtering', (done) => { + spyOn(Api, 'groups').and.callFake((term) => { + expect(term).toBe(searchTerm); + done(); + }); + const inputElement = fillDropdownInput('.js-search-group-dropdown'); + + $(inputElement).trigger('input'); + }); + + it('requests projects from backend when filtering', (done) => { + spyOn(Api, 'projects').and.callFake((term) => { + expect(term).toBe(searchTerm); + done(); + }); + const inputElement = fillDropdownInput('.js-search-project-dropdown'); + + $(inputElement).trigger('input'); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 1127576617b..9230b5874df 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -371,6 +371,10 @@ describe('MRWidgetReadyToMerge', () => { }); }); + beforeEach(() => { + loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + }); + it('should call start and stop polling when MR merged', (done) => { spyOn(eventHub, '$emit'); spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); @@ -392,6 +396,47 @@ describe('MRWidgetReadyToMerge', () => { }, 333); }); + it('updates status box', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + const statusBox = document.querySelector('.status-box'); + expect(statusBox.classList.contains('status-box-mr-merged')).toBeTruthy(); + expect(statusBox.textContent).toContain('Merged'); + + done(); + }); + }); + + it('hides close button', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + expect(document.querySelector('.btn-close').classList.contains('hidden')).toBeTruthy(); + + done(); + }); + }); + + it('updates merge request count badge', (done) => { + spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged')); + spyOn(vm, 'initiateRemoveSourceBranchPolling'); + + vm.handleMergePolling(() => {}, () => {}); + + setTimeout(() => { + expect(document.querySelector('.js-merge-counter').textContent).toBe('0'); + + done(); + }); + }); + it('should continue polling until MR is merged', (done) => { spyOn(vm.service, 'poll').and.returnValue(returnPromise('some_other_state')); spyOn(vm, 'initiateRemoveSourceBranchPolling'); diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js new file mode 100644 index 00000000000..6940b04573e --- /dev/null +++ b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js @@ -0,0 +1,77 @@ +import Vue from 'vue'; + +import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = (config) => { + const Component = Vue.extend(stackedProgressBarComponent); + const defaultConfig = Object.assign({}, { + successLabel: 'Synced', + failureLabel: 'Failed', + neutralLabel: 'Out of sync', + successCount: 10, + failureCount: 5, + totalCount: 20, + }, config); + + return mountComponent(Component, defaultConfig); +}; + +describe('StackedProgressBarComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('neutralCount', () => { + it('returns neutralCount based on totalCount, successCount and failureCount', () => { + expect(vm.neutralCount).toBe(5); // 20 - 10 - 5 + }); + }); + }); + + describe('methods', () => { + describe('getPercent', () => { + it('returns percentage from provided count based on `totalCount`', () => { + expect(vm.getPercent(10)).toBe(50); + }); + }); + + describe('barStyle', () => { + it('returns style string based on percentage provided', () => { + expect(vm.barStyle(50)).toBe('width: 50%;'); + }); + }); + + describe('getTooltip', () => { + it('returns label string based on label and count provided', () => { + expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10'); + }); + }); + }); + + describe('template', () => { + it('renders container element', () => { + expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy(); + }); + + it('renders empty state when count is unavailable', () => { + const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 }); + expect(vmX.$el.querySelectorAll('.status-unavailable').length).not.toBe(0); + vmX.$destroy(); + }); + + it('renders bar elements when count is available', () => { + expect(vm.$el.querySelectorAll('.status-green').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-neutral').length).not.toBe(0); + expect(vm.$el.querySelectorAll('.status-red').length).not.toBe(0); + }); + }); +}); diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index b68301a066a..5100f5737c2 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -194,6 +194,12 @@ describe Backup::Manager do ) end + it 'prints the list of available backups' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('1451606400_2016_01_01_1.2.3\n 1451520000_2015_12_31')) + end + it 'fails the operation and prints an error' do expect { subject.unpack }.to raise_error SystemExit expect(progress).to have_received(:puts) diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb index 9596f004052..50d053011b3 100644 --- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb +++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb @@ -10,15 +10,23 @@ describe Banzai::Filter::WikiLinkFilter do it "doesn't rewrite absolute links" do filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0] + expect(filtered_link.attribute('href').value).to eq('http://example.com:8000/') end + it "doesn't rewrite links to project uploads" do + filtered_link = filter("<a href='/uploads/a.test'>Link</a>", project_wiki: wiki).children[0] + + expect(filtered_link.attribute('href').value).to eq('/uploads/a.test') + end + describe "invalid links" do invalid_links = ["http://:8080", "http://", "http://:8080/path"] invalid_links.each do |invalid_link| it "doesn't rewrite invalid invalid_links like #{invalid_link}" do filtered_link = filter("<a href='#{invalid_link}'>Link</a>", project_wiki: wiki).children[0] + expect(filtered_link.attribute('href').value).to eq(invalid_link) end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index a6fbec295b5..cc202ce8bca 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -136,8 +136,8 @@ describe Gitlab::Auth do it 'grants deploy key write permissions' do project = create(:project) - key = create(:deploy_key, can_push: true) - create(:deploy_keys_project, deploy_key: key, project: project) + key = create(:deploy_key) + create(:deploy_keys_project, :write_access, deploy_key: key, project: project) token = Gitlab::LfsToken.new(key).token expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") @@ -146,7 +146,7 @@ describe Gitlab::Auth do it 'does not grant deploy key write permissions' do project = create(:project) - key = create(:deploy_key, can_push: true) + key = create(:deploy_key) token = Gitlab::LfsToken.new(key).token expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index 98730602863..d21183b668b 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -15,6 +15,10 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :t .to receive(:commits_count=).and_return(nil) end + after do + [Project, MergeRequest, MergeRequestDiff].each(&:reset_column_information) + end + def diffs_to_hashes(diffs) diffs.as_json(only: Gitlab::Git::Diff::SERIALIZE_KEYS).map(&:with_indifferent_access) end diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index 05e2d94cbd6..7549e9941b6 100644 --- a/spec/lib/gitlab/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -217,11 +217,58 @@ describe Gitlab::Ci::Ansi2html do "#{section_end[0...-5]}</div>" end - it "prints light red" do - text = "#{section_start}\e[91mHello\e[0m\n#{section_end}" - html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}} + shared_examples 'forbidden char in section_name' do + it 'ignores sections' do + text = "#{section_start}Some text#{section_end}" + html = text.gsub("\033[0K", '').gsub('<', '<') - expect(convert_html(text)).to eq(html) + expect(convert_html(text)).to eq(html) + end + end + + shared_examples 'a legit section' do + let(:text) { "#{section_start}Some text#{section_end}" } + + it 'prints light red' do + text = "#{section_start}\e[91mHello\e[0m\n#{section_end}" + html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}} + + expect(convert_html(text)).to eq(html) + end + + it 'begins with a section_start html marker' do + expect(convert_html(text)).to start_with(section_start_html) + end + + it 'ends with a section_end html marker' do + expect(convert_html(text)).to end_with(section_end_html) + end + end + + it_behaves_like 'a legit section' + + context 'section name includes $' do + let(:section_name) { 'my_$ection'} + + it_behaves_like 'forbidden char in section_name' + end + + context 'section name includes <' do + let(:section_name) { '<a_tag>'} + + it_behaves_like 'forbidden char in section_name' + end + + context 'section name contains .-_' do + let(:section_name) { 'a.Legit-SeCtIoN_namE' } + + it_behaves_like 'a legit section' + end + + it 'do not allow XSS injections' do + text = "#{section_start}section_end:1:2<script>alert('XSS Hack!');</script>#{section_end}" + + expect(convert_html(text)).not_to include('<script>') end end diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb index 5d4de60bc8a..3cbf19bea8b 100644 --- a/spec/lib/gitlab/ci/config/entry/key_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb @@ -4,6 +4,26 @@ describe Gitlab::Ci::Config::Entry::Key do let(:entry) { described_class.new(config) } describe 'validations' do + shared_examples 'key with slash' do + it 'is invalid' do + expect(entry).not_to be_valid + end + + it 'reports errors with config value' do + expect(entry.errors).to include 'key config cannot contain the "/" character' + end + end + + shared_examples 'key with only dots' do + it 'is invalid' do + expect(entry).not_to be_valid + end + + it 'reports errors with config value' do + expect(entry.errors).to include 'key config cannot be "." or ".."' + end + end + context 'when entry config value is correct' do let(:config) { 'test' } @@ -30,6 +50,48 @@ describe Gitlab::Ci::Config::Entry::Key do end end end + + context 'when entry value contains slash' do + let(:config) { 'key/with/some/slashes' } + + it_behaves_like 'key with slash' + end + + context 'when entry value contains URI encoded slash (%2F)' do + let(:config) { 'key%2Fwith%2Fsome%2Fslashes' } + + it_behaves_like 'key with slash' + end + + context 'when entry value is a dot' do + let(:config) { '.' } + + it_behaves_like 'key with only dots' + end + + context 'when entry value is two dots' do + let(:config) { '..' } + + it_behaves_like 'key with only dots' + end + + context 'when entry value is a URI encoded dot (%2E)' do + let(:config) { '%2e' } + + it_behaves_like 'key with only dots' + end + + context 'when entry value is two URI encoded dots (%2E)' do + let(:config) { '%2E%2e' } + + it_behaves_like 'key with only dots' + end + + context 'when entry value is one dot and one URI encoded dot' do + let(:config) { '.%2e' } + + it_behaves_like 'key with only dots' + end end describe '.default' do diff --git a/spec/lib/gitlab/ci/status/build/action_spec.rb b/spec/lib/gitlab/ci/status/build/action_spec.rb index 8c25f72804b..d612d29e3e0 100644 --- a/spec/lib/gitlab/ci/status/build/action_spec.rb +++ b/spec/lib/gitlab/ci/status/build/action_spec.rb @@ -37,16 +37,16 @@ describe Gitlab::Ci::Status::Build::Action do describe '.matches?' do subject { described_class.matches?(build, user) } - context 'when build is an action' do - let(:build) { create(:ci_build, :manual) } + context 'when build is playable action' do + let(:build) { create(:ci_build, :playable) } it 'is a correct match' do expect(subject).to be true end end - context 'when build is not manual' do - let(:build) { create(:ci_build) } + context 'when build is not playable action' do + let(:build) { create(:ci_build, :non_playable) } it 'does not match' do expect(subject).to be false diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 6a07a3ca8b8..85e6efd7ca2 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -388,6 +388,84 @@ describe Gitlab::Git::Commit, seed_helper: true do end end end + + describe '.extract_signature' do + subject { described_class.extract_signature(repository, commit_id) } + + shared_examples '.extract_signature' do + context 'when the commit is signed' do + let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + + it 'returns signature and signed text' do + signature, signed_text = subject + + expected_signature = <<~SIGNATURE + -----BEGIN PGP SIGNATURE----- + Version: GnuPG/MacGPG2 v2.0.22 (Darwin) + Comment: GPGTools - https://gpgtools.org + + iQEcBAABCgAGBQJTDvaZAAoJEGJ8X1ifRn8XfvYIAMuB0yrbTGo1BnOSoDfyrjb0 + Kw2EyUzvXYL72B63HMdJ+/0tlSDC6zONF3fc+bBD8z+WjQMTbwFNMRbSSy2rKEh+ + mdRybOP3xBIMGgEph0/kmWln39nmFQBsPRbZBWoU10VfI/ieJdEOgOphszgryRar + TyS73dLBGE9y9NIININVaNISet9D9QeXFqc761CGjh4YIghvPpi+YihMWapGka6v + hgKhX+hc5rj+7IEE0CXmlbYR8OYvAbAArc5vJD7UTxAY4Z7/l9d6Ydt9GQ25khfy + ANFgltYzlR6evLFmDjssiP/mx/ZMN91AL0ueJ9nNGv411Mu2CUW+tDCaQf35mdc= + =j51i + -----END PGP SIGNATURE----- + SIGNATURE + + expect(signature).to eq(expected_signature.chomp) + expect(signature).to be_a_binary_string + + expected_signed_text = <<~SIGNED_TEXT + tree 22bfa2fbd217df24731f43ff43a4a0f8db759dae + parent ae73cb07c9eeaf35924a10f713b364d32b2dd34f + author Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + committer Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + + Feature added + + Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> + SIGNED_TEXT + + expect(signed_text).to eq(expected_signed_text) + expect(signed_text).to be_a_binary_string + end + end + + context 'when the commit has no signature' do + let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'when the commit cannot be found' do + let(:commit_id) { Gitlab::Git::BLANK_SHA } + + it 'returns nil' do + expect(subject).to be_nil + end + end + + context 'when the commit ID is invalid' do + let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e' } + + it 'raises ArgumentError' do + expect { subject }.to raise_error(ArgumentError) + end + end + end + + context 'with gitaly' do + it_behaves_like '.extract_signature' + end + + context 'without gitaly', :skip_gitaly_mock do + it_behaves_like '.extract_signature' + end + end end describe '#init_from_rugged' do diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb index beef843537d..78e4fbca28e 100644 --- a/spec/lib/gitlab/git/gitlab_projects_spec.rb +++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb @@ -158,39 +158,55 @@ describe Gitlab::Git::GitlabProjects do subject { gl_projects.import_project(import_url, timeout) } - context 'success import' do - it 'imports a repo' do - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy + shared_examples 'importing repository' do + context 'success import' do + it 'imports a repo' do + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy - message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>." - expect(logger).to receive(:info).with(message) + is_expected.to be_truthy - is_expected.to be_truthy + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_truthy + end + end - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_truthy + context 'already exists' do + it "doesn't import" do + FileUtils.mkdir_p(tmp_repo_path) + + is_expected.to be_falsy + end end end - context 'already exists' do - it "doesn't import" do - FileUtils.mkdir_p(tmp_repo_path) + context 'when Gitaly import_repository feature is enabled' do + it_behaves_like 'importing repository' + end + + context 'when Gitaly import_repository feature is disabled', :disable_gitaly do + describe 'logging' do + it 'imports a repo' do + message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>." + expect(logger).to receive(:info).with(message) - is_expected.to be_falsy + subject + end end - end - context 'timeout' do - it 'does not import a repo' do - stub_spawn_timeout(cmd, timeout, nil) + context 'timeout' do + it 'does not import a repo' do + stub_spawn_timeout(cmd, timeout, nil) - message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed." - expect(logger).to receive(:error).with(message) + message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed." + expect(logger).to receive(:error).with(message) - is_expected.to be_falsy + is_expected.to be_falsy - expect(gl_projects.output).to eq("Timed out\n") - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy + expect(gl_projects.output).to eq("Timed out\n") + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy + end end + + it_behaves_like 'importing repository' end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index f4e781c599e..aec7cde6df8 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -899,44 +899,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - context "compare results between log_by_walk and log_by_shell" do - let(:options) { { ref: "master" } } - let(:commits_by_walk) { repository.log(options).map(&:id) } - let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:id) } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - - context "with limit" do - let(:options) { { ref: "master", limit: 1 } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - end - - context "with offset" do - let(:options) { { ref: "master", offset: 1 } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - end - - context "with skip_merges" do - let(:options) { { ref: "master", skip_merges: true } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - end - - context "with path" do - let(:options) { { ref: "master", path: "encoding" } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - - context "with follow" do - let(:options) { { ref: "master", path: "encoding", follow: true } } - - it { expect(commits_by_walk).to eq(commits_by_shell) } - end - end - end - context "where provides 'after' timestamp" do options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } @@ -1104,14 +1066,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "branch_names_contains" do - subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) } - - it { is_expected.to include('master') } - it { is_expected.not_to include('feature') } - it { is_expected.not_to include('fix') } - end - describe '#autocrlf' do before(:all) do @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index eaf74951b0e..90fbef9d248 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -39,7 +39,7 @@ describe Gitlab::Git::RevList do ] expect(rev_list).to receive(:popen).with(*params) do |*_, lazy_block:| - lazy_block.call(output.split("\n").lazy) + lazy_block.call(output.lines.lazy.map(&:chomp)) end end @@ -64,6 +64,15 @@ describe Gitlab::Git::RevList do expect(rev_list.new_objects(require_path: true)).to eq(%w[sha2]) end + it 'can handle non utf-8 paths' do + non_utf_char = [0x89].pack("c*").force_encoding("UTF-8") + stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1") + + rev_list.new_objects(require_path: true) do |object_ids| + expect(object_ids.force).to eq(%w[sha2]) + end + end + it 'can yield a lazy enumerator' do stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 4290fbb0087..2009a8ac48c 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -51,12 +51,12 @@ describe Gitlab::GitAccess do context 'when the project exists' do context 'when actor exists' do context 'when actor is a DeployKey' do - let(:deploy_key) { create(:deploy_key, user: user, can_push: true) } + let(:deploy_key) { create(:deploy_key, user: user) } let(:actor) { deploy_key } context 'when the DeployKey has access to the project' do before do - deploy_key.projects << project + deploy_key.deploy_keys_projects.create(project: project, can_push: true) end it 'allows push and pull access' do @@ -696,15 +696,13 @@ describe Gitlab::GitAccess do end describe 'deploy key permissions' do - let(:key) { create(:deploy_key, user: user, can_push: can_push) } + let(:key) { create(:deploy_key, user: user) } let(:actor) { key } context 'when deploy_key can push' do - let(:can_push) { true } - context 'when project is authorized' do before do - key.projects << project + key.deploy_keys_projects.create(project: project, can_push: true) end it { expect { push_access_check }.not_to raise_error } @@ -732,11 +730,9 @@ describe Gitlab::GitAccess do end context 'when deploy_key cannot push' do - let(:can_push) { false } - context 'when project is authorized' do before do - key.projects << project + key.deploy_keys_projects.create(project: project, can_push: false) end it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) } diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index a6c99bc07d4..e3bf2801406 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -38,8 +38,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, @@ -77,8 +77,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User3.signed_commit_signature, @@ -116,8 +116,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, @@ -151,8 +151,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, @@ -187,8 +187,8 @@ describe Gitlab::Gpg::Commit do end before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, @@ -217,8 +217,8 @@ describe Gitlab::Gpg::Commit do let!(:commit) { create :commit, project: project, sha: commit_sha } before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return( [ GpgHelpers::User1.signed_commit_signature, diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index d6000af0ecd..c034eccf2a6 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -26,8 +26,8 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do before do allow_any_instance_of(Project).to receive(:commit).and_return(commit) - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) + allow(Gitlab::Git::Commit).to receive(:extract_signature) + .with(Gitlab::Git::Repository, commit_sha) .and_return(signature) end diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb index 162b776e107..5cdc5138fda 100644 --- a/spec/lib/gitlab/import_export/file_importer_spec.rb +++ b/spec/lib/gitlab/import_export/file_importer_spec.rb @@ -12,30 +12,61 @@ describe Gitlab::ImportExport::FileImporter do stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true) - + allow(SecureRandom).to receive(:hex).and_return('abcd') setup_files - - described_class.import(archive_file: '', shared: shared) end after do FileUtils.rm_rf(export_path) end - it 'removes symlinks in root folder' do - expect(File.exist?(symlink_file)).to be false - end + context 'normal run' do + before do + described_class.import(archive_file: '', shared: shared) + end - it 'removes hidden symlinks in root folder' do - expect(File.exist?(hidden_symlink_file)).to be false - end + it 'removes symlinks in root folder' do + expect(File.exist?(symlink_file)).to be false + end + + it 'removes hidden symlinks in root folder' do + expect(File.exist?(hidden_symlink_file)).to be false + end + + it 'removes symlinks in subfolders' do + expect(File.exist?(subfolder_symlink_file)).to be false + end - it 'removes symlinks in subfolders' do - expect(File.exist?(subfolder_symlink_file)).to be false + it 'does not remove a valid file' do + expect(File.exist?(valid_file)).to be true + end + + it 'creates the file in the right subfolder' do + expect(shared.export_path).to include('test/abcd') + end end - it 'does not remove a valid file' do - expect(File.exist?(valid_file)).to be true + context 'error' do + before do + allow_any_instance_of(described_class).to receive(:wait_for_archived_file).and_raise(StandardError) + described_class.import(archive_file: '', shared: shared) + end + + it 'removes symlinks in root folder' do + expect(File.exist?(symlink_file)).to be false + end + + it 'removes hidden symlinks in root folder' do + expect(File.exist?(hidden_symlink_file)).to be false + end + + it 'removes symlinks in subfolders' do + expect(File.exist?(subfolder_symlink_file)).to be false + end + + it 'does not remove a valid file' do + expect(File.exist?(valid_file)).to be true + end end def setup_files diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index 4afe48e72ad..63997a40d52 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -100,6 +100,25 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do is_expected.to eq(command) end end + + context 'when chart values file is present' do + let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) } + let(:command) do + <<~MSG.chomp + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + + helm init --client-only >/dev/null + helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} -f /data/helm/#{prometheus.name}/config/values.yaml >/dev/null + MSG + end + + it 'should return appropriate command' do + is_expected.to eq(command) + end + end end describe "#pod_name" do diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 906b10b96d4..0b8e97b8948 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -52,18 +52,20 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'should include volumes for the container' do container = subject.generate.spec.containers.first - expect(container.volumeMounts.first['name']).to eq('config-volume') - expect(container.volumeMounts.first['mountPath']).to eq('/etc/config') + expect(container.volumeMounts.first['name']).to eq('configuration-volume') + expect(container.volumeMounts.first['mountPath']).to eq("/data/helm/#{app.name}/config") end it 'should include a volume inside the specification' do spec = subject.generate.spec - expect(spec.volumes.first['name']).to eq('config-volume') + expect(spec.volumes.first['name']).to eq('configuration-volume') end it 'should mount configMap specification in the volume' do spec = subject.generate.spec - expect(spec.volumes.first.configMap['name']).to eq('values-config') + expect(spec.volumes.first.configMap['name']).to eq('values-content-configuration') + expect(spec.volumes.first.configMap['items'].first['key']).to eq('values') + expect(spec.volumes.first.configMap['items'].first['path']).to eq('values.yaml') end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 17937726f2c..1ebb0105cf5 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -70,15 +70,6 @@ describe Gitlab::ProjectSearchResults do subject { described_class.parse_search_result(search_result) } - it 'can correctly parse filenames including ":"' do - special_char_result = "\nmaster:testdata/project::function1.yaml-1----\nmaster:testdata/project::function1.yaml:2:test: data1\n" - - blob = described_class.parse_search_result(special_char_result) - - expect(blob.ref).to eq('master') - expect(blob.filename).to eq('testdata/project::function1.yaml') - end - it "returns a valid FoundBlob" do is_expected.to be_an Gitlab::SearchResults::FoundBlob expect(subject.id).to be_nil @@ -90,8 +81,32 @@ describe Gitlab::ProjectSearchResults do expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") end + context 'when the matching filename contains a colon' do + let(:search_result) { "\nmaster:testdata/project::function1.yaml\x001\x00---\n" } + + it 'returns a valid FoundBlob' do + expect(subject.filename).to eq('testdata/project::function1.yaml') + expect(subject.basename).to eq('testdata/project::function1') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq('---') + end + end + + context 'when the matching content contains a number surrounded by colons' do + let(:search_result) { "\nmaster:testdata/foo.txt\x001\x00blah:9:blah" } + + it 'returns a valid FoundBlob' do + expect(subject.filename).to eq('testdata/foo.txt') + expect(subject.basename).to eq('testdata/foo') + expect(subject.ref).to eq('master') + expect(subject.startline).to eq(1) + expect(subject.data).to eq('blah:9:blah') + end + end + context "when filename has extension" do - let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" } + let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" } it { expect(subject.path).to eq('CONTRIBUTE.md') } it { expect(subject.filename).to eq('CONTRIBUTE.md') } @@ -99,7 +114,7 @@ describe Gitlab::ProjectSearchResults do end context "when file under directory" do - let(:search_result) { "master:a/b/c.md:5:a b c\n" } + let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" } it { expect(subject.path).to eq('a/b/c.md') } it { expect(subject.filename).to eq('a/b/c.md') } @@ -144,7 +159,7 @@ describe Gitlab::ProjectSearchResults do end it 'finds by content' do - expect(results).to include("master:Title.md:1:Content\n") + expect(results).to include("master:Title.md\x001\x00Content\n") end end diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb new file mode 100644 index 00000000000..7c97cee982a --- /dev/null +++ b/spec/lib/gitlab/utils/override_spec.rb @@ -0,0 +1,158 @@ +require 'spec_helper' + +describe Gitlab::Utils::Override do + let(:base) { Struct.new(:good) } + + let(:derived) { Class.new(base).tap { |m| m.extend described_class } } + let(:extension) { Module.new.tap { |m| m.extend described_class } } + + let(:prepending_class) { base.tap { |m| m.prepend extension } } + let(:including_class) { base.tap { |m| m.include extension } } + + let(:klass) { subject } + + def good(mod) + mod.module_eval do + override :good + def good + super.succ + end + end + + mod + end + + def bad(mod) + mod.module_eval do + override :bad + def bad + true + end + end + + mod + end + + shared_examples 'checking as intended' do + it 'checks ok for overriding method' do + good(subject) + result = klass.new(0).good + + expect(result).to eq(1) + described_class.verify! + end + + it 'raises NotImplementedError when it is not overriding anything' do + expect do + bad(subject) + klass.new(0).bad + described_class.verify! + end.to raise_error(NotImplementedError) + end + end + + shared_examples 'nothing happened' do + it 'does not complain when it is overriding something' do + good(subject) + result = klass.new(0).good + + expect(result).to eq(1) + described_class.verify! + end + + it 'does not complain when it is not overriding anything' do + bad(subject) + result = klass.new(0).bad + + expect(result).to eq(true) + described_class.verify! + end + end + + before do + # Make sure we're not touching the internal cache + allow(described_class).to receive(:extensions).and_return({}) + end + + describe '#override' do + context 'when STATIC_VERIFICATION is set' do + before do + stub_env('STATIC_VERIFICATION', 'true') + end + + context 'when subject is a class' do + subject { derived } + + it_behaves_like 'checking as intended' + end + + context 'when subject is a module, and class is prepending it' do + subject { extension } + let(:klass) { prepending_class } + + it_behaves_like 'checking as intended' + end + + context 'when subject is a module, and class is including it' do + subject { extension } + let(:klass) { including_class } + + it 'raises NotImplementedError because it is not overriding it' do + expect do + good(subject) + klass.new(0).good + described_class.verify! + end.to raise_error(NotImplementedError) + end + + it 'raises NotImplementedError when it is not overriding anything' do + expect do + bad(subject) + klass.new(0).bad + described_class.verify! + end.to raise_error(NotImplementedError) + end + end + end + end + + context 'when STATIC_VERIFICATION is not set' do + before do + stub_env('STATIC_VERIFICATION', nil) + end + + context 'when subject is a class' do + subject { derived } + + it_behaves_like 'nothing happened' + end + + context 'when subject is a module, and class is prepending it' do + subject { extension } + let(:klass) { prepending_class } + + it_behaves_like 'nothing happened' + end + + context 'when subject is a module, and class is including it' do + subject { extension } + let(:klass) { including_class } + + it 'does not complain when it is overriding something' do + good(subject) + result = klass.new(0).good + + expect(result).to eq(0) + described_class.verify! + end + + it 'does not complain when it is not overriding anything' do + bad(subject) + result = klass.new(0).bad + + expect(result).to eq(true) + described_class.verify! + end + end + end +end diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index e872a5290c5..bda239b7871 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -17,6 +17,22 @@ describe Gitlab::Utils do end end + describe '.remove_line_breaks' do + using RSpec::Parameterized::TableSyntax + + where(:original, :expected) do + "foo\nbar\nbaz" | "foobarbaz" + "foo\r\nbar\r\nbaz" | "foobarbaz" + "foobar" | "foobar" + end + + with_them do + it "replace line breaks with an empty string" do + expect(described_class.remove_line_breaks(original)).to eq(expected) + end + end + end + describe '.to_boolean' do it 'accepts booleans' do expect(to_boolean(true)).to be(true) diff --git a/spec/migrations/fix_wrongly_renamed_routes_spec.rb b/spec/migrations/fix_wrongly_renamed_routes_spec.rb index 5ef10b92a3a..78f8ab7512d 100644 --- a/spec/migrations/fix_wrongly_renamed_routes_spec.rb +++ b/spec/migrations/fix_wrongly_renamed_routes_spec.rb @@ -1,29 +1,35 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170518231126_fix_wrongly_renamed_routes.rb') -describe FixWronglyRenamedRoutes, truncate: true do +describe FixWronglyRenamedRoutes, :truncate, :migration do let(:subject) { described_class.new } + let(:namespaces_table) { table(:namespaces) } + let(:projects_table) { table(:projects) } + let(:routes_table) { table(:routes) } let(:broken_namespace) do - namespace = create(:group, name: 'apiis') - namespace.route.update_attribute(:path, 'api0is') - namespace + namespaces_table.create!(name: 'apiis', path: 'apiis').tap do |namespace| + routes_table.create!(source_type: 'Namespace', source_id: namespace.id, name: 'api0is', path: 'api0is') + end end + let(:broken_namespace_route) { routes_table.where(source_type: 'Namespace', source_id: broken_namespace.id).first } describe '#wrongly_renamed' do it "includes routes that have names that don't match their namespace" do broken_namespace - _other_namespace = create(:group, name: 'api0') + other_namespace = namespaces_table.create!(name: 'api0', path: 'api0') + routes_table.create!(source_type: 'Namespace', source_id: other_namespace.id, name: 'api0', path: 'api0') expect(subject.wrongly_renamed.map(&:id)) - .to contain_exactly(broken_namespace.route.id) + .to contain_exactly(broken_namespace_route.id) end end describe "#paths_and_corrections" do it 'finds the wrong path and gets the correction from the namespace' do broken_namespace - namespace = create(:group, name: 'uploads-test') - namespace.route.update_attribute(:path, 'uploads0-test') + namespaces_table.create!(name: 'uploads-test', path: 'uploads-test').tap do |namespace| + routes_table.create!(source_type: 'Namespace', source_id: namespace.id, name: 'uploads-test', path: 'uploads0-test') + end expected_result = [ { 'namespace_path' => 'apiis', 'path' => 'api0is' }, @@ -36,38 +42,45 @@ describe FixWronglyRenamedRoutes, truncate: true do describe '#routes_in_namespace_query' do it 'includes only the required routes' do - namespace = create(:group, path: 'hello') - project = create(:project, namespace: namespace) - _other_namespace = create(:group, path: 'hello0') - - result = Route.where(subject.routes_in_namespace_query('hello')) - - expect(result).to contain_exactly(namespace.route, project.route) + namespace = namespaces_table.create!(name: 'hello', path: 'hello') + namespace_route = routes_table.create!(source_type: 'Namespace', source_id: namespace.id, name: 'hello', path: 'hello') + project = projects_table.new(name: 'my-project', path: 'my-project', namespace_id: namespace.id).tap do |project| + project.save!(validate: false) + end + routes_table.create!(source_type: 'Project', source_id: project.id, name: 'my-project', path: 'hello/my-project') + _other_namespace = namespaces_table.create!(name: 'hello0', path: 'hello0') + + result = routes_table.where(subject.routes_in_namespace_query('hello')) + project_route = routes_table.where(source_type: 'Project', source_id: project.id).first + + expect(result).to contain_exactly(namespace_route, project_route) end end describe '#up' do - let(:broken_project) do - project = create(:project, namespace: broken_namespace, path: 'broken-project') - project.route.update_attribute(:path, 'api0is/broken-project') - project - end - it 'renames incorrectly named routes' do - broken_project + broken_project = + projects_table.new(name: 'broken-project', path: 'broken-project', namespace_id: broken_namespace.id).tap do |project| + project.save!(validate: false) + routes_table.create!(source_type: 'Project', source_id: project.id, name: 'broken-project', path: 'api0is/broken-project') + end subject.up - expect(broken_project.route.reload.path).to eq('apiis/broken-project') - expect(broken_namespace.route.reload.path).to eq('apiis') + broken_project_route = routes_table.where(source_type: 'Project', source_id: broken_project.id).first + + expect(broken_project_route.path).to eq('apiis/broken-project') + expect(broken_namespace_route.reload.path).to eq('apiis') end it "doesn't touch namespaces that look like something that should be renamed" do - namespace = create(:group, path: 'api0') + namespaces_table.create!(name: 'apiis', path: 'apiis') + namespace = namespaces_table.create!(name: 'hello', path: 'api0') + namespace_route = routes_table.create!(source_type: 'Namespace', source_id: namespace.id, name: 'hello', path: 'api0') subject.up - expect(namespace.route.reload.path).to eq('api0') + expect(namespace_route.reload.path).to eq('api0') end end end diff --git a/spec/migrations/populate_can_push_from_deploy_keys_projects_spec.rb b/spec/migrations/populate_can_push_from_deploy_keys_projects_spec.rb new file mode 100644 index 00000000000..0ff98933d5c --- /dev/null +++ b/spec/migrations/populate_can_push_from_deploy_keys_projects_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20171215113714_populate_can_push_from_deploy_keys_projects.rb') + +describe PopulateCanPushFromDeployKeysProjects, :migration do + let(:migration) { described_class.new } + let(:deploy_keys) { table(:keys) } + let(:deploy_keys_projects) { table(:deploy_keys_projects) } + let(:projects) { table(:projects) } + + before do + deploy_keys.inheritance_column = nil + + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1') + (1..10).each do |index| + deploy_keys.create!(id: index, title: 'dummy', type: 'DeployKey', key: Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com') + deploy_keys_projects.create!(id: index, deploy_key_id: index, project_id: 1) + end + end + + describe '#up' do + it 'migrates can_push from deploy_keys to deploy_keys_projects' do + deploy_keys.limit(5).update_all(can_push: true) + + expected = deploy_keys.order(:id).pluck(:id, :can_push) + + migration.up + + expect(deploy_keys_projects.order(:id).pluck(:deploy_key_id, :can_push)).to eq expected + end + end + + describe '#down' do + it 'migrates can_push from deploy_keys_projects to deploy_keys' do + deploy_keys_projects.limit(5).update_all(can_push: true) + + expected = deploy_keys_projects.order(:id).pluck(:deploy_key_id, :can_push) + + migration.down + + expect(deploy_keys.order(:id).pluck(:id, :can_push)).to eq expected + end + end +end diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb index d4a1553fb0e..984b428a020 100644 --- a/spec/migrations/update_upload_paths_to_system_spec.rb +++ b/spec/migrations/update_upload_paths_to_system_spec.rb @@ -1,53 +1,59 @@ -require "spec_helper" -require Rails.root.join("db", "post_migrate", "20170317162059_update_upload_paths_to_system.rb") +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170317162059_update_upload_paths_to_system.rb') -describe UpdateUploadPathsToSystem do +describe UpdateUploadPathsToSystem, :migration do let(:migration) { described_class.new } + let(:uploads_table) { table(:uploads) } + let(:base_upload_attributes) { { size: 42, uploader: 'John Doe' } } before do allow(migration).to receive(:say) end - describe "#uploads_to_switch_to_new_path" do - it "contains only uploads with the old path for the correct models" do - _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") - _upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") - _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") - old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") - group_upload = create(:upload, model: create(:group), path: "uploads/group/avatar.jpg") + describe '#uploads_to_switch_to_new_path' do + it 'contains only uploads with the old path for the correct models' do + _upload_for_other_type = create_upload('Pipeline', 'uploads/ci_pipeline/avatar.jpg') + _upload_with_system_path = create_upload('Project', 'uploads/-/system/project/avatar.jpg') + _upload_with_other_path = create_upload('Project', 'thelongsecretforafileupload/avatar.jpg') + old_upload = create_upload('Project', 'uploads/project/avatar.jpg') + group_upload = create_upload('Namespace', 'uploads/group/avatar.jpg') - expect(Upload.where(migration.uploads_to_switch_to_new_path)).to contain_exactly(old_upload, group_upload) + expect(uploads_table.where(migration.uploads_to_switch_to_new_path)).to contain_exactly(old_upload, group_upload) end end - describe "#uploads_to_switch_to_old_path" do - it "contains only uploads with the new path for the correct models" do - _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") - upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") - _upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg") - _old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") + describe '#uploads_to_switch_to_old_path' do + it 'contains only uploads with the new path for the correct models' do + _upload_for_other_type = create_upload('Pipeline', 'uploads/ci_pipeline/avatar.jpg') + upload_with_system_path = create_upload('Project', 'uploads/-/system/project/avatar.jpg') + _upload_with_other_path = create_upload('Project', 'thelongsecretforafileupload/avatar.jpg') + _old_upload = create_upload('Project', 'uploads/project/avatar.jpg') - expect(Upload.where(migration.uploads_to_switch_to_old_path)).to contain_exactly(upload_with_system_path) + expect(uploads_table.where(migration.uploads_to_switch_to_old_path)).to contain_exactly(upload_with_system_path) end end - describe "#up", :truncate do - it "updates old upload records to the new path" do - old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") + describe '#up' do + it 'updates old upload records to the new path' do + old_upload = create_upload('Project', 'uploads/project/avatar.jpg') migration.up - expect(old_upload.reload.path).to eq("uploads/-/system/project/avatar.jpg") + expect(old_upload.reload.path).to eq('uploads/-/system/project/avatar.jpg') end end - describe "#down", :truncate do - it "updates the new system patsh to the old paths" do - new_upload = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") + describe '#down' do + it 'updates the new system patsh to the old paths' do + new_upload = create_upload('Project', 'uploads/-/system/project/avatar.jpg') migration.down - expect(new_upload.reload.path).to eq("uploads/project/avatar.jpg") + expect(new_upload.reload.path).to eq('uploads/project/avatar.jpg') end end + + def create_upload(type, path) + uploads_table.create(base_upload_attributes.merge(model_type: type, path: path)) + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 3eaeeebf97d..45a606c1ea8 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -25,6 +25,13 @@ describe Ci::Build do it { is_expected.to be_a(ArtifactMigratable) } + describe 'associations' do + it 'has a bidirectional relationship with projects' do + expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:builds) + expect(Project.reflect_on_association(:builds).has_inverse?).to eq(:project) + end + end + describe 'callbacks' do context 'when running after_create callback' do it 'triggers asynchronous build hooks worker' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 7bef798a782..14d234f6aab 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -28,6 +28,13 @@ describe Ci::Pipeline, :mailer do it { is_expected.to respond_to :short_sha } it { is_expected.to delegate_method(:full_path).to(:project).with_prefix } + describe 'associations' do + it 'has a bidirectional relationship with projects' do + expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:pipelines) + expect(Project.reflect_on_association(:pipelines).has_inverse?).to eq(:project) + end + end + describe '#source' do context 'when creating new pipeline' do let(:pipeline) do diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index d3826417762..f8a98b43e46 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -439,15 +439,25 @@ eos end describe '#uri_type' do - it 'returns the URI type at the given path' do - expect(commit.uri_type('files/html')).to be(:tree) - expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) - expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw) - expect(commit.uri_type('files/js/application.js')).to be(:blob) + shared_examples 'URI type' do + it 'returns the URI type at the given path' do + expect(commit.uri_type('files/html')).to be(:tree) + expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) + expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw) + expect(commit.uri_type('files/js/application.js')).to be(:blob) + end + + it "returns nil if the path doesn't exists" do + expect(commit.uri_type('this/path/doesnt/exist')).to be_nil + end + end + + context 'when Gitaly commit_tree_entry feature is enabled' do + it_behaves_like 'URI type' end - it "returns nil if the path doesn't exists" do - expect(commit.uri_type('this/path/doesnt/exist')).to be_nil + context 'when Gitaly commit_tree_entry feature is disabled', :disable_gitaly do + it_behaves_like 'URI type' end end diff --git a/spec/models/concerns/triggerable_hooks_spec.rb b/spec/models/concerns/triggerable_hooks_spec.rb new file mode 100644 index 00000000000..621d2d38eae --- /dev/null +++ b/spec/models/concerns/triggerable_hooks_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +RSpec.describe TriggerableHooks do + before do + class TestableHook < WebHook + include TriggerableHooks + triggerable_hooks [:push_hooks] + end + end + + describe 'scopes' do + it 'defines a scope for each of the requested triggers' do + expect(TestableHook).to respond_to :push_hooks + expect(TestableHook).not_to respond_to :tag_push_hooks + end + end + + describe '.hooks_for' do + context 'the model has the required trigger scope' do + it 'returns the record' do + hook = TestableHook.create!(url: 'http://example.com', push_events: true) + + expect(TestableHook.hooks_for(:push_hooks)).to eq [hook] + end + end + + context 'the model does not have the required trigger scope' do + it 'returns an empty relation' do + TestableHook.create!(url: 'http://example.com') + + expect(TestableHook.hooks_for(:tag_push_hooks)).to eq [] + end + end + + context 'the stock scope ".all" is accepted' do + it 'returns the record' do + hook = TestableHook.create!(url: 'http://example.com') + + expect(TestableHook.hooks_for(:all)).to eq [hook] + end + end + end +end diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index 0345fefb254..fca3090ff4a 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -8,7 +8,7 @@ describe DeployKeysProject do describe "Validation" do it { is_expected.to validate_presence_of(:project_id) } - it { is_expected.to validate_presence_of(:deploy_key_id) } + it { is_expected.to validate_presence_of(:deploy_key) } end describe "Destroying" do diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 0e965f541d8..8bc45715dcd 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -7,7 +7,8 @@ describe SystemHook do it 'sets defined default parameters' do attrs = { push_events: false, - repository_update_events: true + repository_update_events: true, + merge_requests_events: false } expect(system_hook).to have_attributes(attrs) end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 388120160ab..ea6d6e53ef5 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -29,6 +29,12 @@ describe WebHook do expect(hook.url).to eq('https://example.com') end end + + describe 'token' do + it { is_expected.to allow_value("foobar").for(:token) } + + it { is_expected.not_to allow_values("foo\nbar", "foo\r\nbar").for(:token) } + end end describe 'execute' do diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb index 6a5d0decfec..733086e258f 100644 --- a/spec/models/project_services/microsoft_teams_service_spec.rb +++ b/spec/models/project_services/microsoft_teams_service_spec.rb @@ -92,6 +92,10 @@ describe MicrosoftTeamsService do service.hook_data(merge_request, 'open') end + before do + project.add_developer(user) + end + it "calls Microsoft Teams API" do chat_service.execute(merge_sample_data) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 78223c44999..987be8e8b46 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3206,4 +3206,23 @@ describe Project do expect { project.write_repository_config }.not_to raise_error end end + + describe '#execute_hooks' do + it 'executes the projects hooks with the specified scope' do + hook1 = create(:project_hook, merge_requests_events: true, tag_push_events: false) + hook2 = create(:project_hook, merge_requests_events: false, tag_push_events: true) + project = create(:project, hooks: [hook1, hook2]) + + expect_any_instance_of(ProjectHook).to receive(:async_execute).once + + project.execute_hooks({}, :tag_push_hooks) + end + + it 'executes the system hooks with the specified scope' do + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with({ data: 'data' }, :merge_request_hooks) + + project = build(:project) + project.execute_hooks({ data: 'data' }, :merge_request_hooks) + end + end end diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb index ad3c3a406d9..bfe7a30b96a 100644 --- a/spec/models/push_event_spec.rb +++ b/spec/models/push_event_spec.rb @@ -63,12 +63,14 @@ describe PushEvent do let(:event2) { create(:push_event, project: project) } let(:event3) { create(:push_event, project: project) } let(:event4) { create(:push_event, project: project) } + let(:event5) { create(:push_event, project: project) } before do create(:push_event_payload, event: event1, ref: 'foo', action: :created) create(:push_event_payload, event: event2, ref: 'bar', action: :created) - create(:push_event_payload, event: event3, ref: 'baz', action: :removed) - create(:push_event_payload, event: event4, ref: 'baz', ref_type: :tag) + create(:push_event_payload, event: event3, ref: 'qux', action: :created) + create(:push_event_payload, event: event4, ref: 'baz', action: :removed) + create(:push_event_payload, event: event5, ref: 'baz', ref_type: :tag) project.repository.create_branch('bar', 'master') @@ -78,6 +80,16 @@ describe PushEvent do target_project: project, source_branch: 'bar' ) + + project.repository.create_branch('qux', 'master') + + create( + :merge_request, + :closed, + source_project: project, + target_project: project, + source_branch: 'qux' + ) end let(:relation) { described_class.without_existing_merge_requests } @@ -86,16 +98,20 @@ describe PushEvent do expect(relation).to include(event1) end - it 'does not include events that have a corresponding merge request' do + it 'does not include events that have a corresponding open merge request' do expect(relation).not_to include(event2) end + it 'includes events that has corresponding closed/merged merge requests' do + expect(relation).to include(event3) + end + it 'does not include events for removed refs' do - expect(relation).not_to include(event3) + expect(relation).not_to include(event4) end it 'does not include events for pushing to tags' do - expect(relation).not_to include(event4) + expect(relation).not_to include(event5) end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f3456e5b354..baaa9e3ef44 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -657,7 +657,7 @@ describe Repository do subject { results.first } it { is_expected.to be_an String } - it { expect(subject.lines[2]).to eq("master:CHANGELOG:190: - Feature: Replace teams with group membership\n") } + it { expect(subject.lines[2]).to eq("master:CHANGELOG\x00190\x00 - Feature: Replace teams with group membership\n") } end end @@ -668,6 +668,18 @@ describe Repository do expect(results.first).to eq('files/html/500.html') end + it 'ignores leading slashes' do + results = repository.search_files_by_name('/files', 'master') + + expect(results.first).to eq('files/html/500.html') + end + + it 'properly handles when query is only slashes' do + results = repository.search_files_by_name('//', 'master') + + expect(results).to match_array([]) + end + it 'properly handles when query is not present' do results = repository.search_files_by_name('', 'master') diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index ab6678cab38..79f25dc4360 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -280,4 +280,38 @@ describe Service do expect(KubernetesService.find_by_template).to eq(kubernetes_service) end end + + describe '#api_field_names' do + let(:fake_service) do + Class.new(Service) do + def fields + [ + { name: 'token' }, + { name: 'api_token' }, + { name: 'key' }, + { name: 'api_key' }, + { name: 'password' }, + { name: 'password_field' }, + { name: 'safe_field' } + ] + end + end + end + + let(:service) do + fake_service.new(properties: [ + { token: 'token-value' }, + { api_token: 'api_token-value' }, + { key: 'key-value' }, + { api_key: 'api_key-value' }, + { password: 'password-value' }, + { password_field: 'password_field-value' }, + { safe_field: 'safe_field-value' } + ]) + end + + it 'filters out sensitive fields' do + expect(service.api_field_names).to eq(['safe_field']) + end + end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 1f1e6ea17e4..0772b3f2e64 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -110,7 +110,7 @@ describe API::DeployKeys do end it 'accepts can_push parameter' do - key_attrs = attributes_for :write_access_key + key_attrs = attributes_for(:another_key).merge(can_push: true) post api("/projects/#{project.id}/deploy_keys", admin), key_attrs @@ -160,16 +160,6 @@ describe API::DeployKeys do expect(json_response['title']).to eq('new title') expect(json_response['can_push']).to eq(true) end - - it 'updates a private ssh key from projects user has access with correct attributes' do - create(:deploy_keys_project, project: project2, deploy_key: private_deploy_key) - - put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: 'new title', can_push: true } - - expect(json_response['id']).to eq(private_deploy_key.id) - expect(json_response['title']).to eq('new title') - expect(json_response['can_push']).to eq(true) - end end describe 'DELETE /projects/:id/deploy_keys/:key_id' do diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 6732c99e329..51b70fda148 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -3,24 +3,65 @@ require 'spec_helper' describe API::Deployments do let(:user) { create(:user) } let(:non_member) { create(:user) } - let(:project) { deployment.environment.project } - let!(:deployment) { create(:deployment) } before do project.add_master(user) end describe 'GET /projects/:id/deployments' do + let(:project) { create(:project) } + let!(:deployment_1) { create(:deployment, project: project, iid: 11, ref: 'master', created_at: Time.now) } + let!(:deployment_2) { create(:deployment, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago) } + let!(:deployment_3) { create(:deployment, project: project, iid: 8, ref: 'feature', created_at: 2.days.ago) } + context 'as member of the project' do - it 'returns projects deployments' do + it 'returns projects deployments sorted by id asc' do get api("/projects/#{project.id}/deployments", user) expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.size).to eq(1) - expect(json_response.first['iid']).to eq(deployment.iid) + expect(json_response.size).to eq(3) + expect(json_response.first['iid']).to eq(deployment_1.iid) expect(json_response.first['sha']).to match /\A\h{40}\z/ + expect(json_response.second['iid']).to eq(deployment_2.iid) + expect(json_response.last['iid']).to eq(deployment_3.iid) + end + + describe 'ordering' do + using RSpec::Parameterized::TableSyntax + + let(:order_by) { nil } + let(:sort) { nil } + + subject { get api("/projects/#{project.id}/deployments?order_by=#{order_by}&sort=#{sort}", user) } + + def expect_deployments(ordered_deployments) + json_response.each_with_index do |deployment_json, index| + expect(deployment_json['id']).to eq(public_send(ordered_deployments[index]).id) + end + end + + before do + subject + end + + where(:order_by, :sort, :ordered_deployments) do + 'created_at' | 'asc' | [:deployment_3, :deployment_2, :deployment_1] + 'created_at' | 'desc' | [:deployment_1, :deployment_2, :deployment_3] + 'id' | 'asc' | [:deployment_1, :deployment_2, :deployment_3] + 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1] + 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2] + 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3] + 'ref' | 'asc' | [:deployment_2, :deployment_3, :deployment_1] + 'ref' | 'desc' | [:deployment_1, :deployment_2, :deployment_3] + end + + with_them do + it 'returns the deployments ordered' do + expect_deployments(ordered_deployments) + end + end end end @@ -34,6 +75,9 @@ describe API::Deployments do end describe 'GET /projects/:id/deployments/:deployment_id' do + let(:project) { deployment.environment.project } + let!(:deployment) { create(:deployment) } + context 'as a member of the project' do it 'returns the projects deployment' do get api("/projects/#{project.id}/deployments/#{deployment.id}", user) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 320217f2032..43218755f4f 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -847,6 +847,15 @@ describe API::Issues, :mailer do expect(json_response['assignee']['name']).to eq(user2.name) expect(json_response['assignees'].first['name']).to eq(user2.name) end + + it 'creates a new project issue when assignee_id is empty' do + post api("/projects/#{project.id}/issues", user), + title: 'new issue', assignee_id: '' + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['assignee']).to be_nil + end end context 'single assignee restrictions' do diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 805496e4a54..e211c347266 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -25,8 +25,10 @@ describe API::Jobs do describe 'GET /projects/:id/jobs' do let(:query) { Hash.new } - before do - get api("/projects/#{project.id}/jobs", api_user), query + before do |example| + unless example.metadata[:skip_before_request] + get api("/projects/#{project.id}/jobs", api_user), query + end end context 'authorized user' do @@ -51,6 +53,23 @@ describe API::Jobs do expect(json_job['pipeline']['status']).to eq job.pipeline.status end + it 'avoids N+1 queries', skip_before_request: true do + first_build = create(:ci_build, :artifacts, pipeline: pipeline) + first_build.runner = create(:ci_runner) + first_build.user = create(:user) + first_build.save + + control_count = ActiveRecord::QueryRecorder.new { go }.count + + second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) + second_build = create(:ci_build, :artifacts, pipeline: second_pipeline) + second_build.runner = create(:ci_runner) + second_build.user = create(:user) + second_build.save + + expect { go }.not_to exceed_query_limit(control_count) + end + context 'filter project with one scope element' do let(:query) { { 'scope' => 'pending' } } @@ -83,6 +102,10 @@ describe API::Jobs do expect(response).to have_gitlab_http_status(401) end end + + def go + get api("/projects/#{project.id}/jobs", api_user), query + end end describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 4eae3e50602..8e2982f1a5d 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -754,16 +754,28 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(400) end - context 'when target_branch is specified' do + context 'when target_branch and target_project_id is specified' do + let(:params) do + { title: 'Test merge_request', + target_branch: 'master', + source_branch: 'markdown', + author: user2, + target_project_id: unrelated_project.id } + end + it 'returns 422 if targeting a different fork' do - post api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user2, - target_project_id: unrelated_project.id + unrelated_project.add_developer(user2) + + post api("/projects/#{forked_project.id}/merge_requests", user2), params + expect(response).to have_gitlab_http_status(422) end + + it 'returns 403 if targeting a different fork which user can not access' do + post api("/projects/#{forked_project.id}/merge_requests", user2), params + + expect(response).to have_gitlab_http_status(403) + end end it "returns 201 when target_branch is specified and for the same project" do diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index 08ea7314bb3..6c05c166bd6 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -14,6 +14,46 @@ describe API::ProjectMilestones do let(:route) { "/projects/#{project.id}/milestones" } end + describe 'DELETE /projects/:id/milestones/:milestone_id' do + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + + before do + project.add_reporter(reporter) + end + + it 'returns 404 response when the project does not exists' do + delete api("/projects/999/milestones/#{milestone.id}", user) + + expect(response).to have_gitlab_http_status(404) + end + + it 'returns 404 response when the milestone does not exists' do + delete api("/projects/#{project.id}/milestones/999", user) + + expect(response).to have_gitlab_http_status(404) + end + + it "returns 404 from guest user deleting a milestone" do + delete api("/projects/#{project.id}/milestones/#{milestone.id}", guest) + + expect(response).to have_gitlab_http_status(404) + end + + it "rejects a member with reporter access from deleting a milestone" do + delete api("/projects/#{project.id}/milestones/#{milestone.id}", reporter) + + expect(response).to have_gitlab_http_status(403) + end + + it 'deletes the milestone when the user has developer access to the project' do + delete api("/projects/#{project.id}/milestones/#{milestone.id}", user) + + expect(project.milestones.find_by_id(milestone.id)).to be_nil + expect(response).to have_gitlab_http_status(204) + end + end + describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do it 'creates an activity event when an milestone is closed' do expect(Event).to receive(:create!) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index e741ac4b7bd..4a2289ca137 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' describe API::ProjectSnippets do - let(:project) { create(:project, :public) } - let(:user) { create(:user) } - let(:admin) { create(:admin) } + set(:project) { create(:project, :public) } + set(:user) { create(:user) } + set(:admin) { create(:admin) } describe "GET /projects/:project_id/snippets/:id/user_agent_detail" do let(:snippet) { create(:project_snippet, :public, project: project) } @@ -18,6 +18,13 @@ describe API::ProjectSnippets do expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted) end + it 'respects project scoping' do + other_project = create(:project) + + get api("/projects/#{other_project.id}/snippets/#{snippet.id}/user_agent_detail", admin) + expect(response).to have_gitlab_http_status(404) + end + it "returns unautorized for non-admin users" do get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/user_agent_detail", user) diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 26d56c04862..236f8d7faf5 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -83,14 +83,14 @@ describe API::Services do get api("/projects/#{project.id}/services/#{dashed_service}", admin) expect(response).to have_gitlab_http_status(200) - expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map) + expect(json_response['properties'].keys).to match_array(service_instance.api_field_names) end it "returns properties of service #{service} other than passwords when authenticated as project owner" do get api("/projects/#{project.id}/services/#{dashed_service}", user) expect(response).to have_gitlab_http_status(200) - expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords) + expect(json_response['properties'].keys).to match_array(service_instance.api_field_names) end it "returns error when authenticated but not a project owner" do diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb index c7a009e999e..6c57d443cbf 100644 --- a/spec/requests/api/system_hooks_spec.rb +++ b/spec/requests/api/system_hooks_spec.rb @@ -36,6 +36,7 @@ describe API::SystemHooks do expect(json_response.first['url']).to eq(hook.url) expect(json_response.first['push_events']).to be false expect(json_response.first['tag_push_events']).to be false + expect(json_response.first['merge_requests_events']).to be false expect(json_response.first['repository_update_events']).to be true end end @@ -67,11 +68,28 @@ describe API::SystemHooks do end it 'sets default values for events' do - post api('/hooks', admin), url: 'http://mep.mep', enable_ssl_verification: true + post api('/hooks', admin), url: 'http://mep.mep' expect(response).to have_gitlab_http_status(201) expect(json_response['enable_ssl_verification']).to be true + expect(json_response['push_events']).to be false expect(json_response['tag_push_events']).to be false + expect(json_response['merge_requests_events']).to be false + end + + it 'sets explicit values for events' do + post api('/hooks', admin), + url: 'http://mep.mep', + enable_ssl_verification: false, + push_events: true, + tag_push_events: true, + merge_requests_events: true + + expect(response).to have_http_status(201) + expect(json_response['enable_ssl_verification']).to be false + expect(json_response['push_events']).to be true + expect(json_response['tag_push_events']).to be true + expect(json_response['merge_requests_events']).to be true end end diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb index a73bb456b52..cdbc5692e19 100644 --- a/spec/requests/api/v3/builds_spec.rb +++ b/spec/requests/api/v3/builds_spec.rb @@ -13,10 +13,12 @@ describe API::V3::Builds do describe 'GET /projects/:id/builds ' do let(:query) { '' } - before do + before do |example| create(:ci_build, :skipped, pipeline: pipeline) - get v3_api("/projects/#{project.id}/builds?#{query}", api_user) + unless example.metadata[:skip_before_request] + get v3_api("/projects/#{project.id}/builds?#{query}", api_user) + end end context 'authorized user' do @@ -40,6 +42,23 @@ describe API::V3::Builds do expect(json_build['pipeline']['status']).to eq build.pipeline.status end + it 'avoids N+1 queries', skip_before_request: true do + first_build = create(:ci_build, :artifacts, pipeline: pipeline) + first_build.runner = create(:ci_runner) + first_build.user = create(:user) + first_build.save + + control_count = ActiveRecord::QueryRecorder.new { go }.count + + second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) + second_build = create(:ci_build, :artifacts, pipeline: second_pipeline) + second_build.runner = create(:ci_runner) + second_build.user = create(:user) + second_build.save + + expect { go }.not_to exceed_query_limit(control_count) + end + context 'filter project with one scope element' do let(:query) { 'scope=pending' } @@ -84,6 +103,10 @@ describe API::V3::Builds do expect(response).to have_gitlab_http_status(401) end end + + def go + get v3_api("/projects/#{project.id}/builds?#{query}", api_user) + end end describe 'GET /projects/:id/repository/commits/:sha/builds' do diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb index 785bc1eb4ba..501af587ad4 100644 --- a/spec/requests/api/v3/deploy_keys_spec.rb +++ b/spec/requests/api/v3/deploy_keys_spec.rb @@ -107,7 +107,7 @@ describe API::V3::DeployKeys do end it 'accepts can_push parameter' do - key_attrs = attributes_for :write_access_key + key_attrs = attributes_for(:another_key).merge(can_push: true) post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index b8b7d9d1c40..6b748369f0d 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -371,16 +371,28 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(400) end - context 'when target_branch is specified' do + context 'when target_branch and target_project_id is specified' do + let(:params) do + { title: 'Test merge_request', + target_branch: 'master', + source_branch: 'markdown', + author: user2, + target_project_id: unrelated_project.id } + end + it 'returns 422 if targeting a different fork' do - post v3_api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user2, - target_project_id: unrelated_project.id + unrelated_project.add_developer(user2) + + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params + expect(response).to have_gitlab_http_status(422) end + + it 'returns 403 if targeting a different fork which user can not access' do + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), params + + expect(response).to have_gitlab_http_status(403) + end end it "returns 201 when target_branch is specified and for the same project" do diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 5e59bb0d585..bee918a20aa 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -781,11 +781,11 @@ describe 'Git LFS API and storage' do end context 'when deploy key has project push access' do - let(:key) { create(:deploy_key, can_push: true) } + let(:key) { create(:deploy_key) } let(:authorization) { authorize_deploy_key } let(:update_user_permissions) do - project.deploy_keys << key + project.deploy_keys_projects.create(deploy_key: key, can_push: true) end it_behaves_like 'pushes new LFS objects' diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb index d3aefa2c9eb..2bd8162d1b7 100644 --- a/spec/serializers/deploy_key_entity_spec.rb +++ b/spec/serializers/deploy_key_entity_spec.rb @@ -21,18 +21,21 @@ describe DeployKeyEntity do user_id: deploy_key.user_id, title: deploy_key.title, fingerprint: deploy_key.fingerprint, - can_push: deploy_key.can_push, destroyed_when_orphaned: true, almost_orphaned: false, created_at: deploy_key.created_at, updated_at: deploy_key.updated_at, can_edit: false, - projects: [ + deploy_keys_projects: [ { - id: project.id, - name: project.name, - full_path: project_path(project), - full_name: project.full_name + can_push: false, + project: + { + id: project.id, + name: project.name, + full_path: project_path(project), + full_name: project.full_name + } } ] } diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb index 8809b282127..aa9aba6bdff 100644 --- a/spec/services/labels/promote_service_spec.rb +++ b/spec/services/labels/promote_service_spec.rb @@ -85,6 +85,19 @@ describe Labels::PromoteService do change(project_3.labels, :count).by(-1) end + it 'keeps users\' subscriptions' do + user2 = create(:user) + project_label_1_1.subscriptions.create(user: user, subscribed: true) + project_label_2_1.subscriptions.create(user: user, subscribed: true) + project_label_3_2.subscriptions.create(user: user, subscribed: true) + project_label_2_1.subscriptions.create(user: user2, subscribed: true) + + expect { service.execute(project_label_1_1) }.to change { Subscription.count }.from(4).to(3) + + expect(new_label.subscribed?(user)).to be_truthy + expect(new_label.subscribed?(user2)).to be_truthy + end + it 'recreates priorities' do service.execute(project_label_1_1) diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index 623b182b205..75553afc033 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -112,5 +112,24 @@ describe MergeRequests::CreateFromIssueService do expect(result[:merge_request].assignee).to eq(user) end + + context 'when ref branch is set' do + subject { described_class.new(project, user, issue_iid: issue.iid, ref: 'feature').execute } + + it 'sets the merge request source branch to the new issue branch' do + expect(subject[:merge_request].source_branch).to eq(issue.to_branch_name) + end + + it 'sets the merge request target branch to the ref branch' do + expect(subject[:merge_request].target_branch).to eq('feature') + end + + context 'when ref branch does not exist' do + it 'does not create a merge request' do + expect { described_class.new(project, user, issue_iid: issue.iid, ref: 'nobr').execute } + .not_to change { project.merge_requests.count } + end + end + end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index dd8c803a2f7..5d226f34d2d 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -263,5 +263,66 @@ describe MergeRequests::CreateService do expect(issue_ids).to match_array([first_issue.id, second_issue.id]) end end + + context 'when source and target projects are different' do + let(:target_project) { create(:project) } + + let(:opts) do + { + title: 'Awesome merge_request', + source_branch: 'feature', + target_branch: 'master', + target_project_id: target_project.id + } + end + + context 'when user can not access source project' do + before do + target_project.add_developer(assignee) + target_project.add_master(user) + end + + it 'raises an error' do + expect { described_class.new(project, user, opts).execute } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + + context 'when user can not access target project' do + before do + target_project.add_developer(assignee) + target_project.add_master(user) + end + + it 'raises an error' do + expect { described_class.new(project, user, opts).execute } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + end + + context 'when user sets source project id' do + let(:another_project) { create(:project) } + + let(:opts) do + { + title: 'Awesome merge_request', + source_branch: 'feature', + target_branch: 'master', + source_project_id: another_project.id + } + end + + before do + project.add_developer(assignee) + project.add_master(user) + end + + it 'ignores source_project_id' do + merge_request = described_class.new(project, user, opts).execute + + expect(merge_request.source_project_id).to eq(project.id) + end + end end end diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb index 7a8c54673f7..f7ff8b80bd7 100644 --- a/spec/services/projects/autocomplete_service_spec.rb +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -93,26 +93,27 @@ describe Projects::AutocompleteService do let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, group: group) } - let!(:group_milestone) { create(:milestone, group: group) } - let!(:project_milestone) { create(:milestone, project: project) } + let!(:group_milestone1) { create(:milestone, group: group, due_date: '2017-01-01', title: 'Second Title') } + let!(:group_milestone2) { create(:milestone, group: group, due_date: '2017-01-01', title: 'First Title') } + let!(:project_milestone) { create(:milestone, project: project, due_date: '2016-01-01') } let(:milestone_titles) { described_class.new(project, user).milestones.map(&:title) } - it 'includes project and group milestones' do - expect(milestone_titles).to eq([group_milestone.title, project_milestone.title]) + it 'includes project and group milestones and sorts them correctly' do + expect(milestone_titles).to eq([project_milestone.title, group_milestone2.title, group_milestone1.title]) end it 'does not include closed milestones' do - group_milestone.close + group_milestone1.close - expect(milestone_titles).to eq([project_milestone.title]) + expect(milestone_titles).to eq([project_milestone.title, group_milestone2.title]) end it 'does not include milestones from other projects in the group' do other_project = create(:project, group: group) project_milestone.update!(project: other_project) - expect(milestone_titles).to eq([group_milestone.title]) + expect(milestone_titles).to eq([group_milestone2.title, group_milestone1.title]) end end end diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb new file mode 100644 index 00000000000..bb0e274c93e --- /dev/null +++ b/spec/services/projects/gitlab_projects_import_service_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Projects::GitlabProjectsImportService do + set(:namespace) { build(:namespace) } + let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + subject { described_class.new(namespace.owner, { namespace_id: namespace.id, path: path, file: file }) } + + describe '#execute' do + context 'with an invalid path' do + let(:path) { '/invalid-path/' } + + it 'returns an invalid project' do + project = subject.execute + + expect(project).not_to be_persisted + expect(project).not_to be_valid + end + end + + context 'with a valid path' do + let(:path) { 'test-path' } + + it 'creates a project' do + project = subject.execute + + expect(project).to be_persisted + expect(project).to be_valid + end + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 965fd39c967..ab3a257f36f 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -158,7 +158,7 @@ describe SystemNoteService do end it 'builds a correct phrase when assignee removed' do - expect(build_note([assignee1], [])).to eq 'removed assignee' + expect(build_note([assignee1], [])).to eq "unassigned @#{assignee1.username}" end it 'builds a correct phrase when assignees changed' do diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb index ff8b9595538..74d7715e50f 100644 --- a/spec/services/test_hooks/system_service_spec.rb +++ b/spec/services/test_hooks/system_service_spec.rb @@ -60,5 +60,25 @@ describe TestHooks::SystemService do expect(service.execute).to include(success_result) end end + + context 'merge_requests_events' do + let(:trigger) { 'merge_requests_events' } + + it 'returns error message if the user does not have any repository with a merge request' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure one of your projects has merge requests.' }) + end + + it 'executes hook' do + trigger_key = :merge_request_hooks + sample_data = { data: 'sample' } + create(:project_member, user: current_user, project: project) + create(:merge_request, source_project: project) + allow_any_instance_of(MergeRequest).to receive(:to_hook_data).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result) + expect(service.execute).to include(success_result) + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f51bb44086b..6186fb92bad 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -97,10 +97,6 @@ RSpec.configure do |config| TestEnv.init end - config.after(:suite) do - TestEnv.cleanup - end - config.before(:example) do # Skip pre-receive hook check so we can use the web editor and merge. allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) diff --git a/spec/support/devise_helpers.rb b/spec/support/devise_helpers.rb index 890a2d9d287..66874e10f38 100644 --- a/spec/support/devise_helpers.rb +++ b/spec/support/devise_helpers.rb @@ -2,13 +2,16 @@ module DeviseHelpers # explicitly tells Devise which mapping to use # this is needed when we are testing a Devise controller bypassing the router def set_devise_mapping(context:) - env = - if context.respond_to?(:env_config) - context.env_config - elsif context.respond_to?(:env) - context.env - end + env = env_from_context(context) env['devise.mapping'] = Devise.mappings[:user] if env end + + def env_from_context(context) + if context.respond_to?(:env_config) + context.env_config + elsif context.respond_to?(:env) + context.env + end + end end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 50702a0ac88..b52b6a28c54 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -125,6 +125,13 @@ module LoginHelpers }) end + def stub_omniauth_provider(provider, context: Rails.application) + env = env_from_context(context) + + set_devise_mapping(context: context) + env['omniauth.auth'] = OmniAuth.config.mock_auth[provider] + end + def stub_omniauth_saml_config(messages) set_devise_mapping(context: Rails.application) Rails.application.routes.disable_clear_and_finalize = true diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb index 3f1fd169b72..23f9b46ae0c 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/services_shared_context.rb @@ -3,13 +3,9 @@ Service.available_services_names.each do |service| let(:dashed_service) { service.dasherize } let(:service_method) { "#{service}_service".to_sym } let(:service_klass) { "#{service}_service".classify.constantize } - let(:service_fields) { service_klass.new.fields } + let(:service_instance) { service_klass.new } + let(:service_fields) { service_instance.fields } let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } } - let(:service_attrs_list_without_passwords) do - service_fields - .select { |field| field[:type] != 'password' } - .map { |field| field[:name].to_sym} - end let(:service_attrs) do service_attrs_list.inject({}) do |hash, k| if k =~ /^(token*|.*_token|.*_key)/ diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 17f3a861ba8..e827a8da0b7 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -57,6 +57,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do @issue = issue_service.execute @issues_sample_data = issue_service.hook_data(@issue, 'open') + project.add_developer(user) opts = { title: 'Awesome merge_request', description: 'please fix', diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 25ff6094408..fd6368e7b40 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -90,10 +90,6 @@ module TestEnv setup_forked_repo end - def cleanup - stop_gitaly - end - def disable_mailer allow_any_instance_of(NotificationService).to receive(:mailer) .and_return(double.as_null_object) @@ -163,6 +159,8 @@ module TestEnv spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i } + Kernel.at_exit { stop_gitaly } + wait_gitaly end @@ -309,7 +307,7 @@ module TestEnv # Before we used Git clone's --mirror option, bare repos could end up # with missing refs, clearing them and retrying should fix the issue. - cleanup && clean_gitlab_test_path && init unless reset.call + clean_gitlab_test_path && init unless reset.call end end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 31598586f59..4912baa348c 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -47,6 +47,14 @@ describe RepositoryForkWorker do perform! end + it 'protects the default branch' do + expect_fork_repository.and_return(true) + + perform! + + expect(fork_project.protected_branches.first.name).to eq(fork_project.default_branch) + end + it 'flushes various caches' do expect_fork_repository.and_return(true) diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 75de266369d..eec356b9f47 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -90,10 +90,14 @@ codequality: performance: stage: performance - image: - name: sitespeedio/sitespeed.io:6.0.3 - entrypoint: [""] + image: docker:latest + variables: + DOCKER_DRIVER: overlay2 + allow_failure: true + services: + - docker:dind script: + - setup_docker - performance artifacts: paths: @@ -112,7 +116,7 @@ sast: - sast . artifacts: paths: [gl-sast-report.json] - + sast:container: image: docker:latest variables: @@ -260,7 +264,7 @@ production: export CI_APPLICATION_TAG=$CI_COMMIT_SHA export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID} export TILLER_NAMESPACE=$KUBE_NAMESPACE - + function sast_container() { docker run -d --name db arminc/clair-db:latest docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1 @@ -466,26 +470,26 @@ production: --docker-email="$GITLAB_USER_EMAIL" \ -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f - } - + function performance() { export CI_ENVIRONMENT_URL=$(cat environment_url.txt) - + mkdir gitlab-exporter wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-3/index.js - + mkdir sitespeed-results - + if [ -f .gitlab-urls.txt ] then sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt - /start.sh --plugins.add gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.0.3 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt else - /start.sh --plugins.add gitlab-exporter --outputFolder sitespeed-results $CI_ENVIRONMENT_URL + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.0.3 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" fi - + mv sitespeed-results/data/performance.json performance.json } - + function persist_environment_url() { echo $CI_ENVIRONMENT_URL > environment_url.txt } diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml index dd9496deb4d..5249449c7f8 100644 --- a/vendor/prometheus/values.yaml +++ b/vendor/prometheus/values.yaml @@ -1,134 +1,117 @@ -alertmanager: | +alertmanager: enabled: false -kubeStateMetrics: | - enabled: 'false' +kubeStateMetrics: + enabled: false -nodeExporter: | - enabled: 'false' +nodeExporter: + enabled: false -pushgateway: | - enabled: 'false' +pushgateway: + enabled: false -serverFiles: | - alerts: '' - rules: '' +serverFiles: + alerts: "" + rules: "" prometheus.yml: |- - rule_files: | + rule_files: - /etc/config/rules - /etc/config/alerts - scrape_configs: | + scrape_configs: - job_name: prometheus - static_configs: | + static_configs: - targets: - localhost:9090 - - - job_name: 'kubernetes-apiservers' - kubernetes_sd_configs: | - - role: endpoints + - job_name: kubernetes-cadvisor scheme: https - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" insecure_skip_verify: true - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token + bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" + kubernetes_sd_configs: + - role: node + api_server: https://kubernetes.default.svc:443 + tls_config: + ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" relabel_configs: - - source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] - action: keep - regex: default;kubernetes;https - - job_name: 'kubernetes-nodes' + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + - target_label: __address__ + replacement: kubernetes.default.svc:443 + - source_labels: + - __meta_kubernetes_node_name + regex: "(.+)" + target_label: __metrics_path__ + replacement: "/api/v1/nodes/${1}/proxy/metrics/cadvisor" + metric_relabel_configs: + - source_labels: + - pod_name + target_label: environment + regex: "(.+)-.+-.+" + - job_name: kubernetes-nodes scheme: https tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" insecure_skip_verify: true - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token + bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" kubernetes_sd_configs: - - role: node + - role: node + api_server: https://kubernetes.default.svc:443 + tls_config: + ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - target_label: __address__ - replacement: kubernetes.default.svc:443 - - source_labels: [__meta_kubernetes_node_name] - regex: (.+) - target_label: __metrics_path__ - replacement: /api/v1/nodes/${1}/proxy/metrics - - - job_name: 'kubernetes-service-endpoints' - kubernetes_sd_configs: - - role: endpoints - relabel_configs: | - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] - action: keep - regex: 'true' - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme] - action: replace - target_label: __scheme__ - regex: (https?) - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] - action: replace - target_label: __address__ - regex: (.+)(?::\d+);(\d+) - replacement: $1:$2 - - action: labelmap - regex: __meta_kubernetes_service_label_(.+) - - source_labels: [__meta_kubernetes_namespace] - action: replace - target_label: kubernetes_namespace - - source_labels: [__meta_kubernetes_service_name] - action: replace - target_label: kubernetes_name - - job_name: 'prometheus-pushgateway' - honor_labels: true - kubernetes_sd_configs: | - - role: service - relabel_configs: | - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe] - action: keep - regex: pushgateway - - job_name: 'kubernetes-services' - metrics_path: /probe - params: | - module: [http_2xx] - kubernetes_sd_configs: | - - role: service - relabel_configs: | - - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe] - action: keep - regex: 'true' - - source_labels: [__address__] - target_label: __param_target - - target_label: __address__ - replacement: blackbox - - source_labels: [__param_target] - target_label: instance - - action: labelmap - regex: __meta_kubernetes_service_label_(.+) - - source_labels: [__meta_kubernetes_namespace] - target_label: kubernetes_namespace - - source_labels: [__meta_kubernetes_service_name] - target_label: kubernetes_name - - job_name: 'kubernetes-pods' + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + - target_label: __address__ + replacement: kubernetes.default.svc:443 + - source_labels: + - __meta_kubernetes_node_name + regex: "(.+)" + target_label: __metrics_path__ + replacement: "/api/v1/nodes/${1}/proxy/metrics" + metric_relabel_configs: + - source_labels: + - pod_name + target_label: environment + regex: "(.+)-.+-.+" + - job_name: kubernetes-pods + tls_config: + ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + insecure_skip_verify: true + bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" kubernetes_sd_configs: - - role: pod + - role: pod + api_server: https://kubernetes.default.svc:443 + tls_config: + ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" relabel_configs: - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] - action: keep - regex: 'true' - - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path] - action: replace - target_label: __metrics_path__ - regex: (.+) - - action: labelmap - regex: __meta_kubernetes_pod_label_(.+) - - source_labels: [__meta_kubernetes_namespace] - action: replace - target_label: kubernetes_namespace - - source_labels: [__meta_kubernetes_pod_name] - action: replace - target_label: kubernetes_pod_name + - source_labels: + - __meta_kubernetes_pod_annotation_prometheus_io_scrape + action: keep + regex: 'true' + - source_labels: + - __meta_kubernetes_pod_annotation_prometheus_io_path + action: replace + target_label: __metrics_path__ + regex: "(.+)" + - source_labels: + - __address__ + - __meta_kubernetes_pod_annotation_prometheus_io_port + action: replace + regex: "([^:]+)(?::[0-9]+)?;([0-9]+)" + replacement: "$1:$2" + target_label: __address__ + - action: labelmap + regex: __meta_kubernetes_pod_label_(.+) + - source_labels: + - __meta_kubernetes_namespace + action: replace + target_label: kubernetes_namespace + - source_labels: + - __meta_kubernetes_pod_name + action: replace + target_label: kubernetes_pod_name diff --git a/yarn.lock b/yarn.lock index 5d40e833889..693f7123e8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -237,7 +237,7 @@ array-union@^1.0.1: dependencies: array-uniq "^1.0.1" -array-uniq@^1.0.1: +array-uniq@^1.0.1, array-uniq@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -3198,7 +3198,7 @@ html-entities@1.2.0, html-entities@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2" -htmlparser2@^3.8.2: +htmlparser2@^3.8.2, htmlparser2@^3.9.0: version "3.9.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" dependencies: @@ -4054,6 +4054,10 @@ lodash.capitalize@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz#f826c9b4e2a8511d84e3aca29db05e1a4f3b72a9" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" @@ -4069,6 +4073,10 @@ lodash.defaults@^3.1.2: lodash.assign "^3.0.0" lodash.restparam "^3.0.0" +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + lodash.get@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f" @@ -4103,6 +4111,10 @@ lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" +lodash.mergewith@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" + lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" @@ -4191,9 +4203,9 @@ map-stream@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" -marked@^0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7" +marked@^0.3.12: + version "0.3.12" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.12.tgz#7cf25ff2252632f3fe2406bde258e94eee927519" math-expression-evaluator@^1.2.14: version "1.2.16" @@ -5155,6 +5167,14 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 source-map "^0.5.6" supports-color "^3.2.3" +postcss@^6.0.14: + version "6.0.15" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.15.tgz#f460cd6269fede0d1bf6defff0b934a9845d974d" + dependencies: + chalk "^2.3.0" + source-map "^0.6.1" + supports-color "^5.1.0" + postcss@^6.0.8: version "6.0.14" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885" @@ -5669,6 +5689,18 @@ safe-buffer@^5.0.1, safe-buffer@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" +sanitize-html@^1.16.1: + version "1.16.3" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.16.3.tgz#96c1b44a36ff7312e1c22a14b05274370ac8bd56" + dependencies: + htmlparser2 "^3.9.0" + lodash.clonedeep "^4.5.0" + lodash.escaperegexp "^4.1.2" + lodash.mergewith "^4.6.0" + postcss "^6.0.14" + srcset "^1.0.0" + xtend "^4.0.0" + sax@~1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" @@ -5982,6 +6014,13 @@ sql.js@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-0.4.0.tgz#23be9635520eb0ff43a741e7e830397266e88445" +srcset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" + dependencies: + array-uniq "^1.0.2" + number-is-nan "^1.0.0" + sshpk@^1.7.0: version "1.13.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" @@ -6126,6 +6165,12 @@ supports-color@^4.2.1: dependencies: has-flag "^2.0.0" +supports-color@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5" + dependencies: + has-flag "^2.0.0" + svg4everybody@2.1.9: version "2.1.9" resolved "https://registry.yarnpkg.com/svg4everybody/-/svg4everybody-2.1.9.tgz#5bd9f6defc133859a044646d4743fabc28db7e2d" |