diff options
author | Sam Rose <sam@gitlab.com> | 2017-04-06 21:02:25 -0400 |
---|---|---|
committer | Sam Rose <sam@gitlab.com> | 2017-04-06 21:02:25 -0400 |
commit | bb23534b37f0eff6075dbc1a3337009c9f19c327 (patch) | |
tree | c9f465801b7c907ab1160b3d0378e1087bf3507f | |
parent | b47d8a333c9aa554f6b820c6da101b67d85015c8 (diff) | |
parent | 3900aa803e574ddbcdb1a899f0e6085a0bb6c457 (diff) | |
download | gitlab-ce-mr-shortcuts-counter.tar.gz |
Update branch with latest stable commitsmr-shortcuts-counter
69 files changed, 730 insertions, 429 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index f914111fc4c..111a7d4f51c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.0.4 (2017-04-05) + +- Don’t show source project name when user does not have access. +- Remove the class attribute from the whitelist for HTML generated from Markdown. +- Fix path disclosure in project import/export. +- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status. +- Fix for open redirect vulnerabilities in todos, issues, and MR controllers. + +## 9.0.3 (2017-04-05) + +- Fix name colision when importing GitHub pull requests from forked repositories. !9719 +- Fix GitHub Importer for PRs of deleted forked repositories. !9992 +- Fix environment folder route when special chars present in environment name. !10250 +- Improve Markdown rendering when a lot of merge requests are referenced. !10252 +- Allow users to import GitHub projects to subgroups. +- Backport API changes needed to fix sticking in EE. +- Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery. (mhasbini) +- Make CI build to use optimistic locking only on status change. +- Fix race condition where a namespace would be deleted before a project was deleted. +- Fix linking to new issue with selected template via url parameter. +- Remove unnecessary ORDER BY clause when updating todos. (mhasbini) +- API: Make the /notes endpoint work with noteable iid instead of id. +- Fixes method not replacing URL parameters correctly and breaking pipelines pagination. +- Move issue, mr, todos next to profile dropdown in top nav. + ## 9.0.2 (2017-03-29) - Correctly update paths when changing a child group. @@ -1 +1 @@ -9.0.2 +9.0.4 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index b5a988df897..87a9cc13a7e 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -2,7 +2,8 @@ /* global Vue, CommitsPipelineStore, PipelinesService, Flash */ window.Vue = require('vue'); -require('./pipelines_table'); +const PipelinesTable = require('./pipelines_table'); + /** * Commits View > Pipelines Tab > Pipelines Table. * Merge Request View > Pipelines Tab > Pipelines Table. @@ -21,7 +22,7 @@ $(() => { } const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView(); + gl.commits.pipelines.PipelinesTableBundle = new PipelinesTable(); if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 631ed34851c..4e770ab7c31 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -5,9 +5,9 @@ window.Vue = require('vue'); window.Vue.use(require('vue-resource')); require('../../lib/utils/common_utils'); require('../../vue_shared/vue_resource_interceptor'); -require('../../vue_shared/components/pipelines_table'); require('./pipelines_service'); const PipelineStore = require('./pipelines_store'); +const PipelinesTableComponent = require('../../vue_shared/components/pipelines_table'); /** * @@ -19,86 +19,78 @@ const PipelineStore = require('./pipelines_store'); * Necessary SVG in the table are provided as props. This should be refactored * as soon as we have Webpack and can load them directly into JS files. */ +module.exports = Vue.component('pipelines-table', { + components: { + 'pipelines-table-component': PipelinesTableComponent, + }, -(() => { - window.gl = window.gl || {}; - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} + */ + data() { + const store = new PipelineStore(); - gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', { + return { + endpoint: null, + store, + state: store.state, + isLoading: false, + }; + }, - components: { - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, - }, + /** + * When the component is about to be mounted, tell the service to fetch the data + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + beforeMount() { + this.endpoint = this.$el.dataset.endpoint; + const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} - */ - data() { - const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; - const store = new PipelineStore(); + this.isLoading = true; + return pipelinesService.all() + .then(response => response.json()) + .then((json) => { + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = json.pipelines || json; + this.store.storePipelines(pipelines); + this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); + }); + }, - return { - endpoint: pipelinesTableData.endpoint, - store, - state: store.state, - isLoading: false, - }; - }, + beforeUpdate() { + if (this.state.pipelines.length && this.$children) { + PipelineStore.startTimeAgoLoops.call(this, Vue); + } + }, - /** - * When the component is about to be mounted, tell the service to fetch the data - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - beforeMount() { - const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); - - this.isLoading = true; - return pipelinesService.all() - .then(response => response.json()) - .then((json) => { - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = json.pipelines || json; - this.store.storePipelines(pipelines); - this.isLoading = false; - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); - }); - }, - - beforeUpdate() { - if (this.state.pipelines.length && this.$children) { - PipelineStore.startTimeAgoLoops.call(this, Vue); - } - }, - - template: ` - <div class="pipelines"> - <div class="realtime-loading" v-if="isLoading"> - <i class="fa fa-spinner fa-spin"></i> - </div> + template: ` + <div class="pipelines"> + <div class="realtime-loading" v-if="isLoading"> + <i class="fa fa-spinner fa-spin"></i> + </div> - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.pipelines.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - No pipelines to show - </h2> - </div> + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + No pipelines to show + </h2> + </div> - <div class="table-holder pipelines" - v-if="!isLoading && state.pipelines.length > 0"> - <pipelines-table-component :pipelines="state.pipelines"/> - </div> + <div class="table-holder pipelines" + v-if="!isLoading && state.pipelines.length > 0"> + <pipelines-table-component :pipelines="state.pipelines"/> </div> - `, - }); -})(); + </div> + `, +}); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index a1423b6fda5..6e1590e3b0b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -247,7 +247,7 @@ }); /** - * Updates the search parameter of a URL given the parameter and values provided. + * Updates the search parameter of a URL given the parameter and value provided. * * If no search params are present we'll add it. * If param for page is already present, we'll update it @@ -262,17 +262,24 @@ let search; const locationSearch = window.location.search; - if (locationSearch.length === 0) { - search = `?${param}=${value}`; - } + if (locationSearch.length) { + const parameters = locationSearch.substring(1, locationSearch.length) + .split('&') + .reduce((acc, element) => { + const val = element.split('='); + acc[val[0]] = decodeURIComponent(val[1]); + return acc; + }, {}); - if (locationSearch.indexOf(param) !== -1) { - const regex = new RegExp(param + '=\\d'); - search = locationSearch.replace(regex, `${param}=${value}`); - } + parameters[param] = value; - if (locationSearch.length && locationSearch.indexOf(param) === -1) { - search = `${locationSearch}&${param}=${value}`; + const toString = Object.keys(parameters) + .map(val => `${val}=${encodeURIComponent(parameters[val])}`) + .join('&'); + + search = `?${toString}`; + } else { + search = `?${param}=${value}`; } return search; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 190336dbd20..3f0ad6490b5 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -3,9 +3,12 @@ /* global Cookies */ /* global Flash */ -require('./breakpoints'); -window.Cookies = require('js-cookie'); -require('./flash'); +import Cookies from 'js-cookie'; + +import './breakpoints'; +import './flash'; + +const PipelinesTable = require('./commit/pipelines/pipelines_table'); /* eslint-disable max-len */ // MergeRequestTabs @@ -97,6 +100,13 @@ require('./flash'); .off('click', this.clickTab); } + destroy() { + this.unbindEvents(); + if (this.commitPipelinesTable) { + this.commitPipelinesTable.$destroy(); + } + } + showTab(e) { e.preventDefault(); this.activateTab($(e.target).data('action')); @@ -131,12 +141,8 @@ require('./flash'); offset: 0, }); } else if (action === 'pipelines') { - if (this.pipelinesLoaded) { - return; - } - const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); - this.pipelinesLoaded = true; + this.resetViewContainer(); + this.loadPipelines(); } else { this.expandView(); this.resetViewContainer(); @@ -225,6 +231,18 @@ require('./flash'); }); } + loadPipelines() { + if (this.pipelinesLoaded) { + return; + } + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + // Could already be mounted from the `pipelines_bundle` + if (pipelineTableViewEl) { + this.commitPipelinesTable = new PipelinesTable().$mount(pipelineTableViewEl); + } + this.pipelinesLoaded = true; + } + loadDiff(source) { if (this.diffsLoaded) { return; diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js index 601ef41e917..4732092ab14 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js @@ -2,17 +2,18 @@ /* eslint-disable no-param-reassign */ window.Vue = require('vue'); + require('../vue_shared/components/table_pagination'); require('./store'); -require('../vue_shared/components/pipelines_table'); const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store'); +const PipelinesTableComponent = require('../vue_shared/components/pipelines_table'); ((gl) => { gl.VuePipelines = Vue.extend({ components: { 'gl-pagination': gl.VueGlPagination, - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + 'pipelines-table-component': PipelinesTableComponent, }, data() { diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js index 0d8f85db965..c534bb85bf1 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js @@ -8,45 +8,39 @@ require('./pipelines_table_row'); * Given an array of objects, renders a table. */ -(() => { - window.gl = window.gl || {}; - gl.pipelines = gl.pipelines || {}; - - gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', { - - props: { - pipelines: { - type: Array, - required: true, - default: () => ([]), - }, - +module.exports = { + props: { + pipelines: { + type: Array, + required: true, + default: () => ([]), }, - components: { - 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent, - }, + }, + + components: { + 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent, + }, - template: ` - <table class="table ci-table"> - <thead> - <tr> - <th class="js-pipeline-status pipeline-status">Status</th> - <th class="js-pipeline-info pipeline-info">Pipeline</th> - <th class="js-pipeline-commit pipeline-commit">Commit</th> - <th class="js-pipeline-stages pipeline-stages">Stages</th> - <th class="js-pipeline-date pipeline-date"></th> - <th class="js-pipeline-actions pipeline-actions"></th> - </tr> - </thead> - <tbody> - <template v-for="model in pipelines" - v-bind:model="model"> - <tr is="pipelines-table-row-component" - :pipeline="model"></tr> - </template> - </tbody> - </table> - `, - }); -})(); + template: ` + <table class="table ci-table"> + <thead> + <tr> + <th class="js-pipeline-status pipeline-status">Status</th> + <th class="js-pipeline-info pipeline-info">Pipeline</th> + <th class="js-pipeline-commit pipeline-commit">Commit</th> + <th class="js-pipeline-stages pipeline-stages">Stages</th> + <th class="js-pipeline-date pipeline-date"></th> + <th class="js-pipeline-actions pipeline-actions"></th> + </tr> + </thead> + <tbody> + <template v-for="model in pipelines" + v-bind:model="model"> + <tr is="pipelines-table-row-component" + :pipeline="model"></tr> + </template> + </tbody> + </table> + `, +}; diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb index 0a995c45bdf..eb3a623acdd 100644 --- a/app/controllers/concerns/continue_params.rb +++ b/app/controllers/concerns/continue_params.rb @@ -7,6 +7,7 @@ module ContinueParams continue_params = continue_params.permit(:to, :notice, :notice_now) return unless continue_params[:to] && continue_params[:to].start_with?('/') + return if continue_params[:to].start_with?('//') continue_params end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 5848ca62777..20336349c9a 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -7,7 +7,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController @sort = params[:sort] @todos = @todos.page(params[:page]) if @todos.out_of_range? && @todos.total_pages != 0 - redirect_to url_for(params.merge(page: @todos.total_pages)) + redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true)) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 857d907c1a6..6bdf089d45c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -29,7 +29,7 @@ class Projects::IssuesController < Projects::ApplicationController @issuable_meta_data = issuable_meta_data(@issues, @collection_type) if @issues.out_of_range? && @issues.total_pages != 0 - return redirect_to url_for(params.merge(page: @issues.total_pages)) + return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true)) end if params[:label_name].present? diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 677a8a1a73a..943cc7a688f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -42,7 +42,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 - return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) + return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true)) end if params[:label_name].present? diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index b44f38d4a0c..dff58338b94 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -24,12 +24,12 @@ class RegistrationsController < Devise::RegistrationsController end def destroy - Users::DestroyService.new(current_user).execute(current_user) + DeleteUserWorker.perform_async(current_user.id, current_user.id) respond_to do |format| format.html do session.try(:destroy) - redirect_to new_user_session_path, notice: "Account successfully removed." + redirect_to new_user_session_path, notice: "Account scheduled for removal." end end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index bd0c2cd661e..6b9e4267281 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -407,7 +407,10 @@ module ProjectsHelper def sanitize_repo_path(project, message) return '' unless message.present? - message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") + exports_path = File.join(Settings.shared['path'], 'tmp/project_exports') + filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]") + + filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") end def project_feature_options diff --git a/app/models/user.rb b/app/models/user.rb index 0432b2aa6a1..e786abb98e1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -631,8 +631,10 @@ class User < ActiveRecord::Base end def fork_of(project) - links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects) - + links = ForkedProjectLink.where( + forked_from_project_id: project, + forked_to_project_id: personal_projects.unscope(:order) + ) if links.any? links.first.forked_to_project else diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index fdce542bd9e..d45da5180e1 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -21,7 +21,9 @@ module MergeRequests delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request def find_source_project - source_project || project + return source_project if source_project.present? && can?(current_user, :read_project, source_project) + + project end def find_target_project diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 8787a1c93a9..336f55ed161 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -202,7 +202,7 @@ class TodoService todos = current_user.todos.where(id: ids) # Only return those that are not really on that state - marked_todos = todos.where.not(state: state).update_all(state: state) + marked_todos = todos.where.not(state: state).unscope(:order).update_all(state: state) current_user.update_todos_count_cache marked_todos end diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 833da5bc5d1..a3b32a71a64 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -20,10 +20,10 @@ module Users Groups::DestroyService.new(group, current_user).execute end - user.personal_projects.each do |project| + user.personal_projects.with_deleted.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute end move_issues_to_ghost_user(user) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 25a4aec0a38..6696b988511 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -121,7 +121,7 @@ - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } - = icon('tags', class: 'hidden', 'aria-hidden': 'true') + = icon('tags', 'aria-hidden': 'true') %span = selected_labels.size .title.hide-collapsed diff --git a/changelogs/unreleased/26595-fix-issue-preselected-template.yml b/changelogs/unreleased/26595-fix-issue-preselected-template.yml deleted file mode 100644 index a94765f8f2a..00000000000 --- a/changelogs/unreleased/26595-fix-issue-preselected-template.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix linking to new issue with selected template via url parameter -merge_request: -author: diff --git a/changelogs/unreleased/30098-banzai-filter-mergerequestreferencefilter-has-an-n-1-query-problem.yml b/changelogs/unreleased/30098-banzai-filter-mergerequestreferencefilter-has-an-n-1-query-problem.yml deleted file mode 100644 index f3f4e065aef..00000000000 --- a/changelogs/unreleased/30098-banzai-filter-mergerequestreferencefilter-has-an-n-1-query-problem.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve Markdown rendering when a lot of merge requests are referenced -merge_request: 10252 -author: diff --git a/changelogs/unreleased/30276-move-top-badges.yml b/changelogs/unreleased/30276-move-top-badges.yml deleted file mode 100644 index 68a8ab359fd..00000000000 --- a/changelogs/unreleased/30276-move-top-badges.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move issue, mr, todos next to profile dropdown in top nav -merge_request: -author: diff --git a/changelogs/unreleased/30289-allow-users-to-import-github-projects-to-subgroups.yml b/changelogs/unreleased/30289-allow-users-to-import-github-projects-to-subgroups.yml deleted file mode 100644 index a33382a85e3..00000000000 --- a/changelogs/unreleased/30289-allow-users-to-import-github-projects-to-subgroups.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow users to import GitHub projects to subgroups -merge_request: -author: diff --git a/changelogs/unreleased/fix-gb-environments-folders-route.yml b/changelogs/unreleased/fix-gb-environments-folders-route.yml deleted file mode 100644 index fd9d9e6f168..00000000000 --- a/changelogs/unreleased/fix-gb-environments-folders-route.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix environment folder route when special chars present in environment name -merge_request: 10250 -author: diff --git a/changelogs/unreleased/make-ci-build-to-lock-on-status-change.yml b/changelogs/unreleased/make-ci-build-to-lock-on-status-change.yml deleted file mode 100644 index b0c5eb4ed17..00000000000 --- a/changelogs/unreleased/make-ci-build-to-lock-on-status-change.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make CI build to use optimistic locking only on status change -merge_request: -author: diff --git a/doc/api/notes.md b/doc/api/notes.md index 6ef06b2c2e9..5e927143714 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -9,13 +9,13 @@ Notes are comments on snippets, issues or merge requests. Gets a list of all notes for a single issue. ``` -GET /projects/:id/issues/:issue_id/notes +GET /projects/:id/issues/:issue_iid/notes ``` Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The ID of an issue +- `issue_iid` (required) - The IID of an issue ```json [ @@ -63,13 +63,13 @@ Parameters: Returns a single note for a specific project issue ``` -GET /projects/:id/issues/:issue_id/notes/:note_id +GET /projects/:id/issues/:issue_iid/notes/:note_id ``` Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The ID of a project issue +- `issue_iid` (required) - The IID of a project issue - `note_id` (required) - The ID of an issue note ### Create new issue note @@ -78,13 +78,13 @@ Creates a new note to a single project issue. If you create a note where the bod only contains an Award Emoji, you'll receive this object back. ``` -POST /projects/:id/issues/:issue_id/notes +POST /projects/:id/issues/:issue_iid/notes ``` Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The ID of an issue +- `issue_id` (required) - The IID of an issue - `body` (required) - The content of a note - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z @@ -93,13 +93,13 @@ Parameters: Modify existing note of an issue. ``` -PUT /projects/:id/issues/:issue_id/notes/:note_id +PUT /projects/:id/issues/:issue_iid/notes/:note_id ``` Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The ID of an issue +- `issue_iid` (required) - The IID of an issue - `note_id` (required) - The ID of a note - `body` (required) - The content of a note @@ -108,7 +108,7 @@ Parameters: Deletes an existing note of an issue. ``` -DELETE /projects/:id/issues/:issue_id/notes/:note_id +DELETE /projects/:id/issues/:issue_iid/notes/:note_id ``` Parameters: @@ -116,7 +116,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of an issue | +| `issue_iid` | integer | yes | The IID of an issue | | `note_id` | integer | yes | The ID of a note | ```bash @@ -228,26 +228,26 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://git Gets a list of all notes for a single merge request. ``` -GET /projects/:id/merge_requests/:merge_request_id/notes +GET /projects/:id/merge_requests/:merge_request_iid/notes ``` Parameters: - `id` (required) - The ID of a project -- `merge_request_id` (required) - The ID of a project merge request +- `merge_request_iid` (required) - The IID of a project merge request ### Get single merge request note Returns a single note for a given merge request. ``` -GET /projects/:id/merge_requests/:merge_request_id/notes/:note_id +GET /projects/:id/merge_requests/:merge_request_iid/notes/:note_id ``` Parameters: - `id` (required) - The ID of a project -- `merge_request_id` (required) - The ID of a project merge request +- `merge_request_iid` (required) - The IID of a project merge request - `note_id` (required) - The ID of a merge request note ```json @@ -278,13 +278,13 @@ If you create a note where the body only contains an Award Emoji, you'll receive this object back. ``` -POST /projects/:id/merge_requests/:merge_request_id/notes +POST /projects/:id/merge_requests/:merge_request_iid/notes ``` Parameters: - `id` (required) - The ID of a project -- `merge_request_id` (required) - The ID of a merge request +- `merge_request_iid` (required) - The IID of a merge request - `body` (required) - The content of a note ### Modify existing merge request note @@ -292,13 +292,13 @@ Parameters: Modify existing note of a merge request. ``` -PUT /projects/:id/merge_requests/:merge_request_id/notes/:note_id +PUT /projects/:id/merge_requests/:merge_request_iid/notes/:note_id ``` Parameters: - `id` (required) - The ID of a project -- `merge_request_id` (required) - The ID of a merge request +- `merge_request_iid` (required) - The IID of a merge request - `note_id` (required) - The ID of a note - `body` (required) - The content of a note @@ -307,7 +307,7 @@ Parameters: Deletes an existing note of a merge request. ``` -DELETE /projects/:id/merge_requests/:merge_request_id/notes/:note_id +DELETE /projects/:id/merge_requests/:merge_request_iid/notes/:note_id ``` Parameters: @@ -315,7 +315,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -| `merge_request_id` | integer | yes | The ID of a merge request | +| `merge_request_iid` | integer | yes | The IID of a merge request | | `note_id` | integer | yes | The ID of a note | ```bash diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a9b364da9e1..b680c43a269 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -90,6 +90,11 @@ module API MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid) end + def find_project_snippet(id) + finder_params = { filter: :by_project, project: user_project } + SnippetsFinder.new.execute(current_user, finder_params).find(id) + end + def find_merge_request_with_access(iid, access_level = :read_merge_request) merge_request = user_project.merge_requests.find_by!(iid: iid) authorize! access_level, merge_request diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 74848a6e144..1369b021ea4 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -50,10 +50,14 @@ module API forbidden!('Job has been erased!') if job.erased? end - def authenticate_job!(job) + def authenticate_job! + job = Ci::Build.find_by_id(params[:id]) + validate_job!(job) do forbidden! unless job_token_valid?(job) end + + job end def job_token_valid?(job) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 7a03955a045..09821945053 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -226,41 +226,6 @@ module API .cancel(merge_request) end - desc 'Get the comments of a merge request' do - success Entities::MRNote - end - params do - use :pagination - end - get ':id/merge_requests/:merge_request_iid/comments' do - merge_request = find_merge_request_with_access(params[:merge_request_iid]) - present paginate(merge_request.notes.fresh), with: Entities::MRNote - end - - desc 'Post a comment to a merge request' do - success Entities::MRNote - end - params do - requires :note, type: String, desc: 'The text of the comment' - end - post ':id/merge_requests/:merge_request_iid/comments' do - merge_request = find_merge_request_with_access(params[:merge_request_iid], :create_note) - - opts = { - note: params[:note], - noteable_type: 'MergeRequest', - noteable_id: merge_request.id - } - - note = ::Notes::CreateService.new(user_project, current_user, opts).execute - - if note.save - present note, with: Entities::MRNote - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) - end - end - desc 'List issues that will be closed on merge' do success Entities::MRNote end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 3b3e45cbd06..3dee8938179 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -21,7 +21,7 @@ module API use :pagination end get ":id/#{noteables_str}/:noteable_id/notes" do - noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + noteable = find_project_noteable(noteables_str, params[:noteable_id]) if can?(current_user, noteable_read_ability_name(noteable), noteable) # We exclude notes that are cross-references and that cannot be viewed @@ -49,7 +49,7 @@ module API requires :noteable_id, type: Integer, desc: 'The ID of the noteable' end get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do - noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) + noteable = find_project_noteable(noteables_str, params[:noteable_id]) note = noteable.notes.find(params[:note_id]) can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user) @@ -69,14 +69,14 @@ module API optional :created_at, type: String, desc: 'The creation date of the note' end post ":id/#{noteables_str}/:noteable_id/notes" do + noteable = find_project_noteable(noteables_str, params[:noteable_id]) + opts = { note: params[:body], noteable_type: noteables_str.classify, - noteable_id: params[:noteable_id] + noteable_id: noteable.id } - noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id]) - if can?(current_user, noteable_read_ability_name(noteable), noteable) if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user) opts[:created_at] = params[:created_at] @@ -137,6 +137,10 @@ module API end helpers do + def find_project_noteable(noteables_str, noteable_id) + public_send("find_project_#{noteables_str.singularize}", noteable_id) + end + def noteable_read_ability_name(noteable) "read_#{noteable.class.to_s.underscore}".to_sym end diff --git a/lib/api/runner.rb b/lib/api/runner.rb index 4c9db2c8716..d288369e362 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -113,8 +113,7 @@ module API optional :state, type: String, desc: %q(Job's status: success, failed) end put '/:id' do - job = Ci::Build.find_by_id(params[:id]) - authenticate_job!(job) + job = authenticate_job! job.update_attributes(trace: params[:trace]) if params[:trace] @@ -140,8 +139,7 @@ module API optional :token, type: String, desc: %q(Job's authentication token) end patch '/:id/trace' do - job = Ci::Build.find_by_id(params[:id]) - authenticate_job!(job) + job = authenticate_job! error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range') content_range = request.headers['Content-Range'] @@ -175,8 +173,7 @@ module API require_gitlab_workhorse! Gitlab::Workhorse.verify_api_request!(headers) - job = Ci::Build.find_by_id(params[:id]) - authenticate_job!(job) + job = authenticate_job! forbidden!('Job is not running') unless job.running? if params[:filesize] @@ -212,8 +209,7 @@ module API not_allowed! unless Gitlab.config.artifacts.enabled require_gitlab_workhorse! - job = Ci::Build.find_by_id(params[:id]) - authenticate_job!(job) + job = authenticate_job! forbidden!('Job is not running!') unless job.running? artifacts_upload_path = ArtifactUploader.artifacts_upload_path @@ -245,8 +241,7 @@ module API optional :token, type: String, desc: %q(Job's authentication token) end get '/:id/artifacts' do - job = Ci::Build.find_by_id(params[:id]) - authenticate_job!(job) + job = authenticate_job! artifacts_file = job.artifacts_file unless artifacts_file.file_storage? diff --git a/lib/api/users.rb b/lib/api/users.rb index 549003f576a..a309be9d0fa 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -312,7 +312,7 @@ module API user = User.find_by(id: params[:id]) not_found!('User') unless user - ::Users::DestroyService.new(current_user).execute(user) + DeleteUserWorker.perform_async(current_user.id, user.id) end desc 'Block a user. Available only for admins.' diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index ff580ec68f8..ee73fa91589 100644 --- a/lib/banzai/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -14,7 +14,7 @@ module Banzai def self.renderer @renderer ||= begin - renderer = Redcarpet::Render::HTML.new + renderer = Banzai::Renderer::HTML.new Redcarpet::Markdown.new(renderer, redcarpet_options) end end diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index d5f9e252f62..522217deae4 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -24,10 +24,6 @@ module Banzai # Only push these customizations once return if customized?(whitelist[:transformers]) - # Allow code highlighting - whitelist[:attributes]['pre'] = %w(class v-pre) - whitelist[:attributes]['span'] = %w(class) - # Allow table alignment whitelist[:attributes]['th'] = %w(style) whitelist[:attributes]['td'] = %w(style) @@ -52,9 +48,6 @@ module Banzai # Remove `rel` attribute from `a` elements whitelist[:transformers].push(self.class.remove_rel) - # Remove `class` attribute from non-highlight spans - whitelist[:transformers].push(self.class.clean_spans) - whitelist end @@ -84,21 +77,6 @@ module Banzai end end end - - def clean_spans - lambda do |env| - node = env[:node] - - return unless node.name == 'span' - return unless node.has_attribute?('class') - - unless node.ancestors.any? { |n| n.name.casecmp('pre').zero? } - node.remove_attribute('class') - end - - { node_whitelist: [node] } - end - end end end end diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index a447e2b8bff..d40d1f063bb 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -16,7 +16,7 @@ module Banzai end def highlight_node(node) - language = node.attr('class') + language = node.attr('lang') code = node.text css_classes = "code highlight" lexer = lexer_for(language) diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index b25d6f18d59..639773539ad 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -9,9 +9,9 @@ module Banzai # The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. def self.filters @filters ||= FilterArray[ - Filter::SyntaxHighlightFilter, Filter::PlantumlFilter, Filter::SanitizationFilter, + Filter::SyntaxHighlightFilter, Filter::MathFilter, Filter::UploadLinkFilter, diff --git a/lib/banzai/renderer/html.rb b/lib/banzai/renderer/html.rb new file mode 100644 index 00000000000..252caa35947 --- /dev/null +++ b/lib/banzai/renderer/html.rb @@ -0,0 +1,13 @@ +module Banzai + module Renderer + class HTML < Redcarpet::Render::HTML + def block_code(code, lang) + lang_attr = lang ? %Q{ lang="#{lang}"} : '' + + "\n<pre>" \ + "<code#{lang_attr}>#{html_escape(code)}</code>" \ + "</pre>" + end + end + end +end diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 746e76a1b1f..95cc6308c3b 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -86,8 +86,7 @@ module Ci # Example Request: # PATCH /builds/:id/trace.txt patch ":id/trace.txt" do - build = Ci::Build.find_by_id(params[:id]) - authenticate_build!(build) + build = authenticate_build! error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range') content_range = request.headers['Content-Range'] @@ -117,8 +116,7 @@ module Ci require_gitlab_workhorse! Gitlab::Workhorse.verify_api_request!(headers) not_allowed! unless Gitlab.config.artifacts.enabled - build = Ci::Build.find_by_id(params[:id]) - authenticate_build!(build) + build = authenticate_build! forbidden!('build is not running') unless build.running? if params[:filesize] @@ -154,8 +152,7 @@ module Ci post ":id/artifacts" do require_gitlab_workhorse! not_allowed! unless Gitlab.config.artifacts.enabled - build = Ci::Build.find_by_id(params[:id]) - authenticate_build!(build) + build = authenticate_build! forbidden!('Build is not running!') unless build.running? artifacts_upload_path = ArtifactUploader.artifacts_upload_path @@ -189,8 +186,7 @@ module Ci # Example Request: # GET /builds/:id/artifacts get ":id/artifacts" do - build = Ci::Build.find_by_id(params[:id]) - authenticate_build!(build) + build = authenticate_build! artifacts_file = build.artifacts_file unless artifacts_file.file_storage? @@ -214,8 +210,7 @@ module Ci # Example Request: # DELETE /builds/:id/artifacts delete ":id/artifacts" do - build = Ci::Build.find_by_id(params[:id]) - authenticate_build!(build) + build = authenticate_build! status(200) build.erase_artifacts! diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 996990b464f..5109dc9670f 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -13,10 +13,14 @@ module Ci forbidden! unless current_runner end - def authenticate_build!(build) + def authenticate_build! + build = Ci::Build.find_by_id(params[:id]) + validate_build!(build) do forbidden! unless build_token_valid?(build) end + + build end def validate_build!(build) diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb index 0a8d05b5fe1..e7c6d2d5346 100644 --- a/lib/gitlab/github_import/branch_formatter.rb +++ b/lib/gitlab/github_import/branch_formatter.rb @@ -11,6 +11,14 @@ module Gitlab sha.present? && ref.present? end + def user + raw_data.user&.login || 'unknown' + end + + def short_sha + Commit.truncate_sha(sha) + end + private def branch_exists? diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb index 4ea0200e89b..6d57ac335eb 100644 --- a/lib/gitlab/github_import/pull_request_formatter.rb +++ b/lib/gitlab/github_import/pull_request_formatter.rb @@ -1,8 +1,8 @@ module Gitlab module GithubImport class PullRequestFormatter < IssuableFormatter - delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true - delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true + delegate :user, :project, :ref, :repo, :sha, to: :source_branch, prefix: true + delegate :user, :exists?, :project, :ref, :repo, :sha, :short_sha, to: :target_branch, prefix: true def attributes { @@ -37,9 +37,20 @@ module Gitlab end def source_branch_name - @source_branch_name ||= begin - source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}" - end + @source_branch_name ||= + if cross_project? || !source_branch_exists? + source_branch_name_prefixed + else + source_branch_ref + end + end + + def source_branch_name_prefixed + "gh-#{target_branch_short_sha}/#{number}/#{source_branch_user}/#{source_branch_ref}" + end + + def source_branch_exists? + !cross_project? && source_branch.exists? end def target_branch @@ -47,9 +58,17 @@ module Gitlab end def target_branch_name - @target_branch_name ||= begin - target_branch_exists? ? target_branch_ref : "pull/#{number}/#{target_branch_ref}" - end + @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed + end + + def target_branch_name_prefixed + "gl-#{target_branch_short_sha}/#{number}/#{target_branch_user}/#{target_branch_ref}" + end + + def cross_project? + return true if source_branch_repo.nil? + + source_branch_repo.id != target_branch_repo.id end private diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake index 40465ea3bf0..62a12174efa 100644 --- a/lib/tasks/karma.rake +++ b/lib/tasks/karma.rake @@ -1,9 +1,10 @@ unless Rails.env.production? namespace :karma do desc 'GitLab | Karma | Generate fixtures for JavaScript tests' - RSpec::Core::RakeTask.new(:fixtures) do |t| + RSpec::Core::RakeTask.new(:fixtures, [:pattern]) do |t, args| + args.with_defaults(pattern: 'spec/javascripts/fixtures/*.rb') ENV['NO_KNAPSACK'] = 'true' - t.pattern = 'spec/javascripts/fixtures/*.rb' + t.pattern = args[:pattern] t.rspec_opts = '--format documentation' end diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index 7072bd5e87c..f0395fccfca 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -35,6 +35,13 @@ describe Dashboard::TodosController do expect(assigns(:todos).current_page).to eq(last_page) expect(response).to have_http_status(200) end + + it 'does not redirect to external sites when provided a host field' do + external_host = "www.example.com" + get :index, page: (last_page + 1).to_param, host: external_host + + expect(response).to redirect_to(dashboard_todos_path(page: last_page)) + end end end diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index 7c75815f3c4..6724b474179 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -96,12 +96,19 @@ describe Projects::ImportsController do } end - it 'redirects to params[:to]' do + it 'redirects to internal params[:to]' do get :show, namespace_id: project.namespace.to_param, project_id: project, continue: params expect(flash[:notice]).to eq params[:notice] expect(response).to redirect_to params[:to] end + + it 'does not redirect to external params[:to]' do + params[:to] = "//google.com" + + get :show, namespace_id: project.namespace.to_param, project_id: project, continue: params + expect(response).not_to redirect_to params[:to] + end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 972005ede3a..b16a8db01c4 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -83,6 +83,17 @@ describe Projects::IssuesController do expect(assigns(:issues).current_page).to eq(last_page) expect(response).to have_http_status(200) end + + it 'does not redirect to external sites when provided a host field' do + external_host = "www.example.com" + get :index, + namespace_id: project.namespace.to_param, + project_id: project, + page: (last_page + 1).to_param, + host: external_host + + expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope])) + end end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index c310d830e81..62f48f9d590 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -176,6 +176,18 @@ describe Projects::MergeRequestsController do expect(assigns(:merge_requests).current_page).to eq(last_page) expect(response).to have_http_status(200) end + + it 'does not redirect to external sites when provided a host field' do + external_host = "www.example.com" + get :index, + namespace_id: project.namespace.to_param, + project_id: project, + state: 'opened', + page: (last_page + 1).to_param, + host: external_host + + expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope])) + end end context 'when filtering by opened state' do diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 8cc216445eb..5afd3132da6 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -59,4 +59,20 @@ describe RegistrationsController do end end end + + describe '#destroy' do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'schedules the user for destruction' do + expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id) + + post(:destroy) + + expect(response.status).to eq(302) + end + end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 8cc0996acab..82f1dea99a0 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -43,6 +43,18 @@ feature 'Create New Merge Request', feature: true, js: true do visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id }) expect(page).not_to have_content private_project.path_with_namespace + expect(page).to have_content project.path_with_namespace + end + end + + context 'when source project cannot be viewed by the current user' do + it 'does not leak the private project name & namespace' do + private_project = create(:project, :private) + + visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { source_project_id: private_project.id }) + + expect(page).not_to have_content private_project.path_with_namespace + expect(page).to have_content project.path_with_namespace end end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index 81ba693f2f3..0acbd584f08 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -2,8 +2,10 @@ require 'spec_helper' describe EventsHelper do describe '#event_note' do + let(:user) { build(:user) } + before do - allow(helper).to receive(:current_user).and_return(double) + allow(helper).to receive(:current_user).and_return(user) end it 'displays one line of plain text without alteration' do @@ -62,11 +64,26 @@ describe EventsHelper do expect(helper.event_note(input)).to eq(expected) end - it 'preserves style attribute within a tag' do - input = '<span class="" style="background-color: #44ad8e; color: #FFFFFF;"></span>' - expected = '<p><span style="background-color: #44ad8e; color: #FFFFFF;"></span></p>' + context 'labels formatting' do + let(:input) { 'this should be ~label_1' } - expect(helper.event_note(input)).to eq(expected) + def format_event_note(project) + create(:label, title: 'label_1', project: project) + + helper.event_note(input, { project: project }) + end + + it 'preserves style attribute for a label that can be accessed by current_user' do + project = create(:empty_project, :public) + + expect(format_event_note(project)).to match(/span class=.*style=.*/) + end + + it 'does not style a label that can not be accessed by current_user' do + project = create(:empty_project, :private) + + expect(format_event_note(project)).to eq("<p>#{input}</p>") + end end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index fc6ad6419ac..44312ada438 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -167,6 +167,7 @@ describe ProjectsHelper do before do allow(project).to receive(:repository_storage_path).and_return('/base/repo/path') + allow(Settings.shared).to receive(:[]).with('path').and_return('/base/repo/export/path') end it 'removes the repo path' do @@ -175,6 +176,13 @@ describe ProjectsHelper do expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git') end + + it 'removes the temporary repo path used for uploads/exports' do + repo = '/base/repo/export/path/tmp/project_exports/uploads/test.tar.gz' + import_error = "Unable to decompress #{repo}\n" + + expect(sanitize_repo_path(project, import_error)).to eq('Unable to decompress [REPO EXPORT PATH]/uploads/test.tar.gz') + end end describe '#last_push_event' do diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index f09c57978a1..5b76e2303ab 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,15 +1,18 @@ /* global pipeline, Vue */ +const PipelinesTable = require('~/commit/pipelines/pipelines_table'); + require('~/flash'); require('~/commit/pipelines/pipelines_store'); require('~/commit/pipelines/pipelines_service'); -require('~/commit/pipelines/pipelines_table'); require('~/vue_shared/vue_resource_interceptor'); const pipeline = require('./mock_data'); describe('Pipelines table in Commits and Merge requests', () => { preloadFixtures('static/pipelines_table.html.raw'); + let component; + beforeEach(() => { loadFixtures('static/pipelines_table.html.raw'); }); @@ -24,19 +27,20 @@ describe('Pipelines table in Commits and Merge requests', () => { beforeEach(() => { Vue.http.interceptors.push(pipelinesEmptyResponse); + + component = new PipelinesTable({ + el: document.querySelector('#commit-pipeline-table-view'), + }); }); afterEach(() => { Vue.http.interceptors = _.without( Vue.http.interceptors, pipelinesEmptyResponse, ); + component.$destroy(); }); it('should render the empty state', (done) => { - const component = new gl.commits.pipelines.PipelinesTableView({ - el: document.querySelector('#commit-pipeline-table-view'), - }); - setTimeout(() => { expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); done(); @@ -53,19 +57,20 @@ describe('Pipelines table in Commits and Merge requests', () => { beforeEach(() => { Vue.http.interceptors.push(pipelinesResponse); + + component = new PipelinesTable({ + el: document.querySelector('#commit-pipeline-table-view'), + }); }); afterEach(() => { Vue.http.interceptors = _.without( Vue.http.interceptors, pipelinesResponse, ); + component.$destroy(); }); it('should render a table with the received pipelines', (done) => { - const component = new gl.commits.pipelines.PipelinesTableView({ - el: document.querySelector('#commit-pipeline-table-view'), - }); - setTimeout(() => { expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); done(); @@ -83,19 +88,20 @@ describe('Pipelines table in Commits and Merge requests', () => { beforeEach(() => { Vue.http.interceptors.push(pipelinesErrorResponse); + + component = new PipelinesTable({ + el: document.querySelector('#commit-pipeline-table-view'), + }); }); afterEach(() => { Vue.http.interceptors = _.without( Vue.http.interceptors, pipelinesErrorResponse, ); + component.$destroy(); }); it('should render empty state', (done) => { - const component = new gl.commits.pipelines.PipelinesTableView({ - el: document.querySelector('#commit-pipeline-table-view'), - }); - setTimeout(() => { expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); done(); diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js index edd0cad32d0..eb8e5e50434 100644 --- a/spec/javascripts/environments/environment_spec.js +++ b/spec/javascripts/environments/environment_spec.js @@ -91,6 +91,10 @@ describe('Environment', () => { }); describe('pagination', () => { + afterEach(() => { + window.history.pushState({}, null, ''); + }); + it('should render pagination', (done) => { setTimeout(() => { expect( diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index ee893b76c84..fddeaaf504d 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -6,6 +6,15 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } + let(:pipeline) do + create( + :ci_pipeline, + project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha + ) + end render_views @@ -18,7 +27,8 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont end it 'merge_requests/merge_request_with_task_list.html.raw' do |example| - merge_request = create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') + create(:ci_build, :pending, pipeline: pipeline) + render_merge_request(example.description, merge_request) end diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index f4d3e77e515..7c7b334c5d8 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -46,6 +46,10 @@ require('~/lib/utils/common_utils'); spyOn(window.document, 'getElementById').and.callThrough(); }); + afterEach(() => { + window.history.pushState({}, null, ''); + }); + function expectGetElementIdToHaveBeenCalledWith(elementId) { expect(window.document.getElementById).toHaveBeenCalledWith(elementId); } @@ -75,11 +79,56 @@ require('~/lib/utils/common_utils'); }); }); + describe('gl.utils.setParamInURL', () => { + afterEach(() => { + window.history.pushState({}, null, ''); + }); + + it('should return the parameter', () => { + window.history.replaceState({}, null, ''); + + expect(gl.utils.setParamInURL('page', 156)).toBe('?page=156'); + expect(gl.utils.setParamInURL('page', '156')).toBe('?page=156'); + }); + + it('should update the existing parameter when its a number', () => { + window.history.pushState({}, null, '?page=15'); + + expect(gl.utils.setParamInURL('page', 16)).toBe('?page=16'); + expect(gl.utils.setParamInURL('page', '16')).toBe('?page=16'); + expect(gl.utils.setParamInURL('page', true)).toBe('?page=true'); + }); + + it('should update the existing parameter when its a string', () => { + window.history.pushState({}, null, '?scope=all'); + + expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished'); + }); + + it('should update the existing parameter when more than one parameter exists', () => { + window.history.pushState({}, null, '?scope=all&page=15'); + + expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15'); + }); + + it('should add a new parameter to the end of the existing ones', () => { + window.history.pushState({}, null, '?scope=all'); + + expect(gl.utils.setParamInURL('page', 16)).toBe('?scope=all&page=16'); + expect(gl.utils.setParamInURL('page', '16')).toBe('?scope=all&page=16'); + expect(gl.utils.setParamInURL('page', true)).toBe('?scope=all&page=true'); + }); + }); + describe('gl.utils.getParameterByName', () => { beforeEach(() => { window.history.pushState({}, null, '?scope=all&p=2'); }); + afterEach(() => { + window.history.replaceState({}, null, null); + }); + it('should return valid parameter', () => { const value = gl.utils.getParameterByName('scope'); expect(value).toBe('all'); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 7506e6ab49e..7b9632be84e 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -38,6 +38,10 @@ require('vendor/jquery.scrollTo'); } }); + afterEach(function () { + this.class.destroy(); + }); + describe('#activateTab', function () { beforeEach(function () { spyOn($, 'ajax').and.callFake(function () {}); @@ -200,6 +204,42 @@ require('vendor/jquery.scrollTo'); expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); }); }); + + describe('#tabShown', () => { + beforeEach(function () { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + }); + + describe('with "Side-by-side"/parallel diff view', () => { + beforeEach(function () { + this.class.diffViewType = () => 'parallel'; + }); + + it('maintains `container-limited` for pipelines tab', function (done) { + const asyncClick = function (selector) { + return new Promise((resolve) => { + setTimeout(() => { + document.querySelector(selector).click(); + resolve(); + }); + }); + }; + + asyncClick('.merge-request-tabs .pipelines-tab a') + .then(() => asyncClick('.merge-request-tabs .diffs-tab a')) + .then(() => asyncClick('.merge-request-tabs .pipelines-tab a')) + .then(() => { + const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited'); + expect(hasContainerLimitedClass).toBe(true); + }) + .then(done) + .catch((err) => { + done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`); + }); + }); + }); + }); + describe('#loadDiff', function () { it('requires an absolute pathname', function () { spyOn($, 'ajax').and.callFake(function (options) { diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 4ac7e911740..c5c30fdda90 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -78,5 +78,11 @@ require('~/extensions/jquery.js'); expect(todoToggleSpy.calls.count()).toEqual(1); }); + + it('should not hide collapsed icons', () => { + [].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => { + expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy(); + }); + }); }); }).call(window); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js index 54d81e2ea7d..905ac0ec723 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js @@ -1,6 +1,9 @@ -require('~/vue_shared/components/pipelines_table'); require('~/lib/utils/datetime_utility'); +const Vue = require('vue'); const pipeline = require('../../commit/pipelines/mock_data'); +const PipelinesTable = require('~/vue_shared/components/pipelines_table'); + +const PipelinesTableComponent = Vue.extend(PipelinesTable); describe('Pipelines Table', () => { preloadFixtures('static/environments/element.html.raw'); @@ -12,7 +15,7 @@ describe('Pipelines Table', () => { describe('table', () => { let component; beforeEach(() => { - component = new gl.pipelines.PipelinesTableComponent({ + component = new PipelinesTableComponent({ el: document.querySelector('.test-dom-element'), propsData: { pipelines: [], @@ -21,6 +24,10 @@ describe('Pipelines Table', () => { }); }); + afterEach(() => { + component.$destroy(); + }); + it('should render a table', () => { expect(component.$el).toEqual('TABLE'); }); @@ -37,7 +44,7 @@ describe('Pipelines Table', () => { describe('without data', () => { it('should render an empty table', () => { - const component = new gl.pipelines.PipelinesTableComponent({ + const component = new PipelinesTableComponent({ el: document.querySelector('.test-dom-element'), propsData: { pipelines: [], @@ -50,7 +57,7 @@ describe('Pipelines Table', () => { describe('with data', () => { it('should render rows', () => { - const component = new gl.pipelines.PipelinesTableComponent({ + const component = new PipelinesTableComponent({ el: document.querySelector('.test-dom-element'), propsData: { pipelines: [pipeline], diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index 9cb067921a7..1e3e60f249a 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -136,6 +136,10 @@ describe('Pagination component', () => { }); describe('paramHelper', () => { + afterEach(() => { + window.history.pushState({}, null, ''); + }); + it('can parse url parameters correctly', () => { window.history.pushState({}, null, '?scope=all&p=2'); diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb new file mode 100644 index 00000000000..897288b8ad5 --- /dev/null +++ b/spec/lib/banzai/filter/markdown_filter_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Banzai::Filter::MarkdownFilter, lib: true do + include FilterSpecHelper + + context 'code block' do + it 'adds language to lang attribute when specified' do + result = filter("```html\nsome code\n```") + + expect(result).to start_with("\n<pre><code lang=\"html\">") + end + + it 'does not add language to lang attribute when not specified' do + result = filter("```\nsome code\n```") + + expect(result).to start_with("\n<pre><code>") + end + end +end diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index b4cd5f63a15..fdbc65b5e00 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -49,11 +49,12 @@ describe Banzai::Filter::SanitizationFilter, lib: true do instance = described_class.new('Foo') 3.times { instance.whitelist } - expect(instance.whitelist[:transformers].size).to eq 5 + expect(instance.whitelist[:transformers].size).to eq 4 end - it 'allows syntax highlighting' do - exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} + it 'sanitizes `class` attribute from all elements' do + act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>} + exp = %q{<pre><code><span class="k">def</span></code></pre>} expect(filter(act).to_html).to eq exp end diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index 69e3c52b35a..bfd11941867 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -12,14 +12,14 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do context "when a valid language is specified" do it "highlights as that language" do - result = filter('<pre><code class="ruby">def fun end</code></pre>') + result = filter('<pre><code lang="ruby">def fun end</code></pre>') expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>') end end context "when an invalid language is specified" do it "highlights as plaintext" do - result = filter('<pre><code class="gnuplot">This is a test</code></pre>') + result = filter('<pre><code lang="gnuplot">This is a test</code></pre>') expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>This is a test</code></pre>') end end @@ -30,7 +30,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do end it "highlights as plaintext" do - result = filter('<pre><code class="ruby">This is a test</code></pre>') + result = filter('<pre><code lang="ruby">This is a test</code></pre>') expect(result.to_html).to eq('<pre class="code highlight" lang="" v-pre="true"><code>This is a test</code></pre>') end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 33d83d6d2f1..c131f325c0c 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -130,9 +130,9 @@ describe Gitlab::GithubImport::Importer, lib: true do let!(:user) { create(:user, email: octocat.email) } let(:repository) { double(id: 1, fork: false) } let(:source_sha) { create(:commit, project: project).id } - let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } + let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha, user: octocat) } let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } - let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) } + let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha, user: octocat) } let(:pull_request) do double( number: 1347, diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb index e46be18aa99..736e3bcad63 100644 --- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -4,13 +4,18 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do let(:client) { double } let(:project) { create(:project, :repository) } let(:source_sha) { create(:commit, project: project).id } - let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id } + let(:target_commit) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit) } + let(:target_sha) { target_commit.id } + let(:target_short_sha) { target_commit.id.to_s[0..7] } let(:repository) { double(id: 1, fork: false) } let(:source_repo) { repository } let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) } + let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') } let(:target_repo) { repository } - let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } - let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') } + let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha, user: octocat) } + let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) } + let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) } + let(:branch_deleted_repo) { double(ref: 'master', repo: nil, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) } let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') } let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } @@ -201,8 +206,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do context 'when source branch does not exist' do let(:raw_data) { double(base_data.merge(head: removed_branch)) } - it 'prefixes branch name with pull request number' do - expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch' + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/removed-branch" + end + end + + context 'when source branch is from a fork' do + let(:raw_data) { double(base_data.merge(head: forked_branch)) } + + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master" + end + end + + context 'when source branch is from a deleted fork' do + let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) } + + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master" end end end @@ -219,8 +240,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do context 'when target branch does not exist' do let(:raw_data) { double(base_data.merge(base: removed_branch)) } - it 'prefixes branch name with pull request number' do - expect(pull_request.target_branch_name).to eq 'pull/1347/removed-branch' + it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do + expect(pull_request.target_branch_name).to eq 'gl-2e5d3239/1347/octocat/removed-branch' end end end @@ -271,6 +292,40 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do end end + describe '#cross_project?' do + context 'when source and target repositories are different' do + let(:raw_data) { double(base_data.merge(head: forked_branch)) } + + it 'returns true' do + expect(pull_request.cross_project?).to eq true + end + end + + context 'when source repository does not exist anymore' do + let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) } + + it 'returns true' do + expect(pull_request.cross_project?).to eq true + end + end + + context 'when source and target repositories are the same' do + let(:raw_data) { double(base_data.merge(head: source_branch)) } + + it 'returns false' do + expect(pull_request.cross_project?).to eq false + end + end + end + + describe '#source_branch_exists?' do + let(:raw_data) { double(base_data.merge(head: forked_branch)) } + + it 'returns false when is a cross_project' do + expect(pull_request.source_branch_exists?).to eq false + end + end + describe '#url' do let(:raw_data) { double(base_data) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 9aba1d75612..61d965e8974 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -623,64 +623,6 @@ describe API::MergeRequests, api: true do end end - describe "POST /projects/:id/merge_requests/:merge_request_iid/comments" do - it "returns comment" do - original_count = merge_request.notes.size - - post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user), note: "My comment" - - expect(response).to have_http_status(201) - expect(json_response['note']).to eq('My comment') - expect(json_response['author']['name']).to eq(user.name) - expect(json_response['author']['username']).to eq(user.username) - expect(merge_request.reload.notes.size).to eq(original_count + 1) - end - - it "returns 400 if note is missing" do - post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user) - expect(response).to have_http_status(400) - end - - it "returns 404 if merge request iid is invalid" do - post api("/projects/#{project.id}/merge_requests/404/comments", user), - note: 'My comment' - expect(response).to have_http_status(404) - end - - it "returns 404 if merge request id is used instead of iid" do - post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), - note: 'My comment' - expect(response).to have_http_status(404) - end - end - - describe "GET :id/merge_requests/:merge_request_iid/comments" do - let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } - let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } - - it "returns merge_request comments ordered by created_at" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user) - - expect(response).to have_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['note']).to eq("a comment on a MR") - expect(json_response.first['author']['id']).to eq(user.id) - expect(json_response.last['note']).to eq("another comment on a MR") - end - - it "returns a 404 error if merge_request_iid is invalid" do - get api("/projects/#{project.id}/merge_requests/999/comments", user) - expect(response).to have_http_status(404) - end - - it "returns a 404 error if merge_request id is used instead of iid" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) - expect(response).to have_http_status(404) - end - end - describe 'GET :id/merge_requests/:merge_request_iid/closes_issues' do it 'returns the issue that will be closed on merge' do issue = create(:issue, project: project) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 347f8f6fa3b..d8eb8ce921e 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -34,7 +34,7 @@ describe API::Notes, api: true do describe "GET /projects/:id/noteable/:noteable_id/notes" do context "when noteable is an Issue" do it "returns an array of issue notes" do - get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -50,7 +50,7 @@ describe API::Notes, api: true do context "and current user cannot view the notes" do it "returns an empty array" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -62,7 +62,7 @@ describe API::Notes, api: true do before { ext_issue.update_attributes(confidential: true) } it "returns 404" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user) expect(response).to have_http_status(404) end @@ -70,7 +70,7 @@ describe API::Notes, api: true do context "and current user can view the note" do it "returns an empty array" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", private_user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -106,7 +106,7 @@ describe API::Notes, api: true do context "when noteable is a Merge Request" do it "returns an array of merge_requests notes" do - get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user) expect(response).to have_http_status(200) expect(response).to include_pagination_headers @@ -131,21 +131,21 @@ describe API::Notes, api: true do describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do context "when noteable is an Issue" do it "returns an issue note by id" do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", user) expect(response).to have_http_status(200) expect(json_response['body']).to eq(issue_note.note) end it "returns a 404 error if issue note not found" do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user) expect(response).to have_http_status(404) end context "and current user cannot view the note" do it "returns a 404 error" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", user) expect(response).to have_http_status(404) end @@ -154,7 +154,7 @@ describe API::Notes, api: true do before { issue.update_attributes(confidential: true) } it "returns 404" do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user) + get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", private_user) expect(response).to have_http_status(404) end @@ -162,7 +162,7 @@ describe API::Notes, api: true do context "and current user can view the note" do it "returns an issue note by id" do - get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", private_user) expect(response).to have_http_status(200) expect(json_response['body']).to eq(cross_reference_note.note) @@ -190,7 +190,7 @@ describe API::Notes, api: true do describe "POST /projects/:id/noteable/:noteable_id/notes" do context "when noteable is an Issue" do it "creates a new issue note" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!' expect(response).to have_http_status(201) expect(json_response['body']).to eq('hi!') @@ -198,13 +198,13 @@ describe API::Notes, api: true do end it "returns a 400 bad request error if body not given" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user) expect(response).to have_http_status(400) end it "returns a 401 unauthorized error if user not authenticated" do - post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes"), body: 'hi!' expect(response).to have_http_status(401) end @@ -212,7 +212,7 @@ describe API::Notes, api: true do context 'when an admin or owner makes the request' do it 'accepts the creation date to be set' do creation_time = 2.weeks.ago - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!', created_at: creation_time expect(response).to have_http_status(201) @@ -226,7 +226,7 @@ describe API::Notes, api: true do let(:issue2) { create(:issue, project: project) } it 'creates a new issue note' do - post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' + post api("/projects/#{project.id}/issues/#{issue2.iid}/notes", user), body: ':+1:' expect(response).to have_http_status(201) expect(json_response['body']).to eq(':+1:') @@ -235,7 +235,7 @@ describe API::Notes, api: true do context 'when the user is posting an award emoji on his/her own issue' do it 'creates a new issue note' do - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: ':+1:' expect(response).to have_http_status(201) expect(json_response['body']).to eq(':+1:') @@ -270,7 +270,7 @@ describe API::Notes, api: true do project = create(:empty_project, :private) { |p| p.add_guest(user) } issue = create(:issue, :confidential, project: project) - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'Foo' expect(response).to have_http_status(404) @@ -285,7 +285,7 @@ describe API::Notes, api: true do # from a different project, see #15577 # before do - post api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user), + post api("/projects/#{private_issue.project.id}/issues/#{private_issue.iid}/notes", user), body: 'Hi!' end @@ -303,14 +303,14 @@ describe API::Notes, api: true do it "creates an activity event when an issue note is created" do expect(Event).to receive(:create) - post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!' end end describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do context 'when noteable is an Issue' do it 'returns modified note' do - put api("/projects/#{project.id}/issues/#{issue.id}/"\ + put api("/projects/#{project.id}/issues/#{issue.iid}/"\ "notes/#{issue_note.id}", user), body: 'Hello!' expect(response).to have_http_status(200) @@ -318,14 +318,14 @@ describe API::Notes, api: true do end it 'returns a 404 error when note id not found' do - put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), + put api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user), body: 'Hello!' expect(response).to have_http_status(404) end it 'returns a 400 bad request error if body not given' do - put api("/projects/#{project.id}/issues/#{issue.id}/"\ + put api("/projects/#{project.id}/issues/#{issue.iid}/"\ "notes/#{issue_note.id}", user) expect(response).to have_http_status(400) @@ -351,7 +351,7 @@ describe API::Notes, api: true do context 'when noteable is a Merge Request' do it 'returns modified note' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/"\ "notes/#{merge_request_note.id}", user), body: 'Hello!' expect(response).to have_http_status(200) @@ -359,7 +359,7 @@ describe API::Notes, api: true do end it 'returns a 404 error when note id not found' do - put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/"\ "notes/12345", user), body: "Hello!" expect(response).to have_http_status(404) @@ -370,18 +370,18 @@ describe API::Notes, api: true do describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do context 'when noteable is an Issue' do it 'deletes a note' do - delete api("/projects/#{project.id}/issues/#{issue.id}/"\ + delete api("/projects/#{project.id}/issues/#{issue.iid}/"\ "notes/#{issue_note.id}", user) expect(response).to have_http_status(204) # Check if note is really deleted - delete api("/projects/#{project.id}/issues/#{issue.id}/"\ + delete api("/projects/#{project.id}/issues/#{issue.iid}/"\ "notes/#{issue_note.id}", user) expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do - delete api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) + delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user) expect(response).to have_http_status(404) end @@ -410,18 +410,18 @@ describe API::Notes, api: true do context 'when noteable is a Merge Request' do it 'deletes a note' do delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) + "#{merge_request.iid}/notes/#{merge_request_note.id}", user) expect(response).to have_http_status(204) # Check if note is really deleted delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/#{merge_request_note.id}", user) + "#{merge_request.iid}/notes/#{merge_request_note.id}", user) expect(response).to have_http_status(404) end it 'returns a 404 error when note id not found' do delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/12345", user) + "#{merge_request.iid}/notes/12345", user) expect(response).to have_http_status(404) end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 04e7837fd7a..f793c0db2f3 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -676,7 +676,7 @@ describe API::Users, api: true do before { admin } it "deletes user" do - delete api("/users/#{user.id}", admin) + Sidekiq::Testing.inline! { delete api("/users/#{user.id}", admin) } expect(response).to have_http_status(204) expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound @@ -684,23 +684,23 @@ describe API::Users, api: true do end it "does not delete for unauthenticated user" do - delete api("/users/#{user.id}") + Sidekiq::Testing.inline! { delete api("/users/#{user.id}") } expect(response).to have_http_status(401) end it "is not available for non admin users" do - delete api("/users/#{user.id}", user) + Sidekiq::Testing.inline! { delete api("/users/#{user.id}", user) } expect(response).to have_http_status(403) end it "returns 404 for non-existing user" do - delete api("/users/999999", admin) + Sidekiq::Testing.inline! { delete api("/users/999999", admin) } expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') end it "returns a 404 for invalid ID" do - delete api("/users/ASDF", admin) + Sidekiq::Testing.inline! { delete api("/users/ASDF", admin) } expect(response).to have_http_status(404) end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index adfa75a524f..f78b4928dc3 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -4,6 +4,8 @@ describe MergeRequests::BuildService, services: true do include RepoHelpers let(:project) { create(:project) } + let(:source_project) { nil } + let(:target_project) { nil } let(:user) { create(:user) } let(:issue_confidential) { false } let(:issue) { create(:issue, project: project, title: 'A bug', confidential: issue_confidential) } @@ -20,7 +22,9 @@ describe MergeRequests::BuildService, services: true do MergeRequests::BuildService.new(project, user, description: description, source_branch: source_branch, - target_branch: target_branch) + target_branch: target_branch, + source_project: source_project, + target_project: target_project) end before do @@ -256,5 +260,41 @@ describe MergeRequests::BuildService, services: true do ) end end + + context 'target_project is set and accessible by current_user' do + let(:target_project) { create(:project, :public, :repository)} + let(:commits) { Commit.decorate([commit_1], project) } + + it 'sets target project correctly' do + expect(merge_request.target_project).to eq(target_project) + end + end + + context 'target_project is set but not accessible by current_user' do + let(:target_project) { create(:project, :private, :repository)} + let(:commits) { Commit.decorate([commit_1], project) } + + it 'sets target project correctly' do + expect(merge_request.target_project).to eq(project) + end + end + + context 'source_project is set and accessible by current_user' do + let(:source_project) { create(:project, :public, :repository)} + let(:commits) { Commit.decorate([commit_1], project) } + + it 'sets target project correctly' do + expect(merge_request.source_project).to eq(source_project) + end + end + + context 'source_project is set but not accessible by current_user' do + let(:source_project) { create(:project, :private, :repository)} + let(:commits) { Commit.decorate([commit_1], project) } + + it 'sets target project correctly' do + expect(merge_request.source_project).to eq(project) + end + end end end diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb index 922e82445d0..e65109e6746 100644 --- a/spec/services/users/destroy_spec.rb +++ b/spec/services/users/destroy_spec.rb @@ -17,13 +17,28 @@ describe Users::DestroyService, services: true do expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound) end - it 'will delete the project in the near future' do - expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once + it 'will delete the project' do + expect_any_instance_of(Projects::DestroyService).to receive(:execute).once service.execute(user) end end + context 'projects in pending_delete' do + before do + project.pending_delete = true + project.save + end + + it 'destroys a project in pending_delete' do + expect_any_instance_of(Projects::DestroyService).to receive(:execute).once + + service.execute(user) + + expect { Project.find(project.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + context "a deleted user's issues" do let(:project) { create :project } diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 9da54cb886d..aad8ec38a9c 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -91,7 +91,7 @@ module TestEnv tmp_test_path = Rails.root.join('tmp', 'tests', '**') Dir[tmp_test_path].each do |entry| - unless File.basename(entry) =~ /\Agitlab-(shell|test|test_bare|test-fork)\z/ + unless File.basename(entry) =~ /\Agitlab-(shell|test|test_bare|test-fork|test-fork_bare)\z/ FileUtils.rm_rf(entry) end end |