Commit 76f90950 authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'ce-to-ee-2018-01-19' into 'master'

CE upstream - Friday

Closes #1366, gitlab-ce#42157, and gitlab-ce#41989

See merge request gitlab-org/gitlab-ee!4161
parents 1f3e8f4f 44df7165
This diff is collapsed.
......@@ -353,10 +353,10 @@ group :development, :test do
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'gitlab-styles', '~> 2.2.0', require: false
gem 'gitlab-styles', '~> 2.3', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines
gem 'rubocop', '~> 0.52.0'
gem 'rubocop-rspec', '~> 1.20.1'
gem 'rubocop', '~> 0.52.1'
gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false
......
......@@ -329,7 +329,7 @@ GEM
posix-spawn (~> 0.3)
gitlab-license (1.0.0)
gitlab-markup (1.6.3)
gitlab-styles (2.2.0)
gitlab-styles (2.3.0)
rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
......@@ -612,7 +612,7 @@ GEM
rubypants (~> 0.2)
orm_adapter (0.5.0)
os (0.9.6)
parallel (1.12.0)
parallel (1.12.1)
parser (2.4.0.2)
ast (~> 2.3)
parslet (1.5.0)
......@@ -815,7 +815,7 @@ GEM
pg
rails
sqlite3
rubocop (0.52.0)
rubocop (0.52.1)
parallel (~> 1.10)
parser (>= 2.4.0.2, < 3.0)
powerpack (~> 0.1)
......@@ -824,8 +824,8 @@ GEM
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
rubocop-rspec (1.20.1)
rubocop (>= 0.51.0)
rubocop-rspec (1.22.1)
rubocop (>= 0.52.1)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.16.2)
......@@ -1096,7 +1096,7 @@ DEPENDENCIES
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
gitlab-markup (~> 1.6.2)
gitlab-styles (~> 2.2.0)
gitlab-styles (~> 2.3)
gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
......@@ -1200,8 +1200,8 @@ DEPENDENCIES
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.52.0)
rubocop-rspec (~> 1.20.1)
rubocop (~> 0.52.1)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
ruby_parser (~> 3.8)
......
......@@ -245,6 +245,7 @@ const Api = {
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${options.url}: ${errorThrown}`);
error.textStatus = textStatus;
if (jqXHR && jqXHR.responseJSON) error.responseJSON = jqXHR.responseJSON;
reject(error);
},
);
......
......@@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
flashEl.addEventListener('transitionend', () => {
flashEl.remove();
if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
}, {
once: true,
passive: true,
......@@ -64,6 +65,7 @@ const createFlash = function createFlash(
parent = document,
actionConfig = null,
fadeTransition = true,
addBodyClass = false,
) {
const flashContainer = parent.querySelector('.flash-container');
......@@ -86,6 +88,8 @@ const createFlash = function createFlash(
flashContainer.style.display = 'block';
if (addBodyClass) document.body.classList.add('flash-shown');
return flashContainer;
};
......
......@@ -68,12 +68,8 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
this.$store.dispatch('getTreeData', {
projectId: this.currentProjectId,
branch: this.currentBranchId,
endpoint: `/tree/${this.currentBranchId}`,
force: true,
});
this.commitMessage = '';
this.startNewMR = false;
})
.catch(() => {
this.submitCommitsLoading = false;
......@@ -153,6 +149,7 @@ you started editing. Would you like to create a new branch?`)"
type="submit"
:disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
>
<i
v-if="submitCommitsLoading"
......
......@@ -70,7 +70,10 @@ export default {
this.editor.createInstance(this.$refs.editor);
})
.then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.'));
.catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
},
setupEditor() {
if (!this.activeFile) return;
......
......@@ -37,9 +37,12 @@
return this.file.type === 'tree';
},
levelIndentation() {
return {
marginLeft: `${this.file.level * 16}px`,
};
if (this.file.level > 0) {
return {
marginLeft: `${this.file.level * 16}px`,
};
}
return {};
},
shortId() {
return this.file.id.substr(0, 8);
......@@ -114,7 +117,7 @@
/>
<i
class="fa"
v-if="changedClass"
v-if="file.changed || file.tempFile"
:class="changedClass"
aria-hidden="true"
>
......
......@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => {
}
})
.catch((e) => {
flash('Error while loading the branch files. Please try again.');
flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true);
throw e;
});
}
})
.catch((e) => {
flash('Error while loading the project data. Please try again.');
flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
throw e;
});
}
......
......@@ -55,7 +55,7 @@ export default class Editor {
attachModel(model) {
this.instance.setModel(model.getModel());
this.dirtyDiffController.attachModel(model);
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model;
......@@ -68,7 +68,7 @@ export default class Editor {
return acc;
}, {}));
this.dirtyDiffController.reDecorate(model);
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
clearEditor() {
......
......@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
import { stripHtml } from '../../lib/utils/text_utility';
export const redirectToUrl = (_, url) => visitUrl(url);
......@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) =>
return false;
})
.catch(() => flash('Error checking branch data. Please try again.'));
.catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
export const commitChanges = (
{ commit, state, dispatch, getters },
......@@ -92,7 +93,7 @@ export const commitChanges = (
.then((data) => {
const { branch } = payload;
if (!data.short_id) {
flash(data.message);
flash(data.message, 'alert', document, null, false, true);
return;
}
......@@ -105,19 +106,25 @@ export const commitChanges = (
},
};
let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
if (data.stats) {
commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
}
flash(
`Your changes have been committed. Commit ${data.short_id} with ${
data.stats.additions
} additions, ${data.stats.deletions} deletions.`,
commitMsg,
'notice',
);
document,
null,
false,
true);
window.dispatchEvent(new Event('resize'));
if (newMr) {
dispatch('discardAllChanges');
dispatch(
'redirectToUrl',
`${
selectedProject.web_url
}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
`${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
);
} else {
commit(types.SET_BRANCH_WORKING_REFERENCE, {
......@@ -134,12 +141,18 @@ export const commitChanges = (
});
dispatch('discardAllChanges');
dispatch('closeAllFiles');
window.scrollTo(0, 0);
}
})
.catch(() => flash('Error committing changes. Please try again.'));
.catch((err) => {
let errMsg = 'Error committing changes. Please try again.';
if (err.responseJSON && err.responseJSON.message) {
errMsg += ` (${stripHtml(err.responseJSON.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
});
export const createTempEntry = (
{ state, dispatch },
......
......@@ -17,7 +17,7 @@ export const getBranchData = (
resolve(data);
})
.catch(() => {
flash('Error loading branch data. Please try again.');
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
......
......@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
flash('Error loading file data. Please try again.');
flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
});
};
......@@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile
.then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() => flash('Error loading file content. Please try again.'));
.catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
}
};
export const setFileEOL = ({ state, commit }, { eol }) => {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
}
};
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
if (state.selectedFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
}
};
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
......@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI
url: newUrl,
});
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
commit(types.CREATE_TMP_FILE, {
parent,
......
......@@ -18,7 +18,7 @@ export const getProjectData = (
resolve(data);
})
.catch(() => {
flash('Error loading project data. Please try again.');
flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
......
......@@ -52,7 +52,7 @@ export const getTreeData = (
resolve(data);
})
.catch((e) => {
flash('Error loading tree data. Please try again.');
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
if (tree) commit(types.TOGGLE_LOADING, tree);
reject(e);
});
......@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
dispatch('getLastCommitData', tree);
})
.catch(() => flash('Error fetching log data.'));
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const updateDirectoryData = (
......
......@@ -64,7 +64,7 @@ export default {
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',
content: file.raw,
changed: false,
});
},
......
......@@ -152,6 +152,13 @@
hasUpdated() {
return !!this.state.updatedAt;
},
issueChanged() {
const descriptionChanged =
this.initialDescriptionText !== this.store.formState.description;
const titleChanged =
this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
},
created() {
this.service = new Service(this.endpoint);
......@@ -176,6 +183,8 @@
}
});
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
......@@ -186,8 +195,17 @@
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
},
methods: {
handleBeforeUnloadEvent(e) {
const event = e;
if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?';
}
return undefined;
},
openForm() {
if (!this.showForm) {
this.showForm = true;
......
......@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace
* @returns {String}
*/
export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
......@@ -66,9 +66,7 @@
<template>
<div class="note-header-info">
<a :href="author.path">
<span class="note-header-author-name">
{{ author.name }}
</span>
<span class="note-header-author-name">{{ author.name }}</span>
<span class="note-headline-light">
@{{ author.username }}
</span>
......
import Compare from '~/compare';
import MergeRequest from '~/merge_request';
export default () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
new Compare({ // eslint-disable-line no-new
targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
});
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
new MergeRequest({ // eslint-disable-line no-new
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
}
};
......@@ -33,7 +33,7 @@
<template>
<component
:is="rootElementType"
class="text-center">
class="loading-container text-center">
<i
class="fa fa-spin fa-spinner"
:class="cssClass"
......
......@@ -174,12 +174,13 @@
&.user-authored {
cursor: default;
opacity: 0.65;
background-color: $gray-light;
border-color: $theme-gray-200;
color: $gl-text-color-disabled;
&:hover,
&:active {
background-color: $white-light;
border-color: $border-color;
gl-emoji {
opacity: 0.4;
filter: grayscale(100%);
}
}
......
......@@ -224,14 +224,6 @@
@include btn-with-margin;
}
&.disabled {
pointer-events: auto !important;
}
&[disabled] {
pointer-events: none !important;
}
.fa-caret-down,
.fa-chevron-down {
margin-left: 5px;
......@@ -454,3 +446,28 @@
.btn-svg svg {
@include btn-svg;
}
// All disabled buttons, regardless of color, type, etc
%disabled {
background-color: $gray-light !important;
border-color: $theme-gray-200 !important;
color: $gl-text-color-disabled !important;
opacity: 1 !important;
cursor: default !important;
i {
color: $gl-text-color-disabled !important;
}
}
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn,
.dropdown-toggle[disabled],
[disabled].dropdown-menu-toggle {
@extend %disabled;
&:hover {
@extend %disabled;
}
}
......@@ -63,11 +63,6 @@
border-radius: $border-radius-base;
white-space: nowrap;
&[disabled] {
opacity: .65;
cursor: not-allowed;
}
&.no-outline {
outline: 0;
}
......
......@@ -165,6 +165,7 @@ $gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-color-disabled: #919191;
$gl-text-green: $green-600;
$gl-text-green-hover: $green-700;
$gl-text-red: $red-500;
......@@ -260,6 +261,8 @@ $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
$issue-box-upcoming-bg: #8f8f8f;
$pages-group-name-color: #4c4e54;
......
......@@ -391,11 +391,17 @@
.dropdown-toggle {
float: right;
.toggle-icon {
i {
color: $white-light;
padding-right: 2px;
margin-top: 2px;
}
&[disabled] {
i {
color: $gl-text-color-disabled;
}
}
}
.dropdown-menu {
......
......@@ -112,6 +112,11 @@ table.table tr td.multi-file-table-name {
vertical-align: middle;
margin-right: 2px;
}
.loading-container {
margin-right: 4px;
display: inline-block;
}
}
.multi-file-table-col-commit-message {
......@@ -252,7 +257,6 @@ table.table tr td.multi-file-table-name {
display: flex;
position: relative;
flex-direction: column;
height: 100%;
width: 290px;
padding: 0;
background-color: $gray-light;
......@@ -261,6 +265,11 @@ table.table tr td.multi-file-table-name {
.projects-sidebar {
display: flex;
flex-direction: column;
.context-header {
width: auto;
margin-right: 0;
}
}
.multi-file-commit-panel-inner {
......@@ -508,19 +517,70 @@ table.table tr td.multi-file-table-name {
}
}
.ide-flash-container.flash-container {
margin-top: $header-height;
margin-bottom: 0;
.ide.nav-only {
.flash-container {
margin-top: $header-height;
margin-bottom: 0;
}
.alert-wrapper .flash-container .flash-alert:last-child,
.alert-wrapper .flash-container .flash-notice:last-child {
margin-bottom: 0;
}
.content {
margin-top: $header-height;
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $context-header-height});
}
&.flash-shown {
.content {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $flash-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
}
}
}
.with-performance-bar {
.ide-flash-container.flash-container {
margin-top: $header-height + $performance-bar-height;
.with-performance-bar .ide.nav-only {
.flash-container {
margin-top: #{$header-height + $performance-bar-height};
}
.content {
margin-top: #{$header-height + $performance-bar-height};
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
}
&.flash-shown {
.content {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
}
}
}
......
require 'webpack/rails/manifest'
module WebpackHelper
def webpack_bundle_tag(bundle)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle))
def webpack_bundle_tag(bundle, force_same_domain: false)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: true))
end
# override webpack-rails gem helper until changes can make it upstream
def gitlab_webpack_asset_paths(source, extension: nil)
def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
return "" unless source.present?
paths = Webpack::Rails::Manifest.asset_paths(source)
......@@ -14,9 +14,11 @@ module WebpackHelper
paths.select! { |p| p.ends_with? ".#{extension}" }
end
force_host = webpack_public_host
if force_host
paths.map! { |p| "#{force_host}#{p}" }
unless force_same_domain
force_host = webpack_public_host
if force_host
paths.map! { |p| "#{force_host}#{p}" }
end
end
paths
......
......@@ -318,6 +318,7 @@ class Project < ActiveRecord::Base
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
......
......@@ -41,7 +41,7 @@ class ProjectStatistics < ActiveRecord::Base
def update_build_artifacts_size
self.build_artifacts_size =
project.builds.sum(:artifacts_size) +
Ci::JobArtifact.artifacts_size_for(self)
Ci::JobArtifact.artifacts_size_for(self.project)
end
def update_storage_size
......
class Route < ActiveRecord::Base
include CaseSensitivity
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :source, presence: true
......@@ -10,6 +12,7 @@ class Route < ActiveRecord::Base
validate :ensure_permanent_paths, if: :path_changed?
before_validation :delete_conflicting_orphaned_routes
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path
......@@ -78,4 +81,13 @@ class Route < ActiveRecord::Base
def conflicting_redirect_exists?
RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end
def delete_conflicting_orphaned_routes
conflicting = self.class.iwhere(path: path)
conflicting_orphaned_routes = conflicting.select do |route|
route.source.nil?
end
conflicting_orphaned_routes.each(&:destroy)
end
end
- @body_class = 'ide'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'ide'
.ide-flash-container.flash-container
= webpack_bundle_tag 'ide', force_same_domain: true
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
.text-center
......
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page } }
%body{ class: "#{user_application_theme} #{@body_class} nav-only", data: { page: body_data_page } }
= render 'peek/bar'
= render "layouts/header/default"
= render 'shared/outdated_browser'
......@@ -10,4 +10,5 @@
= render "layouts/broadcast"
= yield :flash_message
= render "layouts/flash"
= yield
.content{ id: "content-body" }
= yield
---
title: Set standard disabled state for all buttons
merge_request:
author:
type: other
---
title: Fix the Projects API with_issues_enabled filter behaving incorrectly
any user
merge_request: 12724
author: Jan Christophersen
type: fixed
---
title: Fix a bug calculating artifact size for project statistics
merge_request: 16539
author:
type: fixed
---
title: Correctly escape UTF-8 path elements for uploads
merge_request: 16560
author:
type: fixed
---
title: Ensure that users can reclaim a namespace or project path that is blocked by
an orphaned route
merge_request: 16242
author:
type: fixed
......@@ -133,7 +133,12 @@ var config = {
{
test: /\_worker\.js$/,
use: [
{ loader: 'worker-loader' },
{
loader: 'worker-loader',
options: {
inline: true
}
},
{ loader: 'babel-loader' },
],
},
......
......@@ -49,7 +49,7 @@ Add the following to your `sshd_config` file. This is usuaully located at
Omnibus Docker:
```
AuthorizedKeysCommand /opt/embedded/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k
AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k
AuthorizedKeysCommandUser git
```
......
import CEMergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { stripeHtml } from '~/lib/utils/text_utility';
import { stripHtml } from '~/lib/utils/text_utility';
export default class MergeRequestStore extends CEMergeRequestStore {
constructor(data) {
......@@ -103,7 +103,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
.filter(item => unapproved.find(el => el === item.vulnerability)) || [];
}
/**
* Dast Report sends some keys in HTML, we need to stripe the `<p>` tags.
* Dast Report sends some keys in HTML, we need to strip the `<p>` tags.
* This should be moved to the backend.
*
* @param {Array} data
......@@ -112,7 +112,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
setDastReport(data) {
this.dastReport = data.site.alerts.map(alert => ({
name: alert.name,
parsedDescription: stripeHtml(alert.desc, ' '),
parsedDescription: stripHtml(alert.desc, ' '),
priority: alert.riskdesc,
...alert,
}));
......
......@@ -82,9 +82,9 @@ module API
def present_projects(projects, options = {})
projects = reorder_projects(projects)
projects = projects.with_statistics if params[:statistics]
projects = projects.with_issues_enabled if params[:with_issues_enabled]
projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics]
projects = paginate(projects)
if current_user
......
......@@ -50,7 +50,7 @@ module Banzai
end
def process_link_to_upload_attr(html_attr)
path_parts = [html_attr.value]
path_parts = [Addressable::URI.unescape(html_attr.value)]
if group
path_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
......@@ -58,13 +58,13 @@ module Banzai
path_parts.unshift(relative_url_root, project.full_path)
end
path = File.join(*path_parts)
path = Addressable::URI.escape(File.join(*path_parts))
html_attr.value =
if context[:only_path]
path
else
URI.join(Gitlab.config.gitlab.base_url, path).to_s
Addressable::URI.join(Gitlab.config.gitlab.base_url, path).to_s
end
end
......
......@@ -33,9 +33,9 @@ module Gitlab
object
end
def initialize(repository, name, target, derefenced_target)
def initialize(repository, name, target, dereferenced_target)
@name = Gitlab::Git.ref_name(name)
@dereferenced_target = derefenced_target
@dereferenced_target = dereferenced_target
@target = if target.respond_to?(:oid)
target.oid
elsif target.respond_to?(:name)
......
......@@ -183,11 +183,15 @@ describe('Flash', () => {
});
it('adds flash element into container', () => {
flash('test');
flash('test', 'alert', document, null, false, true);
expect(
document.querySelector('.flash-alert'),
).not.toBeNull();
expect(
document.body.className,
).toContain('flash-shown');
});
it('adds flash into specified parent', () => {
......@@ -220,13 +224,17 @@ describe('Flash', () => {
});
it('removes element after clicking', () => {
flash('test', 'alert', document, null, false);
flash('test', 'alert', document, null, false, true);
document.querySelector('.flash-alert').click();
expect(
document.querySelector('.flash-alert'),
).toBeNull();
expect(
document.body.className,
).not.toContain('flash-shown');
});
describe('with actionConfig', () => {
......
......@@ -218,6 +218,39 @@ describe('Issuable output', () => {
});
});
describe('shows dialog when issue has unsaved changed', () => {
it('confirms on title change', (done) => {
vm.showForm = true;
vm.state.titleText = 'title has changed';
const e = { returnValue: null };
vm.handleBeforeUnloadEvent(e);
Vue.nextTick(() => {
expect(e.returnValue).not.toBeNull();
done();
});
});
it('confirms on description change', (done) => {
vm.showForm = true;
vm.state.descriptionText = 'description has changed';
const e = { returnValue: null };
vm.handleBeforeUnloadEvent(e);
Vue.nextTick(() => {
expect(e.returnValue).not.toBeNull();
done();
});
});
it('does nothing when nothing has changed', (done) => {
const e = { returnValue: null };
vm.handleBeforeUnloadEvent(e);
Vue.nextTick(() => {
expect(e.returnValue).toBeNull();
done();
});
});
});
describe('error when updating', () => {
beforeEach(() => {
spyOn(window, 'Flash').and.callThrough();
......
......@@ -61,13 +61,13 @@ describe('text_utility', () => {
});
});
describe('stripeHtml', () => {
describe('stripHtml', () => {
it('replaces html tag with the default replacement', () => {
expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.');
expect(textUtils.stripHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.');
});
it('replaces html tags with the provided replacement', () => {
expect(textUtils.stripeHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .');
expect(textUtils.stripHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .');
});
});
});
......@@ -300,19 +300,6 @@ describe('Multi-file store actions', () => {
}).catch(done.fail);
});
it('closes all files', (done) => {
store.state.openFiles.push(file());
store.state.openFiles[0].opened = true;
store.dispatch('commitChanges', { payload, newMr: false })
.then(Vue.nextTick)
.then(() => {
expect(store.state.openFiles.length).toBe(0);
done();
}).catch(done.fail);
});
it('scrolls to top of page', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
......
......@@ -16,7 +16,8 @@ describe('Loading Icon Component', () => {
).toEqual('fa fa-spin fa-spinner fa-1x');
expect(component.$el.tagName).toEqual('DIV');
expect(component.$el.classList.contains('text-center')).toEqual(true);
expect(component.$el.classList).toContain('text-center');
expect(component.$el.classList).toContain('loading-container');
});
it('should render accessibility attributes', () => {
......
......@@ -278,18 +278,19 @@ describe Banzai::Filter::RelativeLinkFilter do
expect(doc.at_css('a')['href']).to eq 'http://example.com'
end
it 'supports Unicode filenames' do
it 'supports unescaped Unicode filenames' do
path = '/uploads/한글.png'
escaped = Addressable::URI.escape(path)
doc = filter(link(path))
# Stub these methods so the file doesn't actually need to be in the repo
allow_any_instance_of(described_class)
.to receive(:file_exists?).and_return(true)
allow_any_instance_of(described_class)
.to receive(:image?).with(path).and_return(true)
expect(doc.at_css('a')['href']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
end
it 'supports escaped Unicode filenames' do
path = '/uploads/한글.png'
escaped = Addressable::URI.escape(path)
doc = filter(image(escaped))
expect(doc.at_css('img')['src']).to match "/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png"
expect(doc.at_css('img')['src']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
end
end
......
......@@ -146,6 +146,12 @@ describe ProjectStatistics do
expect(statistics.build_artifacts_size).to be(106365)
end
it 'calculates related build artifacts by project' do
expect(Ci::JobArtifact).to receive(:artifacts_size_for).with(project) { 0 }
statistics.update_build_artifacts_size
end
end
context 'when legacy artifacts are used' do
......
......@@ -79,6 +79,13 @@ describe Route do
end
describe 'callbacks' do
context 'before validation' do
it 'calls #delete_conflicting_orphaned_routes' do
expect(route).to receive(:delete_conflicting_orphaned_routes)
route.valid?
end
end
context 'after update' do
it 'calls #create_redirect_for_old_path' do
expect(route).to receive(:create_redirect_for_old_path)
......@@ -378,4 +385,58 @@ describe Route do
end
end
end
describe '#delete_conflicting_orphaned_routes' do
context 'when there is a conflicting route' do
let!(:conflicting_group) { create(:group, path: 'foo') }
before do
route.path = conflicting_group.route.path
end
context 'when the route is orphaned' do
let!(:offending_route) { conflicting_group.route }
before do
Group.delete(conflicting_group) # Orphan the route
end
it 'deletes the orphaned route' do
expect do
route.valid?
end.to change { described_class.count }.from(2).to(1)
end
it 'passes validation, as usual' do
expect(route.valid?).to be_truthy
end
end
context 'when the route is not orphaned' do
it 'does not delete the conflicting route' do
expect do
route.valid?
end.not_to change { described_class.count }
end
it 'fails validation, as usual' do
expect(route.valid?).to be_falsey
end
end
end
context 'when there are no conflicting routes' do
it 'does not delete any routes' do
route
expect do
route.valid?
end.not_to change { described_class.count }
end
it 'passes validation, as usual' do
expect(route.valid?).to be_truthy
end
end
end
end
......@@ -59,7 +59,7 @@ 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
it 'avoids N+1 queries', :skip_before_request do
first_build = create(:ci_build, :artifacts, pipeline: pipeline)
first_build.runner = create(:ci_runner)
first_build.user = create(:user)
......
......@@ -150,6 +150,19 @@ describe API::Projects do
expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
end
context 'and with_issues_enabled=true' do
it 'only returns projects with issues enabled' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
get api('/projects?with_issues_enabled=true', user)
expect(response.status).to eq 200
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).not_to include(project.id)
end
end
it "does not include statistics by default" do
get api('/projects', user)
......@@ -352,6 +365,19 @@ describe API::Projects do
let(:current_user) { user2 }
let(:projects) { [public_project] }
end
context 'and with_issues_enabled=true' do
it 'does not return private issue projects' do
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)
get api('/projects?with_issues_enabled=true', user2)
expect(response.status).to eq 200
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |p| p['id'] }).not_to include(project.id)
end
end
end
context 'when authenticated as admin' do
......
......@@ -44,7 +44,7 @@ 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
it 'avoids N+1 queries', :skip_before_request do
first_build = create(:ci_build, :artifacts, pipeline: pipeline)
first_build.runner = create(:ci_runner)
first_build.user = create(:user)
......
......@@ -3586,6 +3586,10 @@ 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"
htmlescape@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
htmlparser2@^3.8.2, htmlparser2@^3.9.0:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
......@@ -4633,6 +4637,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.memoize@~3.0.3:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
lodash.mergewith@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment