Commit b3747a39 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-02-22

# Conflicts:
#	CHANGELOG.md
#	app/assets/javascripts/dispatcher.js
#	app/assets/javascripts/gl_dropdown.js
#	app/assets/javascripts/labels_select.js
#	changelogs/unreleased-ee/4929-internationalize-lock-files.yml
#	config/webpack.config.js
#	ee/changelogs/unreleased/fix-show-sidebar-sub-level-items-for-billing.yml
#	ee/changelogs/unreleased/fix-validation-of-environment-scope-for-variables.yml

[ci skip]
parents 7302e244 275efeeb
...@@ -165,9 +165,14 @@ entry. ...@@ -165,9 +165,14 @@ entry.
- Log and send a system hook if a blocked user attempts to login. - Log and send a system hook if a blocked user attempts to login.
- Add Gitaly Servers admin dashboard. - Add Gitaly Servers admin dashboard.
<<<<<<< HEAD
### Other (26 changes, 7 of them are from the community) ### Other (26 changes, 7 of them are from the community)
- Activated the Web IDE Button also on the main project page. !4250 - Activated the Web IDE Button also on the main project page. !4250
=======
### Other (25 changes, 7 of them are from the community)
>>>>>>> upstream/master
- Updated the katex library. !15864 - Updated the katex library. !15864
- Add modal for deleting a milestone. !16229 - Add modal for deleting a milestone. !16229
- Remove unused CSS selectors for Cycle Analytics. !16270 (Takuya Noguchi) - Remove unused CSS selectors for Cycle Analytics. !16270 (Takuya Noguchi)
......
10.5.0-pre 10.6.0-pre
...@@ -57,6 +57,7 @@ var Dispatcher; ...@@ -57,6 +57,7 @@ var Dispatcher;
case 'projects:commits:show': case 'projects:commits:show':
case 'projects:show': case 'projects:show':
case 'groups:show': case 'groups:show':
<<<<<<< HEAD
case 'projects:find_file:show': case 'projects:find_file:show':
case 'projects:blob:show': case 'projects:blob:show':
case 'projects:blame:show': case 'projects:blame:show':
...@@ -66,6 +67,12 @@ var Dispatcher; ...@@ -66,6 +67,12 @@ var Dispatcher;
import(/* webpackChunkName: "ee_projects_edit" */ 'ee/pages/projects/tree/show') import(/* webpackChunkName: "ee_projects_edit" */ 'ee/pages/projects/tree/show')
.then(callDefault) .then(callDefault)
.catch(fail); .catch(fail);
=======
case 'projects:tree:show':
case 'projects:find_file:show':
case 'projects:blob:show':
case 'projects:blame:show':
>>>>>>> upstream/master
shortcut_handler = true; shortcut_handler = true;
break; break;
case 'groups:labels:new': case 'groups:labels:new':
......
...@@ -630,10 +630,13 @@ GitLabDropdown = (function() { ...@@ -630,10 +630,13 @@ GitLabDropdown = (function() {
} }
html = document.createElement('li'); html = document.createElement('li');
<<<<<<< HEAD
if (rowHidden) { if (rowHidden) {
html.style.display = 'none'; html.style.display = 'none';
} }
=======
>>>>>>> upstream/master
if (data === 'divider' || data === 'separator') { if (data === 'divider' || data === 'separator') {
html.className = data; html.className = data;
return html; return html;
......
...@@ -316,7 +316,11 @@ export default class LabelsSelect { ...@@ -316,7 +316,11 @@ export default class LabelsSelect {
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
<<<<<<< HEAD
clicked: function(clickEvent) { clicked: function(clickEvent) {
=======
clicked: function (clickEvent) {
>>>>>>> upstream/master
const { $el, e, isMarking } = clickEvent; const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj; const label = clickEvent.selectedObj;
......
...@@ -31,10 +31,14 @@ ...@@ -31,10 +31,14 @@
type: String, type: String,
required: true, required: true,
}, },
id: { pipelineId: {
type: Number, type: Number,
required: true, required: true,
}, },
type: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -46,17 +50,27 @@ ...@@ -46,17 +50,27 @@
return `btn ${this.cssClass}`; return `btn ${this.cssClass}`;
}, },
}, },
created() {
// We're using eventHub to listen to the modal here instead of
// using props because it would would make the parent components
// much more complex to keep track of the loading state of each button
eventHub.$on('postAction', this.setLoading);
},
beforeDestroy() {
eventHub.$off('postAction', this.setLoading);
},
methods: { methods: {
onClick() { onClick() {
eventHub.$emit('actionConfirmationModal', { eventHub.$emit('openConfirmationModal', {
id: this.id, pipelineId: this.pipelineId,
callback: this.makeRequest, endpoint: this.endpoint,
type: this.type,
}); });
}, },
makeRequest() { setLoading(endpoint) {
this.isLoading = true; if (endpoint === this.endpoint) {
this.isLoading = true;
eventHub.$emit('postAction', this.endpoint); }
}, },
}, },
}; };
......
<script> <script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import pipelinesTableRowComponent from './pipelines_table_row.vue'; import pipelinesTableRowComponent from './pipelines_table_row.vue';
import stopConfirmationModal from './stop_confirmation_modal.vue'; import eventHub from '../event_hub';
import retryConfirmationModal from './retry_confirmation_modal.vue';
/** /**
* Pipelines Table Component. * Pipelines Table Component.
...@@ -11,8 +12,7 @@ ...@@ -11,8 +12,7 @@
export default { export default {
components: { components: {
pipelinesTableRowComponent, pipelinesTableRowComponent,
stopConfirmationModal, modal,
retryConfirmationModal,
}, },
props: { props: {
pipelines: { pipelines: {
...@@ -33,6 +33,52 @@ ...@@ -33,6 +33,52 @@
required: true, required: true,
}, },
}, },
data() {
return {
pipelineId: '',
endpoint: '',
type: '',
};
},
computed: {
modalTitle() {
return this.type === 'stop' ?
sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), {
pipelineId: `'${this.pipelineId}'`,
}, false) :
sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), {
pipelineId: `'${this.pipelineId}'`,
}, false);
},
modalText() {
return this.type === 'stop' ?
sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), {
pipelineId: `<strong>#${this.pipelineId}</strong>`,
}, false) :
sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), {
pipelineId: `<strong>#${this.pipelineId}</strong>`,
}, false);
},
primaryButtonLabel() {
return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline');
},
},
created() {
eventHub.$on('openConfirmationModal', this.setModalData);
},
beforeDestroy() {
eventHub.$off('openConfirmationModal', this.setModalData);
},
methods: {
setModalData(data) {
this.pipelineId = data.pipelineId;
this.endpoint = data.endpoint;
this.type = data.type;
},
onSubmit() {
eventHub.$emit('postAction', this.endpoint);
},
},
}; };
</script> </script>
<template> <template>
...@@ -74,7 +120,20 @@ ...@@ -74,7 +120,20 @@
:auto-devops-help-path="autoDevopsHelpPath" :auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType" :view-type="viewType"
/> />
<stop-confirmation-modal /> <modal
<retry-confirmation-modal /> id="confirmation-modal"
:title="modalTitle"
:text="modalText"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</div> </div>
</template> </template>
...@@ -306,9 +306,10 @@ ...@@ -306,9 +306,10 @@
css-class="js-pipelines-retry-button btn-default btn-retry" css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry" title="Retry"
icon="repeat" icon="repeat"
:id="pipeline.id" :pipeline-id="pipeline.id"
data-toggle="modal" data-toggle="modal"
data-target="#retry-confirmation-modal" data-target="#confirmation-modal"
type="retry"
/> />
<async-button-component <async-button-component
...@@ -317,9 +318,10 @@ ...@@ -317,9 +318,10 @@
css-class="js-pipelines-cancel-button btn-remove" css-class="js-pipelines-cancel-button btn-remove"
title="Cancel" title="Cancel"
icon="close" icon="close"
:id="pipeline.id" :pipeline-id="pipeline.id"
data-toggle="modal" data-toggle="modal"
data-target="#stop-confirmation-modal" data-target="#confirmation-modal"
type="stop"
/> />
</div> </div>
</div> </div>
......
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Retry pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to retry pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Retry pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="retry-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
<script>
import modal from '~/vue_shared/components/modal.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
modal,
},
data() {
return {
id: '',
callback: () => {},
};
},
computed: {
title() {
return sprintf(s__('Pipeline|Stop pipeline #%{id}?'), {
id: `'${this.id}'`,
}, false);
},
text() {
return sprintf(s__('Pipeline|You’re about to stop pipeline %{id}.'), {
id: `<strong>#${this.id}</strong>`,
}, false);
},
primaryButtonLabel() {
return s__('Pipeline|Stop pipeline');
},
},
created() {
eventHub.$on('actionConfirmationModal', this.updateModal);
},
beforeDestroy() {
eventHub.$off('actionConfirmationModal', this.updateModal);
},
methods: {
updateModal(action) {
this.id = action.id;
this.callback = action.callback;
},
onSubmit() {
this.callback();
},
},
};
</script>
<template>
<modal
id="stop-confirmation-modal"
:title="title"
:text="text"
kind="danger"
:primary-button-label="primaryButtonLabel"
@submit="onSubmit"
>
<template
slot="body"
slot-scope="props"
>
<p v-html="props.text"></p>
</template>
</modal>
</template>
...@@ -25,7 +25,7 @@ module Ci ...@@ -25,7 +25,7 @@ module Ci
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
has_many :stages has_many :stages
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
has_many :builds, foreign_key: :commit_id has_many :builds, foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable' has_many :variables, class_name: 'Ci::PipelineVariable'
......
---
title: Fix 500 error being shown when diff has context marker with invalid encoding
merge_request:
author:
type: fixed
---
title: Improve performance of pipeline page by reducing DB queries
merge_request: 17168
author:
type: performance
---
title: Allow branch names to be named the same as the sha it points to
merge_request:
author:
type: fixed
...@@ -38,10 +38,13 @@ function generateAutoEntries(path, prefix = '.') { ...@@ -38,10 +38,13 @@ function generateAutoEntries(path, prefix = '.') {
} }
pageEntries.forEach(( path ) => generateAutoEntries(path)); pageEntries.forEach(( path ) => generateAutoEntries(path));
<<<<<<< HEAD
// add and replace any ce entries with ee entries // add and replace any ce entries with ee entries
const eePageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'ee/app/assets/javascripts') }); const eePageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'ee/app/assets/javascripts') });
eePageEntries.forEach(( path ) => generateAutoEntries(path, 'ee')); eePageEntries.forEach(( path ) => generateAutoEntries(path, 'ee'));
=======
>>>>>>> upstream/master
// report our auto-generated bundle count // report our auto-generated bundle count
var autoEntriesCount = Object.keys(autoEntries).length; var autoEntriesCount = Object.keys(autoEntries).length;
......
...@@ -37,12 +37,31 @@ are very appreciative of the work done by translators and proofreaders! ...@@ -37,12 +37,31 @@ are very appreciative of the work done by translators and proofreaders!
> sure that you have a history of contributing translations to the GitLab > sure that you have a history of contributing translations to the GitLab
> project. > project.
1. Once your translations have been accepted, 1. Contribute translations to GitLab. See instructions for
[open a merge request](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/new) [translating GitLab](translation.md).
to request Proofreader permissions and add yourself to the list above.
Translating GitLab is a community effort that requires team work and
attention to detail. Proofreaders play an important role helping new
contributors, and ensuring the consistency and quality of translations.
Your conduct and contributions as a translator should reflect this before
requesting to be a proofreader.
1. Request proofreader permissions by opening a merge request to add yourself
to the list of proofreaders.
Open the [proofreader.md source file][proofreader-src] and click **Edit**.
Add your language in alphabetical order, and add yourself to the list
including:
- name
- link to your GitLab profile
- link to your CrowdIn profile
In the merge request description, please include links to any projects you In the merge request description, please include links to any projects you
have previously translated. have previously translated.
1. Your request to become a proofreader will be considered on the merits of 1. Your request to become a proofreader will be considered on the merits of
your previous translations. your previous translations.
[proofreader-src]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/i18n/proofreader.md
...@@ -755,7 +755,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa ...@@ -755,7 +755,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also a separate paragraph, but... This line is also a separate paragraph, but...
This line is only separated by a single newline, so it *does not break* and just follows the previous line in the *same paragraph*. This line is only separated by a single newline, so it *does not break* and just follows the previous line in the *same paragraph*.
This line is also a separate paragraph, and... This line is also a separate paragraph, and...
This line is *on its own line*, because the previous line ends with two spaces. (but still in the *same paragraph*) This line is *on its own line*, because the previous line ends with two spaces. (but still in the *same paragraph*)
spaces. spaces.
...@@ -768,7 +768,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa ...@@ -768,7 +768,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also a separate paragraph, but... This line is also a separate paragraph, but...
This line is only separated by a single newline, so it *does not break* and just follows the previous line in the *same paragraph*. This line is only separated by a single newline, so it *does not break* and just follows the previous line in the *same paragraph*.
This line is also a separate paragraph, and... This line is also a separate paragraph, and...
This line is *on its own line*, because the previous line ends with two spaces. (but still in the *same paragraph*) This line is *on its own line*, because the previous line ends with two spaces. (but still in the *same paragraph*)
spaces. spaces.
......
...@@ -5,157 +5,10 @@ module Gitlab ...@@ -5,157 +5,10 @@ module Gitlab
# This class processes a batch of rows in `untracked_files_for_uploads` by # This class processes a batch of rows in `untracked_files_for_uploads` by
# adding each file to the `uploads` table if it does not exist. # adding each file to the `uploads` table if it does not exist.
class PopulateUntrackedUploads # rubocop:disable Metrics/ClassLength class PopulateUntrackedUploads # rubocop:disable Metrics/ClassLength
# This class is responsible for producing the attributes necessary to
# track an uploaded file in the `uploads` table.
class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength
self.table_name = 'untracked_files_for_uploads'
# Ends with /:random_hex/:filename
FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}
FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/
# These regex patterns are tested against a relative path, relative to
# the upload directory.
# For convenience, if there exists a capture group in the pattern, then
# it indicates the model_id.
PATH_PATTERNS = [
{
pattern: %r{\A-/system/appearance/logo/(\d+)/},
uploader: 'AttachmentUploader',
model_type: 'Appearance'
},
{
pattern: %r{\A-/system/appearance/header_logo/(\d+)/},
uploader: 'AttachmentUploader',
model_type: 'Appearance'
},
{
pattern: %r{\A-/system/note/attachment/(\d+)/},
uploader: 'AttachmentUploader',
model_type: 'Note'
},
{
pattern: %r{\A-/system/user/avatar/(\d+)/},
uploader: 'AvatarUploader',
model_type: 'User'
},
{
pattern: %r{\A-/system/group/avatar/(\d+)/},
uploader: 'AvatarUploader',
model_type: 'Namespace'
},
{
pattern: %r{\A-/system/project/avatar/(\d+)/},
uploader: 'AvatarUploader',
model_type: 'Project'
},
{
pattern: FILE_UPLOADER_PATH,
uploader: 'FileUploader',
model_type: 'Project'
}
].freeze
def to_h
@upload_hash ||= {
path: upload_path,
uploader: uploader,
model_type: model_type,
model_id: model_id,
size: file_size,
checksum: checksum
}
end
def upload_path
# UntrackedFile#path is absolute, but Upload#path depends on uploader
@upload_path ||=
if uploader == 'FileUploader'
# Path relative to project directory in uploads
matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH)
matchd[0].sub(%r{\A/}, '') # remove leading slash
else
path
end
end
def uploader
matching_pattern_map[:uploader]
end
def model_type
matching_pattern_map[:model_type]
end
def model_id
return @model_id if defined?(@model_id)
pattern = matching_pattern_map[:pattern]
matchd = path_relative_to_upload_dir.match(pattern)
# If something is captured (matchd[1] is not nil), it is a model_id
# Only the FileUploader pattern will not match an ID
@model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id
end
def file_size
File.size(absolute_path)
end
def checksum
Digest::SHA256.file(absolute_path).hexdigest
end
private
def matching_pattern_map
@matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map|
path_relative_to_upload_dir.match(path_pattern_map[:pattern])
end
unless @matching_pattern_map
raise "Unknown upload path pattern \"#{path}\""
end
@matching_pattern_map
end
def file_uploader_model_id
matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE)
not_found_msg = <<~MSG
Could not capture project full_path from a FileUploader path:
"#{path_relative_to_upload_dir}"
MSG
raise not_found_msg unless matchd
full_path = matchd[1]
project = Project.find_by_full_path(full_path)
return nil unless project
project.id
end
# Not including a leading slash
def path_relative_to_upload_dir
upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength
base = %r{\A#{Regexp.escape(upload_dir)}/}
@path_relative_to_upload_dir ||= path.sub(base, '')
end
def absolute_path
File.join(Gitlab.config.uploads.storage_path, path)
end
end
# This class is used to query the `uploads` table.
class Upload < ActiveRecord::Base
self.table_name = 'uploads'
end
def perform(start_id, end_id) def perform(start_id, end_id)
return unless migrate? return unless migrate?
files = UntrackedFile.where(id: start_id..end_id) files = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.where(id: start_id..end_id)
processed_files = insert_uploads_if_needed(files) processed_files = insert_uploads_if_needed(files)
processed_files.delete_all processed_files.delete_all
...@@ -165,7 +18,8 @@ module Gitlab ...@@ -165,7 +18,8 @@ module Gitlab
private private
def migrate? def migrate?
UntrackedFile.table_exists? && Upload.table_exists? Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.table_exists? &&
Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Upload.table_exists?
end end
def insert_uploads_if_needed(files) def insert_uploads_if_needed(files)
...@@ -197,7 +51,7 @@ module Gitlab ...@@ -197,7 +51,7 @@ module Gitlab
def filter_existing_uploads(files) def filter_existing_uploads(files)
paths = files.map(&:upload_path) paths = files.map(&:upload_path)
existing_paths = Upload.where(path: paths).pluck(:path).to_set existing_paths = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Upload.where(path: paths).pluck(:path).to_set
files.reject do |file| files.reject do |file|
existing_paths.include?(file.upload_path) existing_paths.include?(file.upload_path)
...@@ -229,7 +83,7 @@ module Gitlab ...@@ -229,7 +83,7 @@ module Gitlab
end end
ids.each do |model_type, model_ids| ids.each do |model_type, model_ids|
model_class = Object.const_get(model_type) model_class = "Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::#{model_type}".constantize
found_ids = model_class.where(id: model_ids.uniq).pluck(:id) found_ids = model_class.where(id: model_ids.uniq).pluck(:id)
deleted_ids = ids[model_type] - found_ids deleted_ids = ids[model_type] - found_ids
ids[model_type] = deleted_ids ids[model_type] = deleted_ids
...@@ -249,8 +103,8 @@ module Gitlab ...@@ -249,8 +103,8 @@ module Gitlab
end end
def drop_temp_table_if_finished def drop_temp_table_if_finished
if UntrackedFile.all.empty? && !Rails.env.test? # Dropping a table intermittently breaks test cleanup if Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.all.empty? && !Rails.env.test? # Dropping a table intermittently breaks test cleanup
UntrackedFile.connection.drop_table(:untracked_files_for_uploads, Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
if_exists: true) if_exists: true)
end end
end end
......
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module PopulateUntrackedUploadsDependencies
# This class is responsible for producing the attributes necessary to
# track an uploaded file in the `uploads` table.
class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength
self.table_name = 'untracked_files_for_uploads'
# Ends with /:random_hex/:filename
FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}
FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/
# These regex patterns are tested against a relative path, relative to
# the upload directory.
# For convenience, if there exists a capture group in the pattern, then
# it indicates the model_id.
PATH_PATTERNS = [
{
pattern: %r{\A-/system/appearance/logo/(\d+)/},
uploader: 'AttachmentUploader',
model_type: 'Appearance'
},
{
pattern: %r{\A-/system/appearance/header_logo/(\d+)/},
uploader: 'AttachmentUploader',
model_type: 'Appearance'
},
{
pattern: %r{\A-/system/note/attachment/(\d+)/},
uploader: 'AttachmentUploader',
model_type: 'Note'
},
{
pattern: %r{\A-/system/user/avatar/(\d+)/},
uploader: 'AvatarUploader',
model_type: 'User'
},
{
pattern: %r{\A-/system/group/avatar/(\d+)/},
uploader: 'AvatarUploader',
model_type: 'Namespace'
},
{
pattern: %r{\A-/system/project/avatar/(\d+)/},
uploader: 'AvatarUploader',
model_type: 'Project'
},
{
pattern: FILE_UPLOADER_PATH,
uploader: 'FileUploader',
model_type: 'Project'
}
].freeze
def to_h
@upload_hash ||= {
path: upload_path,
uploader: uploader,
model_type: model_type,
model_id: model_id,
size: file_size,
checksum: checksum
}
end
def upload_path
# UntrackedFile#path is absolute, but Upload#path depends on uploader
@upload_path ||=
if uploader == 'FileUploader'
# Path relative to project directory in uploads
matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH)
matchd[0].sub(%r{\A/}, '') # remove leading slash
else
path
end
end
def uploader
matching_pattern_map[:uploader]
end
def model_type
matching_pattern_map[:model_type]
end
def model_id
return @model_id if defined?(@model_id)
pattern = matching_pattern_map[:pattern]
matchd = path_relative_to_upload_dir.match(pattern)
# If something is captured (matchd[1] is not nil), it is a model_id
# Only the FileUploader pattern will not match an ID
@model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id
end
def file_size
File.size(absolute_path)
end
def checksum
Digest::SHA256.file(absolute_path).hexdigest
end
private
def matching_pattern_map
@matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map|
path_relative_to_upload_dir.match(path_pattern_map[:pattern])
end
unless @matching_pattern_map
raise "Unknown upload path pattern \"#{path}\""
end
@matching_pattern_map
end
def file_uploader_model_id
matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE)
not_found_msg = <<~MSG
Could not capture project full_path from a FileUploader path:
"#{path_relative_to_upload_dir}"
MSG
raise not_found_msg unless matchd
full_path = matchd[1]
project = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Project.find_by_full_path(full_path)
return nil unless project
project.id
end
# Not including a leading slash
def path_relative_to_upload_dir
upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength
base = %r{\A#{Regexp.escape(upload_dir)}/}
@path_relative_to_upload_dir ||= path.sub(base, '')
end
def absolute_path
File.join(Gitlab.config.uploads.storage_path, path)
end
end
# Avoid using application code
class Upload < ActiveRecord::Base
self.table_name = 'uploads'
end
# Avoid using application code
class Appearance < ActiveRecord::Base
self.table_name = 'appearances'
end
# Avoid using application code
class Namespace < ActiveRecord::Base
self.table_name = 'namespaces'
end
# Avoid using application code
class Note < ActiveRecord::Base
self.table_name = 'notes'
end
# Avoid using application code
class User < ActiveRecord::Base
self.table_name = 'users'
end
# Since project Markdown upload paths don't contain the project ID, we have to find the
# project by its full_path. Due to MySQL/PostgreSQL differences, and historical reasons,
# the logic is somewhat complex, so I've mostly copied it in here.
class Project < ActiveRecord::Base
self.table_name = 'projects'
def self.find_by_full_path(path)
binary = Gitlab::Database.mysql? ? 'BINARY' : ''
order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
where_full_path_in(path).reorder(order_sql).take
end
def self.where_full_path_in(path)
cast_lower = Gitlab::Database.postgresql?
path = connection.quote(path)
where =
if cast_lower
"(LOWER(routes.path) = LOWER(#{path}))"
else
"(routes.path = #{path})"
end
joins("INNER JOIN routes ON routes.source_id = projects.id AND routes.source_type = 'Project'").where(where)
end
end
end
end
end
...@@ -27,7 +27,17 @@ module Gitlab ...@@ -27,7 +27,17 @@ module Gitlab
rich_line = highlight_line(diff_line) || diff_line.text rich_line = highlight_line(diff_line) || diff_line.text
if line_inline_diffs = inline_diffs[i] if line_inline_diffs = inline_diffs[i]
rich_line = InlineDiffMarker.new(diff_line.text, rich_line).mark(line_inline_diffs) begin
rich_line = InlineDiffMarker.new(diff_line.text, rich_line).mark(line_inline_diffs)
# This should only happen when the encoding of the diff doesn't
# match the blob, which is a bug. But we shouldn't fail to render
# completely in that case, even though we want to report the error.
rescue RangeError => e
if Gitlab::Sentry.enabled?
Gitlab::Sentry.context
Raven.capture_exception(e)
end
end
end end
diff_line.text = rich_line diff_line.text = rich_line
......
...@@ -1355,7 +1355,7 @@ module Gitlab ...@@ -1355,7 +1355,7 @@ module Gitlab
if is_enabled if is_enabled
gitaly_ref_client.branch_names_contains_sha(sha) gitaly_ref_client.branch_names_contains_sha(sha)
else else
refs_contains_sha(:branch, sha) refs_contains_sha('refs/heads/', sha)
end end
end end
end end
...@@ -1365,7 +1365,7 @@ module Gitlab ...@@ -1365,7 +1365,7 @@ module Gitlab
if is_enabled if is_enabled
gitaly_ref_client.tag_names_contains_sha(sha) gitaly_ref_client.tag_names_contains_sha(sha)
else else
refs_contains_sha(:tag, sha) refs_contains_sha('refs/tags/', sha)
end end
end end
end end
...@@ -1464,19 +1464,25 @@ module Gitlab ...@@ -1464,19 +1464,25 @@ module Gitlab
end end
end end
def refs_contains_sha(ref_type, sha) def refs_contains_sha(refs_prefix, sha)
args = %W(#{ref_type} --contains #{sha}) refs_prefix << "/" unless refs_prefix.ends_with?('/')
names = run_git(args).first
return [] unless names.respond_to?(:split) # By forcing the output to %(refname) each line wiht a ref will start with
# the ref prefix. All other lines can be discarded.
args = %W(for-each-ref --contains=#{sha} --format=%(refname) #{refs_prefix})
names, code = run_git(args)
names = names.split("\n").map(&:strip) return [] unless code.zero?
names.each do |name| refs = []
name.slice! '* ' left_slice_count = refs_prefix.length
names.lines.each do |line|
next unless line.start_with?(refs_prefix)
refs << line.rstrip[left_slice_count..-1]
end end
names refs
end end
def rugged_write_config(full_path:) def rugged_write_config(full_path:)
......
...@@ -201,7 +201,7 @@ module Gitlab ...@@ -201,7 +201,7 @@ module Gitlab
end end
def check_repository_existence! def check_repository_existence!
unless project.repository.exists? unless repository.exists?
raise UnauthorizedError, ERROR_MESSAGES[:no_repo] raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
end end
end end
...@@ -350,5 +350,9 @@ module Gitlab ...@@ -350,5 +350,9 @@ module Gitlab
def push_to_read_only_message def push_to_read_only_message
ERROR_MESSAGES[:cannot_push_to_read_only] ERROR_MESSAGES[:cannot_push_to_read_only]
end end
def repository
project.repository
end
end end
end end
...@@ -30,5 +30,11 @@ module Gitlab ...@@ -30,5 +30,11 @@ module Gitlab
def push_to_read_only_message def push_to_read_only_message
ERROR_MESSAGES[:read_only] ERROR_MESSAGES[:read_only]
end end
private
def repository
project.wiki.repository
end
end end
end end
require 'spec_helper' require 'spec_helper'
feature 'Login' do feature 'Login' do
scenario 'Successful user signin invalidates password reset token' do
user = create(:user)
expect(user.reset_password_token).to be_nil
visit new_user_password_path
fill_in 'user_email', with: user.email
click_button 'Reset password'
user.reload
expect(user.reset_password_token).not_to be_nil
find('a[href="#login-pane"]').click
gitlab_sign_in(user)
expect(current_path).to eq root_path
user.reload
expect(user.reset_password_token).to be_nil
end
describe 'initial login after setup' do describe 'initial login after setup' do
it 'allows the initial admin to create a password' do it 'allows the initial admin to create a password' do
# This behavior is dependent on there only being one user # This behavior is dependent on there only being one user
......
require 'spec_helper'
describe 'Projects tab on a user profile', :js do
let(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace) }
let!(:project2) { create(:project, namespace: user.namespace) }
before do
allow(Project).to receive(:default_per_page).and_return(1)
sign_in(user)
visit user_path(user)
page.within('.user-profile-nav') do
click_link('Personal projects')
end
wait_for_requests
end
it 'paginates results' do
expect(page).to have_content(project2.name)
click_link('Next')
expect(page).to have_content(project.name)
end
end
require 'spec_helper' require 'spec_helper'
feature 'Signup' do describe 'Signup' do
describe 'signup with no errors' do let(:new_user) { build_stubbed(:user) }
describe 'username validation', :js do
before do
visit root_path
click_link 'Register'
end
it 'does not show an error border if the username is available' do
fill_in 'new_user_username', with: 'new-user'
wait_for_requests
expect(find('.username')).not_to have_css '.gl-field-error-outline'
end
it 'does not show an error border if the username contains dots (.)' do
fill_in 'new_user_username', with: 'new.user.username'
wait_for_requests
expect(find('.username')).not_to have_css '.gl-field-error-outline'
end
it 'shows an error border if the username already exists' do
existing_user = create(:user)
fill_in 'new_user_username', with: existing_user.username
wait_for_requests
expect(find('.username')).to have_css '.gl-field-error-outline'
end
it 'shows an error border if the username contains special characters' do
fill_in 'new_user_username', with: 'new$user!username'
wait_for_requests
expect(find('.username')).to have_css '.gl-field-error-outline'
end
end
context 'with no errors' do
context "when sending confirmation email" do context "when sending confirmation email" do
before do before do
stub_application_setting(send_user_confirmation_email: true) stub_application_setting(send_user_confirmation_email: true)
end end
it 'creates the user account and sends a confirmation email' do it 'creates the user account and sends a confirmation email' do
user = build(:user)
visit root_path visit root_path
fill_in 'new_user_name', with: user.name fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: user.username fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: user.email fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_email_confirmation', with: user.email fill_in 'new_user_email_confirmation', with: new_user.email
fill_in 'new_user_password', with: user.password fill_in 'new_user_password', with: new_user.password
click_button "Register"
expect { click_button 'Register' }.to change { User.count }.by(1)
expect(current_path).to eq users_almost_there_path expect(current_path).to eq users_almost_there_path
expect(page).to have_content("Please check your email to confirm your account") expect(page).to have_content("Please check your email to confirm your account")
...@@ -26,15 +64,13 @@ feature 'Signup' do ...@@ -26,15 +64,13 @@ feature 'Signup' do
context "when sigining up with different cased emails" do context "when sigining up with different cased emails" do
it "creates the user successfully" do it "creates the user successfully" do
user = build(:user)
visit root_path visit root_path
fill_in 'new_user_name', with: user.name fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: user.username fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: user.email fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_email_confirmation', with: user.email.capitalize fill_in 'new_user_email_confirmation', with: new_user.email.capitalize
fill_in 'new_user_password', with: user.password fill_in 'new_user_password', with: new_user.password
click_button "Register" click_button "Register"
expect(current_path).to eq dashboard_projects_path expect(current_path).to eq dashboard_projects_path
...@@ -48,15 +84,13 @@ feature 'Signup' do ...@@ -48,15 +84,13 @@ feature 'Signup' do
end end
it 'creates the user account and goes to dashboard' do it 'creates the user account and goes to dashboard' do
user = build(:user)
visit root_path visit root_path
fill_in 'new_user_name', with: user.name fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: user.username fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: user.email fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_email_confirmation', with: user.email fill_in 'new_user_email_confirmation', with: new_user.email
fill_in 'new_user_password', with: user.password fill_in 'new_user_password', with: new_user.password
click_button "Register" click_button "Register"
expect(current_path).to eq dashboard_projects_path expect(current_path).to eq dashboard_projects_path
...@@ -65,17 +99,16 @@ feature 'Signup' do ...@@ -65,17 +99,16 @@ feature 'Signup' do
end end
end end
describe 'signup with errors' do context 'with errors' do
it "displays the errors" do it "displays the errors" do
existing_user = create(:user) existing_user = create(:user)
user = build(:user)
visit root_path visit root_path
fill_in 'new_user_name', with: user.name fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: user.username fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: existing_user.email fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password fill_in 'new_user_password', with: new_user.password
click_button "Register" click_button "Register"
expect(current_path).to eq user_registration_path expect(current_path).to eq user_registration_path
...@@ -86,18 +119,17 @@ feature 'Signup' do ...@@ -86,18 +119,17 @@ feature 'Signup' do
it 'does not redisplay the password' do it 'does not redisplay the password' do
existing_user = create(:user) existing_user = create(:user)
user = build(:user)
visit root_path visit root_path
fill_in 'new_user_name', with: user.name fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: user.username fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: existing_user.email fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password fill_in 'new_user_password', with: new_user.password
click_button "Register" click_button "Register"
expect(current_path).to eq user_registration_path expect(current_path).to eq user_registration_path
expect(page.body).not_to match(/#{user.password}/) expect(page.body).not_to match(/#{new_user.password}/)
end end
end end
end end
require 'spec_helper' require 'spec_helper'
describe 'User page', :js do describe 'Users > User browses projects on user page', :js do
let!(:user) { create :user } let!(:user) { create :user }
let!(:private_project) do let!(:private_project) do
create :project, :private, name: 'private', namespace: user.namespace do |project| create :project, :private, name: 'private', namespace: user.namespace do |project|
...@@ -26,6 +26,28 @@ describe 'User page', :js do ...@@ -26,6 +26,28 @@ describe 'User page', :js do
end end
end end
it 'paginates projects', :js do
project = create(:project, namespace: user.namespace)
project2 = create(:project, namespace: user.namespace)
allow(Project).to receive(:default_per_page).and_return(1)
sign_in(user)
visit user_path(user)
page.within('.user-profile-nav') do
click_link('Personal projects')
end
wait_for_requests
expect(page).to have_content(project2.name)
click_link('Next')
expect(page).to have_content(project.name)
end
context 'when not signed in' do context 'when not signed in' do
it 'renders user public project' do it 'renders user public project' do
visit user_path(user) visit user_path(user)
......
require 'spec_helper'
feature 'Users', :js do
let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
scenario 'GET /users/sign_in creates a new user account' do
visit new_user_session_path
click_link 'Register'
fill_in 'new_user_name', with: 'Name Surname'
fill_in 'new_user_username', with: 'Great'
fill_in 'new_user_email', with: 'name@mail.com'
fill_in 'new_user_email_confirmation', with: 'name@mail.com'
fill_in 'new_user_password', with: 'password1234'
expect { click_button 'Register' }.to change { User.count }.by(1)
end
scenario 'Successful user signin invalidates password reset token' do
expect(user.reset_password_token).to be_nil
visit new_user_password_path
fill_in 'user_email', with: user.email
click_button 'Reset password'
user.reload
expect(user.reset_password_token).not_to be_nil
find('a[href="#login-pane"]').click
gitlab_sign_in(user)
expect(current_path).to eq root_path
user.reload
expect(user.reset_password_token).to be_nil
end
scenario 'Should show one error if email is already taken' do
visit new_user_session_path
click_link 'Register'
fill_in 'new_user_name', with: 'Another user name'
fill_in 'new_user_username', with: 'anotheruser'
fill_in 'new_user_email', with: user.email
fill_in 'new_user_email_confirmation', with: user.email
fill_in 'new_user_password', with: '12341234'
expect { click_button 'Register' }.to change { User.count }.by(0)
expect(page).to have_text('Email has already been taken')
expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}'
end
describe 'redirect alias routes' do
before do
expect(user).to be_persisted
end
scenario '/u/user1 redirects to user page' do
visit '/u/user1'
expect(current_path).to eq user_path(user)
expect(page).to have_text(user.name)
end
scenario '/u/user1/groups redirects to user groups page' do
visit '/u/user1/groups'
expect(current_path).to eq user_groups_path(user)
end
scenario '/u/user1/projects redirects to user projects page' do
visit '/u/user1/projects'
expect(current_path).to eq user_projects_path(user)
end
end
feature 'username validation' do
let(:loading_icon) { '.fa.fa-spinner' }
let(:username_input) { 'new_user_username' }
before do
visit new_user_session_path
click_link 'Register'
end
scenario 'doesn\'t show an error border if the username is available' do
fill_in username_input, with: 'new-user'
wait_for_requests
expect(find('.username')).not_to have_css '.gl-field-error-outline'
end
scenario 'does not show an error border if the username contains dots (.)' do
fill_in username_input, with: 'new.user.username'
wait_for_requests
expect(find('.username')).not_to have_css '.gl-field-error-outline'
end
scenario 'shows an error border if the username already exists' do
fill_in username_input, with: user.username
wait_for_requests
expect(find('.username')).to have_css '.gl-field-error-outline'
end
scenario 'shows an error border if the username contains special characters' do
fill_in username_input, with: 'new$user!username'
wait_for_requests
expect(find('.username')).to have_css '.gl-field-error-outline'
end
end
def errors_on_page(page)
page.find('#error_explanation').find('ul').all('li').map { |item| item.text }.join("\n")
end
def number_of_errors_on_page(page)
page.find('#error_explanation').find('ul').all('li').count
end
end
...@@ -15,7 +15,8 @@ describe('Pipelines Async Button', () => { ...@@ -15,7 +15,8 @@ describe('Pipelines Async Button', () => {
title: 'Foo', title: 'Foo',
icon: 'repeat', icon: 'repeat',
cssClass: 'bar', cssClass: 'bar',
id: 123, pipelineId: 123,
type: 'explode',
}, },
}).$mount(); }).$mount();
}); });
...@@ -39,8 +40,9 @@ describe('Pipelines Async Button', () => { ...@@ -39,8 +40,9 @@ describe('Pipelines Async Button', () => {
describe('With confirm dialog', () => { describe('With confirm dialog', () => {
it('should call the service when confimation is positive', () => { it('should call the service when confimation is positive', () => {
eventHub.$on('actionConfirmationModal', (data) => { eventHub.$on('openConfirmationModal', (data) => {
expect(data.id).toEqual(123); expect(data.pipelineId).toEqual(123);
expect(data.type).toEqual('explode');
}); });
component = new AsyncButtonComponent({ component = new AsyncButtonComponent({
...@@ -49,7 +51,8 @@ describe('Pipelines Async Button', () => { ...@@ -49,7 +51,8 @@ describe('Pipelines Async Button', () => {
title: 'Foo', title: 'Foo',
icon: 'fa fa-foo', icon: 'fa fa-foo',
cssClass: 'bar', cssClass: 'bar',
id: 123, pipelineId: 123,
type: 'explode',
}, },
}).$mount(); }).$mount();
......
require 'spec_helper'
# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work.
describe Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile, :migration, schema: 20180208183958 do
include MigrationsHelpers::TrackUntrackedUploadsHelpers
let!(:appearances) { table(:appearances) }
let!(:namespaces) { table(:namespaces) }
let!(:projects) { table(:projects) }
let!(:routes) { table(:routes) }
let!(:uploads) { table(:uploads) }
before(:all) do
ensure_temporary_tracking_table_exists
end
describe '#upload_path' do
def assert_upload_path(file_path, expected_upload_path)
untracked_file = create_untracked_file(file_path)
expect(untracked_file.upload_path).to eq(expected_upload_path)
end
context 'for an appearance logo file path' do
it 'returns the file path relative to the CarrierWave root' do
assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg')
end
end
context 'for an appearance header_logo file path' do
it 'returns the file path relative to the CarrierWave root' do
assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg')
end
end
context 'for a pre-Markdown Note attachment file path' do
it 'returns the file path relative to the CarrierWave root' do
assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf')
end
end
context 'for a user avatar file path' do
it 'returns the file path relative to the CarrierWave root' do
assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg')
end
end
context 'for a group avatar file path' do
it 'returns the file path relative to the CarrierWave root' do
assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg')
end
end
context 'for a project avatar file path' do
it 'returns the file path relative to the CarrierWave root' do
assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg')
end
end
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
it 'returns the file path relative to the project directory in uploads' do
project = create_project
random_hex = SecureRandom.hex
assert_upload_path("/#{get_full_path(project)}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg")
end
end
end
describe '#uploader' do
def assert_uploader(file_path, expected_uploader)
untracked_file = create_untracked_file(file_path)
expect(untracked_file.uploader).to eq(expected_uploader)
end
context 'for an appearance logo file path' do
it 'returns AttachmentUploader as a string' do
assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader')
end
end
context 'for an appearance header_logo file path' do
it 'returns AttachmentUploader as a string' do
assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader')
end
end
context 'for a pre-Markdown Note attachment file path' do
it 'returns AttachmentUploader as a string' do
assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader')
end
end
context 'for a user avatar file path' do
it 'returns AvatarUploader as a string' do
assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader')
end
end
context 'for a group avatar file path' do
it 'returns AvatarUploader as a string' do
assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader')
end
end
context 'for a project avatar file path' do
it 'returns AvatarUploader as a string' do
assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader')
end
end
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
it 'returns FileUploader as a string' do
project = create_project
assert_uploader("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader')
end
end
end
describe '#model_type' do
def assert_model_type(file_path, expected_model_type)
untracked_file = create_untracked_file(file_path)
expect(untracked_file.model_type).to eq(expected_model_type)
end
context 'for an appearance logo file path' do
it 'returns Appearance as a string' do
assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance')
end
end
context 'for an appearance header_logo file path' do
it 'returns Appearance as a string' do
assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance')
end
end
context 'for a pre-Markdown Note attachment file path' do
it 'returns Note as a string' do
assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note')
end
end
context 'for a user avatar file path' do
it 'returns User as a string' do
assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User')
end
end
context 'for a group avatar file path' do
it 'returns Namespace as a string' do
assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace')
end
end
context 'for a project avatar file path' do
it 'returns Project as a string' do
assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project')
end
end
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
it 'returns Project as a string' do
project = create_project
assert_model_type("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'Project')
end
end
end
describe '#model_id' do
def assert_model_id(file_path, expected_model_id)
untracked_file = create_untracked_file(file_path)
expect(untracked_file.model_id).to eq(expected_model_id)
end
context 'for an appearance logo file path' do
it 'returns the ID as a string' do
assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1)
end
end
context 'for an appearance header_logo file path' do
it 'returns the ID as a string' do
assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1)
end
end
context 'for a pre-Markdown Note attachment file path' do
it 'returns the ID as a string' do
assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234)
end
end
context 'for a user avatar file path' do
it 'returns the ID as a string' do
assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234)
end
end
context 'for a group avatar file path' do
it 'returns the ID as a string' do
assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234)
end
end
context 'for a project avatar file path' do
it 'returns the ID as a string' do
assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234)
end
end
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
it 'returns the ID as a string' do
project = create_project
assert_model_id("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", project.id)
end
end
end
describe '#file_size' do
context 'for an appearance logo file path' do
let(:appearance) { create_or_update_appearance(logo: true) }
let(:untracked_file) { described_class.create!(path: get_uploads(appearance, 'Appearance').first.path) }
it 'returns the file size' do
expect(untracked_file.file_size).to eq(1062)
end
end
context 'for a project avatar file path' do
let(:project) { create_project(avatar: true) }
let(:untracked_file) { described_class.create!(path: get_uploads(project, 'Project').first.path) }
it 'returns the file size' do
expect(untracked_file.file_size).to eq(1062)
end
end
context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
let(:project) { create_project }
let(:untracked_file) { create_untracked_file("/#{get_full_path(project)}/#{get_uploads(project, 'Project').first.path}") }
before do
add_markdown_attachment(project)
end
it 'returns the file size' do
expect(untracked_file.file_size).to eq(1062)
end
end
end
def create_untracked_file(path_relative_to_upload_dir)
described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}")
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180129193323 do # Rollback DB to 10.5 (later than this was originally written for) because it still needs to work.
include TrackUntrackedUploadsHelpers describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180208183958 do
include MigrationsHelpers include MigrationsHelpers::TrackUntrackedUploadsHelpers
let!(:untracked_files_for_uploads) { described_class::UntrackedFile } let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) }
let!(:appearances) { table(:appearances) }
let!(:namespaces) { table(:namespaces) }
let!(:projects) { table(:projects) }
let!(:routes) { table(:routes) }
let!(:uploads) { table(:uploads) }
let!(:users) { table(:users) }
around do |example| around do |example|
# Especially important so the follow-up migration does not get run # Especially important so the follow-up migration does not get run
...@@ -15,19 +21,17 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat ...@@ -15,19 +21,17 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat
shared_examples 'prepares the untracked_files_for_uploads table' do shared_examples 'prepares the untracked_files_for_uploads table' do
context 'when files were uploaded before and after hashed storage was enabled' do context 'when files were uploaded before and after hashed storage was enabled' do
let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) }
let!(:user) { create(:user, :with_avatar) } let!(:user) { create_user(avatar: true) }
let!(:project1) { create(:project, :with_avatar, :legacy_storage) } let!(:project1) { create_project(avatar: true) }
let(:project2) { create(:project) } # instantiate after enabling hashed_storage let(:project2) { create_project } # instantiate after enabling hashed_storage
before do before do
# Markdown upload before enabling hashed_storage # Markdown upload before enabling hashed_storage
UploadService.new(project1, uploaded_file, FileUploader).execute add_markdown_attachment(project1)
stub_application_setting(hashed_storage_enabled: true)
# Markdown upload after enabling hashed_storage # Markdown upload after enabling hashed_storage
UploadService.new(project2, uploaded_file, FileUploader).execute add_markdown_attachment(project2, hashed_storage: true)
end end
it 'has a path field long enough for really long paths' do it 'has a path field long enough for really long paths' do
...@@ -61,7 +65,7 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat ...@@ -61,7 +65,7 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat
it 'does not add hashed files to the untracked_files_for_uploads table' do it 'does not add hashed files to the untracked_files_for_uploads table' do
described_class.new.perform described_class.new.perform
hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path hashed_file_path = get_uploads(project2, 'Project').where(uploader: 'FileUploader').first.path
expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey
end end
......
...@@ -72,6 +72,28 @@ describe Gitlab::Diff::Highlight do ...@@ -72,6 +72,28 @@ describe Gitlab::Diff::Highlight do
expect(subject[5].text).to eq(code) expect(subject[5].text).to eq(code)
expect(subject[5].text).to be_html_safe expect(subject[5].text).to be_html_safe
end end
context 'when the inline diff marker has an invalid range' do
before do
allow_any_instance_of(Gitlab::Diff::InlineDiffMarker).to receive(:mark).and_raise(RangeError)
end
it 'keeps the original rich line' do
code = %q{+ raise RuntimeError, "System commands must be given as an array of strings"}
expect(subject[5].text).to eq(code)
expect(subject[5].text).not_to be_html_safe
end
it 'reports to Sentry if configured' do
allow(Gitlab::Sentry).to receive(:enabled?).and_return(true)
expect(Gitlab::Sentry).to receive(:context)
expect(Raven).to receive(:capture_exception)
subject
end
end
end end
end end
end end
...@@ -600,6 +600,33 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -600,6 +600,33 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
end end
describe '#branch_names_contains_sha' do
shared_examples 'returning the right branches' do
let(:head_id) { repository.rugged.head.target.oid }
let(:new_branch) { head_id }
before do
repository.create_branch(new_branch, 'master')
end
after do
repository.delete_branch(new_branch)
end
it 'displays that branch' do
expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch)
end
end
context 'when Gitaly is enabled' do
it_behaves_like 'returning the right branches'
end
context 'when Gitaly is disabled', :disable_gitaly do
it_behaves_like 'returning the right branches'
end
end
describe "#refs_hash" do describe "#refs_hash" do
subject { repository.refs_hash } subject { repository.refs_hash }
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::GitAccessWiki do describe Gitlab::GitAccessWiki do
let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :wiki_repo) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] } let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] }
let(:redirected_path) { nil } let(:redirected_path) { nil }
...@@ -48,6 +48,18 @@ describe Gitlab::GitAccessWiki do ...@@ -48,6 +48,18 @@ describe Gitlab::GitAccessWiki do
it 'give access to download wiki code' do it 'give access to download wiki code' do
expect { subject }.not_to raise_error expect { subject }.not_to raise_error
end end
context 'when the wiki repository does not exist' do
it 'returns not found' do
wiki_repo = project.wiki.repository
FileUtils.rm_rf(wiki_repo.path)
# Sanity check for rm_rf
expect(wiki_repo.exists?).to eq(false)
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'A repository for this project does not exist yet.')
end
end
end end
context 'when wiki feature is disabled' do context 'when wiki feature is disabled' do
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_uploads') require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_uploads')
describe TrackUntrackedUploads, :migration, :sidekiq do describe TrackUntrackedUploads, :migration, :sidekiq do
include TrackUntrackedUploadsHelpers include MigrationsHelpers::TrackUntrackedUploadsHelpers
it 'correctly schedules the follow-up background migration' do it 'correctly schedules the follow-up background migration' do
Sidekiq::Testing.fake! do Sidekiq::Testing.fake! do
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Internal do describe API::Internal do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:key) { create(:key, user: user) } let(:key) { create(:key, user: user) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository, :wiki_repo) }
let(:secret_token) { Gitlab::Shell.secret_token } let(:secret_token) { Gitlab::Shell.secret_token }
let(:gl_repository) { "project-#{project.id}" } let(:gl_repository) { "project-#{project.id}" }
let(:reference_counter) { double('ReferenceCounter') } let(:reference_counter) { double('ReferenceCounter') }
......
...@@ -150,7 +150,7 @@ describe 'Git HTTP requests' do ...@@ -150,7 +150,7 @@ describe 'Git HTTP requests' do
let(:path) { "/#{wiki.repository.full_path}.git" } let(:path) { "/#{wiki.repository.full_path}.git" }
context "when the project is public" do context "when the project is public" do
let(:project) { create(:project, :repository, :public, :wiki_enabled) } let(:project) { create(:project, :wiki_repo, :public, :wiki_enabled) }
it_behaves_like 'pushes require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication'
...@@ -177,7 +177,7 @@ describe 'Git HTTP requests' do ...@@ -177,7 +177,7 @@ describe 'Git HTTP requests' do
end end
context 'but the repo is disabled' do context 'but the repo is disabled' do
let(:project) { create(:project, :repository, :public, :repository_disabled, :wiki_enabled) } let(:project) { create(:project, :wiki_repo, :public, :repository_disabled, :wiki_enabled) }
it_behaves_like 'pulls are allowed' it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed' it_behaves_like 'pushes are allowed'
...@@ -198,7 +198,7 @@ describe 'Git HTTP requests' do ...@@ -198,7 +198,7 @@ describe 'Git HTTP requests' do
end end
context "when the project is private" do context "when the project is private" do
let(:project) { create(:project, :repository, :private, :wiki_enabled) } let(:project) { create(:project, :wiki_repo, :private, :wiki_enabled) }
it_behaves_like 'pulls require Basic HTTP Authentication' it_behaves_like 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication'
...@@ -210,7 +210,7 @@ describe 'Git HTTP requests' do ...@@ -210,7 +210,7 @@ describe 'Git HTTP requests' do
end end
context 'but the repo is disabled' do context 'but the repo is disabled' do
let(:project) { create(:project, :repository, :private, :repository_disabled, :wiki_enabled) } let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled) }
it 'allows clones' do it 'allows clones' do
download(path, user: user.username, password: user.password) do |response| download(path, user: user.username, password: user.password) do |response|
......
...@@ -37,6 +37,22 @@ describe UsersController, "routing" do ...@@ -37,6 +37,22 @@ describe UsersController, "routing" do
it "to #calendar_activities" do it "to #calendar_activities" do
expect(get("/users/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') expect(get("/users/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User')
end end
describe 'redirect alias routes' do
include RSpec::Rails::RequestExampleGroup
it '/u/user1 redirects to /user1' do
expect(get("/u/user1")).to redirect_to('/user1')
end
it '/u/user1/groups redirects to /user1/groups' do
expect(get("/u/user1/groups")).to redirect_to('/users/user1/groups')
end
it '/u/user1/projects redirects to /user1/projects' do
expect(get("/u/user1/projects")).to redirect_to('/users/user1/projects')
end
end
end end
# search GET /search(.:format) search#show # search GET /search(.:format) search#show
......
...@@ -168,6 +168,22 @@ RSpec.configure do |config| ...@@ -168,6 +168,22 @@ RSpec.configure do |config|
Sidekiq.redis(&:flushall) Sidekiq.redis(&:flushall)
end end
# The :each scope runs "inside" the example, so this hook ensures the DB is in the
# correct state before any examples' before hooks are called. This prevents a
# problem where `ScheduleIssuesClosedAtTypeChange` (or any migration that depends
# on background migrations being run inline during test setup) can be broken by
# altering Sidekiq behavior in an unrelated spec like so:
#
# around do |example|
# Sidekiq::Testing.fake! do
# example.run
# end
# end
config.before(:context, :migration) do
schema_migrate_down!
end
# Each example may call `migrate!`, so we must ensure we are migrated down every time
config.before(:each, :migration) do config.before(:each, :migration) do
schema_migrate_down! schema_migrate_down!
end end
......
module MigrationsHelpers
module TrackUntrackedUploadsHelpers
PUBLIC_DIR = File.join(Rails.root, 'tmp', 'tests', 'public')
UPLOADS_DIR = File.join(PUBLIC_DIR, 'uploads')
SYSTEM_DIR = File.join(UPLOADS_DIR, '-', 'system')
UPLOAD_FILENAME = 'image.png'.freeze
FIXTURE_FILE_PATH = File.join(Rails.root, 'spec', 'fixtures', 'dk.png')
FIXTURE_CHECKSUM = 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'.freeze
def create_or_update_appearance(logo: false, header_logo: false)
appearance = appearances.first_or_create(title: 'foo', description: 'bar', logo: (UPLOAD_FILENAME if logo), header_logo: (UPLOAD_FILENAME if header_logo))
add_upload(appearance, 'Appearance', 'logo', 'AttachmentUploader') if logo
add_upload(appearance, 'Appearance', 'header_logo', 'AttachmentUploader') if header_logo
appearance
end
def create_group(avatar: false)
index = unique_index(:group)
group = namespaces.create(name: "group#{index}", path: "group#{index}", avatar: (UPLOAD_FILENAME if avatar))
add_upload(group, 'Group', 'avatar', 'AvatarUploader') if avatar
group
end
def create_note(attachment: false)
note = notes.create(attachment: (UPLOAD_FILENAME if attachment))
add_upload(note, 'Note', 'attachment', 'AttachmentUploader') if attachment
note
end
def create_project(avatar: false)
group = create_group
project = projects.create(namespace_id: group.id, path: "project#{unique_index(:project)}", avatar: (UPLOAD_FILENAME if avatar))
routes.create(path: "#{group.path}/#{project.path}", source_id: project.id, source_type: 'Project') # so Project.find_by_full_path works
add_upload(project, 'Project', 'avatar', 'AvatarUploader') if avatar
project
end
def create_user(avatar: false)
user = users.create(email: "foo#{unique_index(:user)}@bar.com", avatar: (UPLOAD_FILENAME if avatar), projects_limit: 100)
add_upload(user, 'User', 'avatar', 'AvatarUploader') if avatar
user
end
def unique_index(name = :unnamed)
@unique_index ||= {}
@unique_index[name] ||= 0
@unique_index[name] += 1
end
def add_upload(model, model_type, attachment_type, uploader)
file_path = upload_file_path(model, model_type, attachment_type)
path_relative_to_public = file_path.sub("#{PUBLIC_DIR}/", '')
create_file(file_path)
uploads.create!(
size: 1062,
path: path_relative_to_public,
model_id: model.id,
model_type: model_type == 'Group' ? 'Namespace' : model_type,
uploader: uploader,
checksum: FIXTURE_CHECKSUM
)
end
def add_markdown_attachment(project, hashed_storage: false)
project_dir = hashed_storage ? hashed_project_uploads_dir(project) : legacy_project_uploads_dir(project)
attachment_dir = File.join(project_dir, SecureRandom.hex)
attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME)
project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '')
create_file(attachment_file_path)
uploads.create!(
size: 1062,
path: project_attachment_path_relative_to_project,
model_id: project.id,
model_type: 'Project',
uploader: 'FileUploader',
checksum: FIXTURE_CHECKSUM
)
end
def legacy_project_uploads_dir(project)
namespace = namespaces.find_by(id: project.namespace_id)
File.join(UPLOADS_DIR, namespace.path, project.path)
end
def hashed_project_uploads_dir(project)
File.join(UPLOADS_DIR, '@hashed', 'aa', 'aaaaaaaaaaaa')
end
def upload_file_path(model, model_type, attachment_type)
dir = File.join(upload_dir(model_type.downcase, attachment_type.to_s), model.id.to_s)
File.join(dir, UPLOAD_FILENAME)
end
def upload_dir(model_type, attachment_type)
File.join(SYSTEM_DIR, model_type, attachment_type)
end
def create_file(path)
File.delete(path) if File.exist?(path)
FileUtils.mkdir_p(File.dirname(path))
FileUtils.cp(FIXTURE_FILE_PATH, path)
end
def get_uploads(model, model_type)
uploads.where(model_type: model_type, model_id: model.id)
end
def get_full_path(project)
routes.find_by(source_id: project.id, source_type: 'Project').path
end
def ensure_temporary_tracking_table_exists
Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists)
end
end
end
module TrackUntrackedUploadsHelpers
def uploaded_file
fixture_path = Rails.root.join('spec/fixtures/rails_sample.jpg')
fixture_file_upload(fixture_path)
end
def ensure_temporary_tracking_table_exists
Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists)
end
def create_or_update_appearance(attrs)
a = Appearance.first_or_initialize(title: 'foo', description: 'bar')
a.update!(attrs)
a
end
end
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