Commit 38e56395 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-03-07' into 'master'

CE upstream - 2018-03-07 17:11 UTC

Closes #5171

See merge request gitlab-org/gitlab-ee!4884
parents 92be6261 a196d949
...@@ -136,6 +136,7 @@ gem 'html-pipeline', '~> 1.11.0' ...@@ -136,6 +136,7 @@ gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '2.0.0' gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.2' gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4' gem 'redcarpet', '~> 3.4'
gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2' gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12' gem 'org-ruby', '~> 0.9.12'
......
...@@ -139,6 +139,8 @@ GEM ...@@ -139,6 +139,8 @@ GEM
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
colorize (0.7.7) colorize (0.7.7)
commonmarker (0.17.8)
ruby-enum (~> 0.5)
concord (0.1.5) concord (0.1.5)
adamantium (~> 0.2.0) adamantium (~> 0.2.0)
equalizer (~> 0.0.9) equalizer (~> 0.0.9)
...@@ -827,6 +829,8 @@ GEM ...@@ -827,6 +829,8 @@ GEM
rubocop (>= 0.51) rubocop (>= 0.51)
rubocop-rspec (1.22.1) rubocop-rspec (1.22.1)
rubocop (>= 0.52.1) rubocop (>= 0.52.1)
ruby-enum (0.7.2)
i18n
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
ruby-prof (0.16.2) ruby-prof (0.16.2)
...@@ -1050,6 +1054,7 @@ DEPENDENCIES ...@@ -1050,6 +1054,7 @@ DEPENDENCIES
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2) chronic (~> 0.10.2)
chronic_duration (~> 0.10.6) chronic_duration (~> 0.10.6)
commonmarker (~> 0.17)
concurrent-ruby (~> 1.0.5) concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0) connection_pool (~> 2.0)
creole (~> 0.5.0) creole (~> 0.5.0)
......
...@@ -186,7 +186,7 @@ ...@@ -186,7 +186,7 @@
<clipboard-button <clipboard-button
:text="ingressExternalIp" :text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')" :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
css-class="btn btn-default js-clipboard-btn" class="js-clipboard-btn"
/> />
</span> </span>
</div> </div>
......
...@@ -35,6 +35,7 @@ ...@@ -35,6 +35,7 @@
<clipboard-button <clipboard-button
title="Copy file path to clipboard" title="Copy file path to clipboard"
:text="diffFile.submoduleLink" :text="diffFile.submoduleLink"
css-class="btn-default btn-transparent btn-clipboard"
/> />
</span> </span>
</div> </div>
...@@ -79,6 +80,7 @@ ...@@ -79,6 +80,7 @@
<clipboard-button <clipboard-button
title="Copy file path to clipboard" title="Copy file path to clipboard"
:text="diffFile.filePath" :text="diffFile.filePath"
css-class="btn-default btn-transparent btn-clipboard"
/> />
<small <small
......
<script>
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
components: {
GlModal,
},
props: {
milestoneTitle: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
},
computed: {
title() {
return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
},
text() {
return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
Existing project milestones with the same title will be merged.
This action cannot be reversed.`);
},
},
methods: {
onSubmit() {
eventHub.$emit('promoteMilestoneModal.requestStarted', this.url);
return axios.post(this.url, { params: { format: 'json' } })
.then((response) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: true });
visitUrl(response.data.url);
})
.catch((error) => {
eventHub.$emit('promoteMilestoneModal.requestFinished', { milestoneUrl: this.url, successful: false });
createFlash(error);
});
},
},
};
</script>
<template>
<gl-modal
id="promote-milestone-modal"
footer-primary-button-variant="warning"
:footer-primary-button-text="s__('Milestones|Promote Milestone')"
@submit="onSubmit"
>
<template
slot="title"
>
{{ title }}
</template>
{{ text }}
</gl-modal>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
import eventHub from './event_hub';
export default () => {
Vue.use(Translate);
const onRequestFinished = ({ milestoneUrl, successful }) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
if (!successful) {
button.removeAttribute('disabled');
}
button.querySelector('.js-loading-icon').classList.add('hidden');
};
const onRequestStarted = (milestoneUrl) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
button.setAttribute('disabled', '');
button.querySelector('.js-loading-icon').classList.remove('hidden');
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
milestoneId: parseInt(button.dataset.milestoneId, 10),
milestoneTitle: button.dataset.milestoneTitle,
milestoneUrl: button.dataset.milestoneUrl,
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
};
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('deleteMilestoneModal.props', modalProps);
};
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
deleteMilestoneButtons.forEach((button) => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('deleteMilestoneModal.mounted', () => {
deleteMilestoneButtons.forEach((button) => {
button.removeAttribute('disabled');
});
});
return new Vue({
el: '#delete-milestone-modal',
components: {
deleteMilestoneModal,
},
data() {
return {
modalProps: {
milestoneId: -1,
milestoneTitle: '',
milestoneUrl: '',
issueCount: -1,
mergeRequestCount: -1,
},
};
},
mounted() {
eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
eventHub.$emit('deleteMilestoneModal.mounted');
},
beforeDestroy() {
eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement(deleteMilestoneModal, {
props: this.modalProps,
});
},
});
};
import Vue from 'vue'; import initDeleteMilestoneModal from './delete_milestone_modal_init';
import initPromoteMilestoneModal from './promote_milestone_modal_init';
import Translate from '~/vue_shared/translate';
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
import eventHub from './event_hub';
export default () => { export default () => {
Vue.use(Translate); initDeleteMilestoneModal();
initPromoteMilestoneModal();
const onRequestFinished = ({ milestoneUrl, successful }) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
if (!successful) {
button.removeAttribute('disabled');
}
button.querySelector('.js-loading-icon').classList.add('hidden');
};
const onRequestStarted = (milestoneUrl) => {
const button = document.querySelector(`.js-delete-milestone-button[data-milestone-url="${milestoneUrl}"]`);
button.setAttribute('disabled', '');
button.querySelector('.js-loading-icon').classList.remove('hidden');
eventHub.$once('deleteMilestoneModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
milestoneId: parseInt(button.dataset.milestoneId, 10),
milestoneTitle: button.dataset.milestoneTitle,
milestoneUrl: button.dataset.milestoneUrl,
issueCount: parseInt(button.dataset.milestoneIssueCount, 10),
mergeRequestCount: parseInt(button.dataset.milestoneMergeRequestCount, 10),
};
eventHub.$once('deleteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('deleteMilestoneModal.props', modalProps);
};
const deleteMilestoneButtons = document.querySelectorAll('.js-delete-milestone-button');
for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
const button = deleteMilestoneButtons[i];
button.addEventListener('click', onDeleteButtonClick);
}
eventHub.$once('deleteMilestoneModal.mounted', () => {
for (let i = 0; i < deleteMilestoneButtons.length; i += 1) {
const button = deleteMilestoneButtons[i];
button.removeAttribute('disabled');
}
});
return new Vue({
el: '#delete-milestone-modal',
components: {
deleteMilestoneModal,
},
data() {
return {
modalProps: {
milestoneId: -1,
milestoneTitle: '',
milestoneUrl: '',
issueCount: -1,
mergeRequestCount: -1,
},
};
},
mounted() {
eventHub.$on('deleteMilestoneModal.props', this.setModalProps);
eventHub.$emit('deleteMilestoneModal.mounted');
},
beforeDestroy() {
eventHub.$off('deleteMilestoneModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement(deleteMilestoneModal, {
props: this.modalProps,
});
},
});
}; };
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import PromoteMilestoneModal from './components/promote_milestone_modal.vue';
import eventHub from './event_hub';
Vue.use(Translate);
export default () => {
const onRequestFinished = ({ milestoneUrl, successful }) => {
const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
if (!successful) {
button.removeAttribute('disabled');
}
};
const onRequestStarted = (milestoneUrl) => {
const button = document.querySelector(`.js-promote-project-milestone-button[data-url="${milestoneUrl}"]`);
button.setAttribute('disabled', '');
eventHub.$once('promoteMilestoneModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
milestoneTitle: button.dataset.milestoneTitle,
url: button.dataset.url,
};
eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteMilestoneModal.props', modalProps);
};
const promoteMilestoneButtons = document.querySelectorAll('.js-promote-project-milestone-button');
promoteMilestoneButtons.forEach((button) => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('promoteMilestoneModal.mounted', () => {
promoteMilestoneButtons.forEach((button) => {
button.removeAttribute('disabled');
});
});
const promoteMilestoneModal = document.getElementById('promote-milestone-modal');
let promoteMilestoneComponent;
if (promoteMilestoneModal) {
promoteMilestoneComponent = new Vue({
el: promoteMilestoneModal,
components: {
PromoteMilestoneModal,
},
data() {
return {
modalProps: {
milestoneTitle: '',
url: '',
},
};
},
mounted() {
eventHub.$on('promoteMilestoneModal.props', this.setModalProps);
eventHub.$emit('promoteMilestoneModal.mounted');
},
beforeDestroy() {
eventHub.$off('promoteMilestoneModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement('promote-milestone-modal', {
props: this.modalProps,
});
},
});
}
return promoteMilestoneComponent;
};
<script>
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
components: {
GlModal,
},
props: {
url: {
type: String,
required: true,
},
labelTitle: {
type: String,
required: true,
},
labelColor: {
type: String,
required: true,
},
labelTextColor: {
type: String,
required: true,
},
},
computed: {
text() {
return s__(`Milestones|Promoting this label will make it available for all projects inside the group.
Existing project labels with the same title will be merged. This action cannot be reversed.`);
},
title() {
const label = `<span
class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
>${this.labelTitle}</span>`;
return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
labelTitle: label,
}, false);
},
},
methods: {
onSubmit() {
eventHub.$emit('promoteLabelModal.requestStarted', this.url);
return axios.post(this.url, { params: { format: 'json' } })
.then((response) => {
eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: true });
visitUrl(response.data.url);
})
.catch((error) => {
eventHub.$emit('promoteLabelModal.requestFinished', { labelUrl: this.url, successful: false });
createFlash(error);
});
},
},
};
</script>
<template>
<gl-modal
id="promote-label-modal"
footer-primary-button-variant="warning"
:footer-primary-button-text="s__('Labels|Promote Label')"
@submit="onSubmit"
>
<div
slot="title"
v-html="title"
>
{{ title }}
</div>
{{ text }}
</gl-modal>
</template>
import Vue from 'vue';
export default new Vue();
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import initLabels from '~/init_labels'; import initLabels from '~/init_labels';
import eventHub from '../event_hub';
import PromoteLabelModal from '../components/promote_label_modal.vue';
document.addEventListener('DOMContentLoaded', initLabels); Vue.use(Translate);
const initLabelIndex = () => {
initLabels();
const onRequestFinished = ({ labelUrl, successful }) => {
const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
if (!successful) {
button.removeAttribute('disabled');
}
};
const onRequestStarted = (labelUrl) => {
const button = document.querySelector(`.js-promote-project-label-button[data-url="${labelUrl}"]`);
button.setAttribute('disabled', '');
eventHub.$once('promoteLabelModal.requestFinished', onRequestFinished);
};
const onDeleteButtonClick = (event) => {
const button = event.currentTarget;
const modalProps = {
labelTitle: button.dataset.labelTitle,
labelColor: button.dataset.labelColor,
labelTextColor: button.dataset.labelTextColor,
url: button.dataset.url,
};
eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
eventHub.$emit('promoteLabelModal.props', modalProps);
};
const promoteLabelButtons = document.querySelectorAll('.js-promote-project-label-button');
promoteLabelButtons.forEach((button) => {
button.addEventListener('click', onDeleteButtonClick);
});
eventHub.$once('promoteLabelModal.mounted', () => {
promoteLabelButtons.forEach((button) => {
button.removeAttribute('disabled');
});
});
const promoteLabelModal = document.getElementById('promote-label-modal');
let promoteLabelModalComponent;
if (promoteLabelModal) {
promoteLabelModalComponent = new Vue({
el: promoteLabelModal,
components: {
PromoteLabelModal,
},
data() {
return {
modalProps: {
labelTitle: '',
labelColor: '',
labelTextColor: '',
url: '',
},
};
},
mounted() {
eventHub.$on('promoteLabelModal.props', this.setModalProps);
eventHub.$emit('promoteLabelModal.mounted');
},
beforeDestroy() {
eventHub.$off('promoteLabelModal.props', this.setModalProps);
},
methods: {
setModalProps(modalProps) {
this.modalProps = modalProps;
},
},
render(createElement) {
return createElement('promote-label-modal', {
props: this.modalProps,
});
},
});
}
return promoteLabelModalComponent;
};
document.addEventListener('DOMContentLoaded', initLabelIndex);
...@@ -86,6 +86,7 @@ ...@@ -86,6 +86,7 @@
v-if="repo.location" v-if="repo.location"
:text="clipboardText" :text="clipboardText"
:title="repo.location" :title="repo.location"
css-class="btn-default btn-transparent btn-clipboard"
/> />
<div class="controls hidden-xs pull-right"> <div class="controls hidden-xs pull-right">
......
...@@ -90,6 +90,7 @@ ...@@ -90,6 +90,7 @@
v-if="item.location" v-if="item.location"
:title="item.location" :title="item.location"
:text="clipboardText(item.location)" :text="clipboardText(item.location)"
css-class="btn-default btn-transparent btn-clipboard"
/> />
</td> </td>
<td> <td>
......
...@@ -67,6 +67,7 @@ ...@@ -67,6 +67,7 @@
<clipboard-button <clipboard-button
:text="branchNameClipboardData" :text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')" :title="__('Copy branch name to clipboard')"
css-class="btn-default btn-transparent btn-clipboard"
/> />
{{ s__("mrWidget|into") }} {{ s__("mrWidget|into") }}
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
cssClass: { cssClass: {
type: String, type: String,
required: false, required: false,
default: 'btn btn-default btn-transparent btn-clipboard', default: 'btn-default',
}, },
}, },
}; };
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
<template> <template>
<button <button
type="button" type="button"
class="btn"
:class="cssClass" :class="cssClass"
:title="title" :title="title"
:data-clipboard-text="text" :data-clipboard-text="text"
......
...@@ -2,14 +2,17 @@ ...@@ -2,14 +2,17 @@
background-color: $modal-body-bg; background-color: $modal-body-bg;
padding: #{3 * $grid-size} #{2 * $grid-size}; padding: #{3 * $grid-size} #{2 * $grid-size};
.page-title { .page-title,
margin-top: 0; .modal-title {
.color-label { .color-label {
font-size: $gl-font-size; font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal; padding: $gl-vert-padding $label-padding-modal;
} }
} }
.page-title {
margin-top: 0;
}
} }
.modal-body { .modal-body {
......
...@@ -56,6 +56,7 @@ module AuthenticatesWithTwoFactor ...@@ -56,6 +56,7 @@ module AuthenticatesWithTwoFactor
session.delete(:otp_user_id) session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1' remember_me(user) if user_params[:remember_me] == '1'
user.save!
sign_in(user) sign_in(user)
else else
user.increment_failed_attempts! user.increment_failed_attempts!
......
...@@ -112,12 +112,14 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -112,12 +112,14 @@ class Projects::LabelsController < Projects::ApplicationController
begin begin
return render_404 unless promote_service.execute(@label) return render_404 unless promote_service.execute(@label)
flash[:notice] = "#{@label.title} promoted to group label."
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to(project_labels_path(@project), redirect_to(project_labels_path(@project), status: 303)
notice: 'Label was promoted to a Group Label') end
format.json do
render json: { url: project_labels_path(@project) }
end end
format.js
end end
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label" Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label"
......
...@@ -191,7 +191,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -191,7 +191,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
begin begin
@merge_request.environments_for(current_user).map do |environment| @merge_request.environments_for(current_user).map do |environment|
project = environment.project project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit) deployment = environment.first_deployment_for(@merge_request.diff_head_sha)
stop_url = stop_url =
if environment.stop_action? && can?(current_user, :create_deployment, environment) if environment.stop_action? && can?(current_user, :create_deployment, environment)
......
...@@ -75,9 +75,17 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -75,9 +75,17 @@ class Projects::MilestonesController < Projects::ApplicationController
end end
def promote def promote
promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone) Milestones::PromoteService.new(project, current_user).execute(milestone)
flash[:notice] = "Milestone has been promoted to group milestone."
redirect_to group_milestone_path(project.group, promoted_milestone.iid) flash[:notice] = "#{milestone.title} promoted to group milestone"
respond_to do |format|
format.html do
redirect_to project_milestones_path(project)
end
format.json do
render json: { url: project_milestones_path(project) }
end
end
rescue Milestones::PromoteService::PromoteMilestoneError => error rescue Milestones::PromoteService::PromoteMilestoneError => error
redirect_to milestone, alert: error.message redirect_to milestone, alert: error.message
end end
......
...@@ -121,7 +121,7 @@ class Notify < BaseMailer ...@@ -121,7 +121,7 @@ class Notify < BaseMailer
if Gitlab::IncomingEmail.enabled? && @sent_notification if Gitlab::IncomingEmail.enabled? && @sent_notification
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace address.display_name = @project.full_name
headers['Reply-To'] = address headers['Reply-To'] = address
......
...@@ -99,8 +99,8 @@ class Environment < ActiveRecord::Base ...@@ -99,8 +99,8 @@ class Environment < ActiveRecord::Base
folder_name == "production" folder_name == "production"
end end
def first_deployment_for(commit) def first_deployment_for(commit_sha)
ref = project.repository.ref_name_for_sha(ref_path, commit.sha) ref = project.repository.ref_name_for_sha(ref_path, commit_sha)
return nil unless ref return nil unless ref
......
...@@ -65,6 +65,7 @@ class Event < ActiveRecord::Base ...@@ -65,6 +65,7 @@ class Event < ActiveRecord::Base
# Callbacks # Callbacks
after_create :reset_project_activity after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push? after_create :set_last_repository_updated_at, if: :push?
after_create :track_user_interacted_projects
# Scopes # Scopes
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
...@@ -395,4 +396,11 @@ class Event < ActiveRecord::Base ...@@ -395,4 +396,11 @@ class Event < ActiveRecord::Base
Project.unscoped.where(id: project_id) Project.unscoped.where(id: project_id)
.update_all(last_repository_updated_at: created_at) .update_all(last_repository_updated_at: created_at)
end end
def track_user_interacted_projects
# Note the call to .available? is due to earlier migrations
# that would otherwise conflict with the call to .track
# (because the table does not exist yet).
UserInteractedProject.track(self) if UserInteractedProject.available?
end
end end
...@@ -385,15 +385,27 @@ class MergeRequest < ActiveRecord::Base ...@@ -385,15 +385,27 @@ class MergeRequest < ActiveRecord::Base
end end
def diff_start_sha def diff_start_sha
diff_start_commit.try(:sha) if persisted?
merge_request_diff.start_commit_sha
else
target_branch_head.try(:sha)
end
end end
def diff_base_sha def diff_base_sha
diff_base_commit.try(:sha) if persisted?
merge_request_diff.base_commit_sha
else
branch_merge_base_commit.try(:sha)
end
end end
def diff_head_sha def diff_head_sha
diff_head_commit.try(:sha) if persisted?
merge_request_diff.head_commit_sha
else
source_branch_head.try(:sha)
end
end end
# When importing a pull request from GitHub, the old and new branches may no # When importing a pull request from GitHub, the old and new branches may no
...@@ -673,7 +685,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -673,7 +685,7 @@ class MergeRequest < ActiveRecord::Base
!ProtectedBranch.protected?(source_project, source_branch) && !ProtectedBranch.protected?(source_project, source_branch) &&
!source_project.root_ref?(source_branch) && !source_project.root_ref?(source_branch) &&
Ability.allowed?(current_user, :push_code, source_project) && Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head diff_head_sha == source_branch_head.try(:sha)
end end
def should_remove_source_branch? def should_remove_source_branch?
......
class UserInteractedProject < ActiveRecord::Base
belongs_to :user
belongs_to :project
validates :project_id, presence: true
validates :user_id, presence: true
CACHE_EXPIRY_TIME = 1.day
# Schema version required for this model
REQUIRED_SCHEMA_VERSION = 20180223120443
class << self
def track(event)
# For events without a project, we simply don't care.
# An example of this is the creation of a snippet (which
# is not related to any project).
return unless event.project_id
attributes = {
project_id: event.project_id,
user_id: event.author_id
}
cached_exists?(attributes) do
transaction(requires_new: true) do
begin
where(attributes).select(1).first || create!(attributes)
true # not caching the whole record here for now
rescue ActiveRecord::RecordNotUnique
# Note, above queries are not atomic and prone
# to race conditions (similar like #find_or_create!).
# In the case where we hit this, the record we want
# already exists - shortcut and return.
true
end
end
end
end
# Check if we can safely call .track (table exists)
def available?
@available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization
end
# Flushes cached information about schema
def reset_column_information
@available_flag = nil
super
end
private
def cached_exists?(project_id:, user_id:, &block)
cache_key = "user_interacted_projects:#{project_id}:#{user_id}"
Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRY_TIME, &block)
end
end
end
...@@ -57,7 +57,7 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -57,7 +57,7 @@ class MergeRequestWidgetEntity < IssuableEntity
# Diff sha's # Diff sha's
expose :diff_head_sha do |merge_request| expose :diff_head_sha do |merge_request|
merge_request.diff_head_sha if merge_request.diff_head_commit merge_request.diff_head_sha.presence
end end
expose :merge_commit_message expose :merge_commit_message
......
...@@ -87,7 +87,7 @@ module Projects ...@@ -87,7 +87,7 @@ module Projects
end end
def after_create_actions def after_create_actions
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"") log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"")
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
@project.write_repository_config @project.write_repository_config
......
...@@ -697,9 +697,11 @@ ...@@ -697,9 +697,11 @@
.checkbox .checkbox
= f.label :version_check_enabled do = f.label :version_check_enabled do
= f.check_box :version_check_enabled = f.check_box :version_check_enabled
Version check enabled Enable version check
.help-block .help-block
Let GitLab inform you when an update is available. GitLab will inform you if a new version is available.
= link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
about what information is shared with GitLab Inc.
.form-group .form-group
.col-sm-offset-2.col-sm-10 .col-sm-offset-2.col-sm-10
- can_be_configured = @application_setting.usage_ping_can_be_configured? - can_be_configured = @application_setting.usage_ping_can_be_configured?
......
= form_errors(hook) = form_errors(hook)
.form-group .form-group
= form.label :url, 'URL', class: 'control-label' = form.label :url, 'URL', class: 'label-light'
.col-sm-10
= form.text_field :url, class: 'form-control' = form.text_field :url, class: 'form-control'
.form-group .form-group
= form.label :token, 'Secret Token', class: 'control-label' = form.label :token, 'Secret Token', class: 'label-light'
.col-sm-10
= form.text_field :token, class: 'form-control' = form.text_field :token, class: 'form-control'
%p.help-block %p.help-block
Use this token to validate received payloads Use this token to validate received payloads
.form-group .form-group
= form.label :url, 'Trigger', class: 'control-label' = form.label :url, 'Trigger', class: 'label-light'
.col-sm-10.prepend-top-10 %ul.list-unstyled
%div %li
.help-block
System hook will be triggered on set of events like creating project System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events. or adding ssh key. But you can also enable extra triggers like Push events.
...@@ -24,21 +23,21 @@ ...@@ -24,21 +23,21 @@
%strong Repository update events %strong Repository update events
%p.light %p.light
This URL will be triggered when repository is updated This URL will be triggered when repository is updated
%div %li
= form.check_box :push_events, class: 'pull-left' = form.check_box :push_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= form.label :push_events, class: 'list-label' do = form.label :push_events, class: 'list-label' do
%strong Push events %strong Push events
%p.light %p.light
This URL will be triggered for each branch updated to the repository This URL will be triggered for each branch updated to the repository
%div %li
= form.check_box :tag_push_events, class: 'pull-left' = form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= form.label :tag_push_events, class: 'list-label' do = form.label :tag_push_events, class: 'list-label' do
%strong Tag push events %strong Tag push events
%p.light %p.light
This URL will be triggered when a new tag is pushed to the repository This URL will be triggered when a new tag is pushed to the repository
%div %li
= form.check_box :merge_requests_events, class: 'pull-left' = form.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= form.label :merge_requests_events, class: 'list-label' do = form.label :merge_requests_events, class: 'list-label' do
...@@ -46,8 +45,7 @@ ...@@ -46,8 +45,7 @@
%p.light %p.light
This URL will be triggered when a merge request is created/updated/merged This URL will be triggered when a merge request is created/updated/merged
.form-group .form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox' = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
.col-sm-10
.checkbox .checkbox
= form.label :enable_ssl_verification do = form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification = form.check_box :enable_ssl_verification
......
- page_title 'System Hooks' - page_title 'System Hooks'
%h3.page-title .row.prepend-top-default
System hooks .col-lg-4
%h4.prepend-top-0
%p.light = page_title
%p
#{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project. used for binding events when GitLab creates a User or Project.
%hr .col-lg-8.append-bottom-default
= form_for @hook, as: :hook, url: admin_hooks_path do |f|
= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
= render partial: 'form', locals: { form: f, hook: @hook } = render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Add system hook', class: 'btn btn-create' = f.submit 'Add system hook', class: 'btn btn-create'
%hr
- if @hooks.any? %hr
- if @hooks.any?
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
System hooks (#{@hooks.count}) System hooks (#{@hooks.count})
...@@ -31,3 +31,5 @@ ...@@ -31,3 +31,5 @@
- if hook.public_send(event) - if hook.public_send(event)
%span.label.label-gray= event.to_s.titleize %span.label.label-gray= event.to_s.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
= render 'shared/plugins/index'
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
- can_admin_label = can?(current_user, :admin_label, @project) - can_admin_label = can?(current_user, :admin_label, @project)
- if @labels.exists? || @prioritized_labels.exists? - if @labels.exists? || @prioritized_labels.exists?
#promote-label-modal
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
.nav-text .nav-text
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
.milestones .milestones
#delete-milestone-modal #delete-milestone-modal
#promote-milestone-modal
%ul.content-list %ul.content-list
= render @milestones = render @milestones
......
...@@ -27,8 +27,15 @@ ...@@ -27,8 +27,15 @@
Edit Edit
- if @project.group - if @project.group
= link_to promote_project_milestone_path(@milestone.project, @milestone), title: "Promote to Group Milestone", class: 'btn btn-grouped', data: { confirm: "Promoting #{@milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do %button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
Promote target: '#promote-milestone-modal',
milestone_title: @milestone.title,
url: promote_project_milestone_path(@milestone.project, @milestone),
container: 'body' },
disabled: true,
type: 'button' }
= _('Promote')
#promote-milestone-modal
- if @milestone.active? - if @milestone.active?
= link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" = link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
......
...@@ -53,8 +53,16 @@ ...@@ -53,8 +53,16 @@
.pull-right.hidden-xs.hidden-sm .pull-right.hidden-xs.hidden-sm
- if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
= link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting #{label.title} will make it available for all projects inside #{label.project.group.name}. Existing project labels with the same name will be merged. This action cannot be reversed.", toggle: "tooltip"}, method: :post do %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
%span.sr-only Promote to Group disabled: true,
type: 'button',
data: { url: promote_project_label_path(label.project, label),
label_title: label.title,
label_color: label.color,
label_text_color: label.text_color,
target: '#promote-label-modal',
container: 'body',
toggle: 'modal' } }
= sprite_icon('level-up') = sprite_icon('level-up')
- if can?(current_user, :admin_label, label) - if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
......
...@@ -51,8 +51,15 @@ ...@@ -51,8 +51,15 @@
\ \
- if @project.group - if @project.group
= link_to promote_project_milestone_path(milestone.project, milestone), title: "Promote to Group Milestone", class: 'btn btn-xs btn-grouped', data: { confirm: "Promoting #{milestone.title} will make it available for all projects inside #{@project.group.name}. Existing project milestones with the same name will be merged. This action cannot be reversed.", toggle: "tooltip" }, method: :post do %button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
Promote disabled: true,
type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone),
milestone_title: milestone.title,
target: '#promote-milestone-modal',
container: 'body',
toggle: 'modal' } }
= _('Promote')
= link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped" = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close btn-grouped"
......
- plugins = Gitlab::Plugin.files
.row.prepend-top-default
.col-lg-4
%h4.prepend-top-0
Plugins
%p
#{link_to 'Plugins', help_page_path('administration/plugins')} are similar to
system hooks but are executed as files instead of sending data to a URL.
.col-lg-8.append-bottom-default
- if plugins.any?
.panel.panel-default
.panel-heading
Plugins (#{plugins.count})
%ul.content-list
- plugins.each do |file|
%li
.monospace
= File.basename(file)
- else
%p.light-well.text-center
No plugins found.
...@@ -66,7 +66,7 @@ class EmailsOnPushWorker ...@@ -66,7 +66,7 @@ class EmailsOnPushWorker
# These are input errors and won't be corrected even if Sidekiq retries # These are input errors and won't be corrected even if Sidekiq retries
rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}") logger.info("Failed to send e-mail for project '#{project.full_name}' to #{recipient}: #{e}")
end end
end end
ensure ensure
......
---
title: CI charts now include the current day
merge_request: 17032
author: Dakkaron
type: changed
---
title: Keep track of projects a user interacted with.
merge_request: 17327
author:
type: other
---
title: Add discussions API for Issues and Snippets
merge_request:
author:
type: added
---
title: Add plugins list to the system hooks page
merge_request: 17518
author:
type: added
--- ---
title: Count comments on diffs as contributions for the contributions calendar title: Count comments on diffs and discussions as contributions for the contributions calendar
merge_request: 17418 merge_request: 17418
author: Riccardo Padovani author: Riccardo Padovani
type: fixed type: fixed
---
title: Added new design for promotion modals
merge_request: 17197
author:
type: other
---
title: Use persisted/memoized value for MRs shas instead of doing git lookups
merge_request: 17555
author:
type: performance
---
title: Add CommonMark markdown engine (experimental)
merge_request: 14835
author: blackst0ne
type: added
---
title: Ensure that OTP backup codes are always invalidated
merge_request:
author:
type: security
---
title: Make --prune a configurable parameter in fetching a git remote
merge_request:
author:
type: performance
---
title: Move Ruby endpoints to OPT_OUT
merge_request:
author:
type: other
...@@ -94,6 +94,7 @@ def instrument_classes(instrumentation) ...@@ -94,6 +94,7 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker) instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
instrumentation.instrument_instance_methods(Rouge::Plugins::CommonMark)
instrumentation.instrument_instance_methods(Rouge::Plugins::Redcarpet) instrumentation.instrument_instance_methods(Rouge::Plugins::Redcarpet)
instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab) instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
......
class CreateUserInteractedProjectsTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
create_table :user_interacted_projects, id: false do |t|
t.references :user, null: false
t.references :project, null: false
end
end
def down
drop_table :user_interacted_projects
end
end
class BuildUserInteractedProjectsTable < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
if Gitlab::Database.postgresql?
PostgresStrategy.new
else
MysqlStrategy.new
end.up
unless index_exists?(:user_interacted_projects, [:project_id, :user_id])
add_concurrent_index :user_interacted_projects, [:project_id, :user_id], unique: true
end
unless foreign_key_exists?(:user_interacted_projects, :user_id)
add_concurrent_foreign_key :user_interacted_projects, :users, column: :user_id, on_delete: :cascade
end
unless foreign_key_exists?(:user_interacted_projects, :project_id)
add_concurrent_foreign_key :user_interacted_projects, :projects, column: :project_id, on_delete: :cascade
end
end
def down
execute "TRUNCATE user_interacted_projects"
if foreign_key_exists?(:user_interacted_projects, :user_id)
remove_foreign_key :user_interacted_projects, :users
end
if foreign_key_exists?(:user_interacted_projects, :project_id)
remove_foreign_key :user_interacted_projects, :projects
end
if index_exists_by_name?(:user_interacted_projects, 'index_user_interacted_projects_on_project_id_and_user_id')
remove_concurrent_index_by_name :user_interacted_projects, 'index_user_interacted_projects_on_project_id_and_user_id'
end
end
private
# Rails' index_exists? doesn't work when you only give it a table and index
# name. As such we have to use some extra code to check if an index exists for
# a given name.
def index_exists_by_name?(table, index)
indexes_for_table[table].include?(index)
end
def indexes_for_table
@indexes_for_table ||= Hash.new do |hash, table_name|
hash[table_name] = indexes(table_name).map(&:name)
end
end
def foreign_key_exists?(table, column)
foreign_keys(table).any? do |key|
key.options[:column] == column.to_s
end
end
class PostgresStrategy < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
BATCH_SIZE = 100_000
SLEEP_TIME = 5
def up
with_index(:events, [:author_id, :project_id], name: 'events_user_interactions_temp', where: 'project_id IS NOT NULL') do
iteration = 0
records = 0
begin
Rails.logger.info "Building user_interacted_projects table, batch ##{iteration}"
result = execute <<~SQL
INSERT INTO user_interacted_projects (user_id, project_id)
SELECT e.user_id, e.project_id
FROM (SELECT DISTINCT author_id AS user_id, project_id FROM events WHERE project_id IS NOT NULL) AS e
LEFT JOIN user_interacted_projects ucp USING (user_id, project_id)
WHERE ucp.user_id IS NULL
LIMIT #{BATCH_SIZE}
SQL
iteration += 1
records += result.cmd_tuples
Rails.logger.info "Building user_interacted_projects table, batch ##{iteration} complete, created #{records} overall"
Kernel.sleep(SLEEP_TIME) if result.cmd_tuples > 0
rescue ActiveRecord::InvalidForeignKey => e
Rails.logger.info "Retry on InvalidForeignKey: #{e}"
retry
end while result.cmd_tuples > 0
end
execute "ANALYZE user_interacted_projects"
end
private
def with_index(*args)
add_concurrent_index(*args) unless index_exists?(*args)
yield
ensure
remove_concurrent_index(*args) if index_exists?(*args)
end
end
class MysqlStrategy < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
def up
execute <<~SQL
INSERT INTO user_interacted_projects (user_id, project_id)
SELECT e.user_id, e.project_id
FROM (SELECT DISTINCT author_id AS user_id, project_id FROM events WHERE project_id IS NOT NULL) AS e
LEFT JOIN user_interacted_projects ucp USING (user_id, project_id)
WHERE ucp.user_id IS NULL
SQL
end
end
end
...@@ -2370,6 +2370,13 @@ ActiveRecord::Schema.define(version: 20180307164427) do ...@@ -2370,6 +2370,13 @@ ActiveRecord::Schema.define(version: 20180307164427) do
add_index "user_custom_attributes", ["key", "value"], name: "index_user_custom_attributes_on_key_and_value", using: :btree add_index "user_custom_attributes", ["key", "value"], name: "index_user_custom_attributes_on_key_and_value", using: :btree
add_index "user_custom_attributes", ["user_id", "key"], name: "index_user_custom_attributes_on_user_id_and_key", unique: true, using: :btree add_index "user_custom_attributes", ["user_id", "key"], name: "index_user_custom_attributes_on_user_id_and_key", unique: true, using: :btree
create_table "user_interacted_projects", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id", null: false
end
add_index "user_interacted_projects", ["project_id", "user_id"], name: "index_user_interacted_projects_on_project_id_and_user_id", unique: true, using: :btree
create_table "user_synced_attributes_metadata", force: :cascade do |t| create_table "user_synced_attributes_metadata", force: :cascade do |t|
t.boolean "name_synced", default: false t.boolean "name_synced", default: false
t.boolean "email_synced", default: false t.boolean "email_synced", default: false
...@@ -2706,6 +2713,8 @@ ActiveRecord::Schema.define(version: 20180307164427) do ...@@ -2706,6 +2713,8 @@ ActiveRecord::Schema.define(version: 20180307164427) do
add_foreign_key "u2f_registrations", "users" add_foreign_key "u2f_registrations", "users"
add_foreign_key "user_callouts", "users", on_delete: :cascade add_foreign_key "user_callouts", "users", on_delete: :cascade
add_foreign_key "user_custom_attributes", "users", on_delete: :cascade add_foreign_key "user_custom_attributes", "users", on_delete: :cascade
add_foreign_key "user_interacted_projects", "projects", name: "fk_722ceba4f7", on_delete: :cascade
add_foreign_key "user_interacted_projects", "users", name: "fk_0894651f08", on_delete: :cascade
add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade
add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
......
...@@ -7,3 +7,4 @@ Explore our features to monitor your GitLab instance: ...@@ -7,3 +7,4 @@ Explore our features to monitor your GitLab instance:
- [GitHub imports](github_imports.md): Monitor the health and progress of GitLab's GitHub importer with various Prometheus metrics. - [GitHub imports](github_imports.md): Monitor the health and progress of GitLab's GitHub importer with various Prometheus metrics.
- [Monitoring uptime](../../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint. - [Monitoring uptime](../../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
- [IP whitelists](ip_whitelist.md): Configure GitLab for monitoring endpoints that provide health check information when probed. - [IP whitelists](ip_whitelist.md): Configure GitLab for monitoring endpoints that provide health check information when probed.
- [nginx_status](https://docs.gitlab.com/omnibus/settings/nginx.html#enabling-disabling-nginx_status): Monitor your Nginx server status
...@@ -163,8 +163,7 @@ in your `.gitlab-ci.yml`. ...@@ -163,8 +163,7 @@ in your `.gitlab-ci.yml`.
Behind the scenes, this works by increasing a counter in the database, and the Behind the scenes, this works by increasing a counter in the database, and the
value of that counter is used to create the key for the cache. After a push, a value of that counter is used to create the key for the cache. After a push, a
new key is generated and the old cache is not valid anymore. Eventually, the new key is generated and the old cache is not valid anymore.
Runner's garbage collector will remove it form the filesystem.
## How shared Runners pick jobs ## How shared Runners pick jobs
......
...@@ -8,20 +8,26 @@ under **Admin area > Settings > Usage statistics**. ...@@ -8,20 +8,26 @@ under **Admin area > Settings > Usage statistics**.
## Version check ## Version check
GitLab can inform you when an update is available and the importance of it. If enabled, version check will inform you if a new version is available and the
importance of it through a status. This is shown on the help page (i.e. `/help`)
for all signed in users, and on the admin pages. The statuses are:
No information other than the GitLab version and the instance's hostname (through the HTTP referer) * Green: You are running the latest version of GitLab.
are collected. * Orange: An updated version of GitLab is available.
* Red: The version of GitLab you are running is vulnerable. You should install
the latest version with security fixes as soon as possible.
In the **Overview** tab you can see if your GitLab version is up to date. There ![Orange version check example](img/update-available.png)
are three cases: 1) you are up to date (green), 2) there is an update available
(yellow) and 3) your version is vulnerable and a security fix is released (red).
In any case, you will see a message informing you of the state and the GitLab Inc. collects your instance's version and hostname (through the HTTP
importance of the update. referer) as part of the version check. No other information is collected.
If enabled, the version status will also be shown in the help page (`/help`) This information is used, among other things, to identify to which versions
for all signed in users. patches will need to be back ported, making sure active GitLab instances remain
secure.
If you disable version check, this information will not be collected. Enable or
disable the version check at **Admin area > Settings > Usage statistics**.
## Usage ping ## Usage ping
......
...@@ -82,7 +82,7 @@ module SharedProject ...@@ -82,7 +82,7 @@ module SharedProject
step 'I should see project "Shop" activity feed' do step 'I should see project "Shop" activity feed' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
expect(page).to have_content "#{@user.name} pushed new branch fix at #{project.name_with_namespace}" expect(page).to have_content "#{@user.name} pushed new branch fix at #{project.full_name}"
end end
step 'I should see project settings' do step 'I should see project settings' do
...@@ -113,12 +113,12 @@ module SharedProject ...@@ -113,12 +113,12 @@ module SharedProject
step 'I should not see project "Archive"' do step 'I should not see project "Archive"' do
project = Project.find_by(name: "Archive") project = Project.find_by(name: "Archive")
expect(page).not_to have_content project.name_with_namespace expect(page).not_to have_content project.full_name
end end
step 'I should see project "Archive"' do step 'I should see project "Archive"' do
project = Project.find_by(name: "Archive") project = Project.find_by(name: "Archive")
expect(page).to have_content project.name_with_namespace expect(page).to have_content project.full_name
end end
# ---------------------------------------- # ----------------------------------------
......
...@@ -111,13 +111,6 @@ module API ...@@ -111,13 +111,6 @@ module API
def gitaly_payload(action) def gitaly_payload(action)
return unless %w[git-receive-pack git-upload-pack].include?(action) return unless %w[git-receive-pack git-upload-pack].include?(action)
if action == 'git-receive-pack'
return unless Gitlab::GitalyClient.feature_enabled?(
:ssh_receive_pack,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT
)
end
{ {
repository: repository.gitaly_repository, repository: repository.gitaly_repository,
address: Gitlab::GitalyClient.address(project.repository_storage), address: Gitlab::GitalyClient.address(project.repository_storage),
......
...@@ -18,7 +18,8 @@ module Banzai ...@@ -18,7 +18,8 @@ module Banzai
def find_object(project, id) def find_object(project, id)
if project && project.valid_repo? if project && project.valid_repo?
project.commit(id) # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/43894
Gitlab::GitalyClient.allow_n_plus_1_calls { project.commit(id) }
end end
end end
......
# `CommonMark` markdown engine for GitLab's Banzai markdown filter.
# This module is used in Banzai::Filter::MarkdownFilter.
# Used gem is `commonmarker` which is a ruby wrapper for libcmark (CommonMark parser)
# including GitHub's GFM extensions.
# Homepage: https://github.com/gjtorikian/commonmarker
module Banzai
module Filter
module MarkdownEngines
class CommonMark
EXTENSIONS = [
:autolink, # provides support for automatically converting URLs to anchor tags.
:strikethrough, # provides support for strikethroughs.
:table, # provides support for tables.
:tagfilter # strips out several "unsafe" HTML tags from being used: https://github.github.com/gfm/#disallowed-raw-html-extension-
].freeze
PARSE_OPTIONS = [
:FOOTNOTES, # parse footnotes.
:STRIKETHROUGH_DOUBLE_TILDE, # parse strikethroughs by double tildes (as redcarpet does).
:VALIDATE_UTF8 # replace illegal sequences with the replacement character U+FFFD.
].freeze
# The `:GITHUB_PRE_LANG` option is not used intentionally because
# it renders a fence block with language as `<pre lang="LANG"><code>some code\n</code></pre>`
# while GitLab's syntax is `<pre><code lang="LANG">some code\n</code></pre>`.
# If in the future the syntax is about to be made GitHub-compatible, please, add `:GITHUB_PRE_LANG` render option below
# and remove `code_block` method from `lib/banzai/renderer/common_mark/html.rb`.
RENDER_OPTIONS = [
:DEFAULT # default rendering system. Nothing special.
].freeze
def initialize
@renderer = Banzai::Renderer::CommonMark::HTML.new(options: RENDER_OPTIONS)
end
def render(text)
doc = CommonMarker.render_doc(text, PARSE_OPTIONS, EXTENSIONS)
@renderer.render(doc)
end
end
end
end
end
# `Redcarpet` markdown engine for GitLab's Banzai markdown filter.
# This module is used in Banzai::Filter::MarkdownFilter.
# Used gem is `redcarpet` which is a ruby library for markdown processing.
# Homepage: https://github.com/vmg/redcarpet
module Banzai
module Filter
module MarkdownEngines
class Redcarpet
OPTIONS = {
fenced_code_blocks: true,
footnotes: true,
lax_spacing: true,
no_intra_emphasis: true,
space_after_headers: true,
strikethrough: true,
superscript: true,
tables: true
}.freeze
def initialize
html_renderer = Banzai::Renderer::Redcarpet::HTML.new
@renderer = ::Redcarpet::Markdown.new(html_renderer, OPTIONS)
end
def render(text)
@renderer.render(text)
end
end
end
end
end
module Banzai module Banzai
module Filter module Filter
class MarkdownFilter < HTML::Pipeline::TextFilter class MarkdownFilter < HTML::Pipeline::TextFilter
# https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
REDCARPET_OPTIONS = {
fenced_code_blocks: true,
footnotes: true,
lax_spacing: true,
no_intra_emphasis: true,
space_after_headers: true,
strikethrough: true,
superscript: true,
tables: true
}.freeze
def initialize(text, context = nil, result = nil) def initialize(text, context = nil, result = nil)
super text, context, result super(text, context, result)
@text = @text.delete "\r"
@renderer = renderer(context[:markdown_engine]).new
@text = @text.delete("\r")
end end
def call def call
html = self.class.renderer.render(@text) @renderer.render(@text).rstrip
html.rstrip!
html
end end
def self.renderer private
Thread.current[:banzai_markdown_renderer] ||= begin
renderer = Banzai::Renderer::HTML.new DEFAULT_ENGINE = :redcarpet
Redcarpet::Markdown.new(renderer, REDCARPET_OPTIONS)
def engine(engine_from_context)
engine_from_context ||= DEFAULT_ENGINE
engine_from_context.to_s.classify
end end
def renderer(engine_from_context)
"Banzai::Filter::MarkdownEngines::#{engine(engine_from_context)}".constantize
rescue NameError
raise NameError, "`#{engine_from_context}` is unknown markdown engine"
end end
end end
end end
......
require 'rouge/plugins/common_mark'
require 'rouge/plugins/redcarpet' require 'rouge/plugins/redcarpet'
module Banzai module Banzai
......
module Banzai
module Renderer
module CommonMark
class HTML < CommonMarker::HtmlRenderer
def code_block(node)
block do
code = node.string_content
lang = node.fence_info
lang_attr = lang.present? ? %Q{ lang="#{lang}"} : ''
result =
"<pre>" \
"<code#{lang_attr}>#{html_escape(code)}</code>" \
"</pre>"
out(result)
end
end
end
end
end
end
module Banzai
module Renderer
class HTML < Redcarpet::Render::HTML
def block_code(code, lang)
lang_attr = lang ? %Q{ lang="#{lang}"} : ''
"\n<pre>" \
"<code#{lang_attr}>#{html_escape(code)}</code>" \
"</pre>"
end
end
end
end
module Banzai
module Renderer
module Redcarpet
class HTML < ::Redcarpet::Render::HTML
def block_code(code, lang)
lang_attr = lang ? %Q{ lang="#{lang}"} : ''
"\n<pre>" \
"<code#{lang_attr}>#{html_escape(code)}</code>" \
"</pre>"
end
end
end
end
end
...@@ -135,7 +135,7 @@ module Gitlab ...@@ -135,7 +135,7 @@ module Gitlab
if label.valid? if label.valid?
@labels[label_params[:title]] = label @labels[label_params[:title]] = label
else else
raise "Failed to create label \"#{label_params[:title]}\" for project \"#{project.name_with_namespace}\"" raise "Failed to create label \"#{label_params[:title]}\" for project \"#{project.full_name}\""
end end
end end
end end
......
...@@ -68,10 +68,11 @@ module Gitlab ...@@ -68,10 +68,11 @@ module Gitlab
class YearChart < Chart class YearChart < Chart
include MonthlyInterval include MonthlyInterval
attr_reader :to, :from
def initialize(*) def initialize(*)
@to = Date.today.end_of_month @to = Date.today.end_of_month.end_of_day
@from = @to.years_ago(1).beginning_of_month @from = @to.years_ago(1).beginning_of_month.beginning_of_day
@format = '%d %B %Y' @format = '%d %B %Y'
super super
...@@ -80,10 +81,11 @@ module Gitlab ...@@ -80,10 +81,11 @@ module Gitlab
class MonthChart < Chart class MonthChart < Chart
include DailyInterval include DailyInterval
attr_reader :to, :from
def initialize(*) def initialize(*)
@to = Date.today @to = Date.today.end_of_day
@from = @to - 30.days @from = 1.month.ago.beginning_of_day
@format = '%d %B' @format = '%d %B'
super super
...@@ -92,10 +94,11 @@ module Gitlab ...@@ -92,10 +94,11 @@ module Gitlab
class WeekChart < Chart class WeekChart < Chart
include DailyInterval include DailyInterval
attr_reader :to, :from
def initialize(*) def initialize(*)
@to = Date.today @to = Date.today.end_of_day
@from = @to - 7.days @from = 1.week.ago.beginning_of_day
@format = '%d %B' @format = '%d %B'
super super
......
...@@ -23,7 +23,7 @@ module Gitlab ...@@ -23,7 +23,7 @@ module Gitlab
mr_events = event_counts(date_from, :merge_requests) mr_events = event_counts(date_from, :merge_requests)
.having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest") .having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest")
note_events = event_counts(date_from, :merge_requests) note_events = event_counts(date_from, :merge_requests)
.having(action: [Event::COMMENTED], target_type: %w(Note DiffNote)) .having(action: [Event::COMMENTED])
union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events, note_events]) union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events, note_events])
events = Event.find_by_sql(union.to_sql).map(&:attributes) events = Event.find_by_sql(union.to_sql).map(&:attributes)
......
...@@ -31,7 +31,7 @@ module Gitlab ...@@ -31,7 +31,7 @@ module Gitlab
# TODO: do we still need it? # TODO: do we still need it?
project_id: project.id, project_id: project.id,
project_name: project.name_with_namespace, project_name: project.full_name,
user: { user: {
id: user.try(:id), id: user.try(:id),
......
...@@ -347,7 +347,7 @@ module Gitlab ...@@ -347,7 +347,7 @@ module Gitlab
# #
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324 # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324
def to_diff def to_diff
Gitlab::GitalyClient.migrate(:commit_patch) do |is_enabled| Gitlab::GitalyClient.migrate(:commit_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
@repository.gitaly_commit_client.patch(id) @repository.gitaly_commit_client.patch(id)
else else
......
...@@ -228,7 +228,7 @@ module Gitlab ...@@ -228,7 +228,7 @@ module Gitlab
end end
def has_local_branches? def has_local_branches?
gitaly_migrate(:has_local_branches) do |is_enabled| gitaly_migrate(:has_local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_repository_client.has_local_branches? gitaly_repository_client.has_local_branches?
else else
...@@ -715,7 +715,7 @@ module Gitlab ...@@ -715,7 +715,7 @@ module Gitlab
end end
def add_branch(branch_name, user:, target:) def add_branch(branch_name, user:, target:)
gitaly_migrate(:operation_user_create_branch) do |is_enabled| gitaly_migrate(:operation_user_create_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_add_branch(branch_name, user, target) gitaly_add_branch(branch_name, user, target)
else else
...@@ -725,7 +725,7 @@ module Gitlab ...@@ -725,7 +725,7 @@ module Gitlab
end end
def add_tag(tag_name, user:, target:, message: nil) def add_tag(tag_name, user:, target:, message: nil)
gitaly_migrate(:operation_user_add_tag) do |is_enabled| gitaly_migrate(:operation_user_add_tag, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_add_tag(tag_name, user: user, target: target, message: message) gitaly_add_tag(tag_name, user: user, target: target, message: message)
else else
...@@ -735,7 +735,7 @@ module Gitlab ...@@ -735,7 +735,7 @@ module Gitlab
end end
def rm_branch(branch_name, user:) def rm_branch(branch_name, user:)
gitaly_migrate(:operation_user_delete_branch) do |is_enabled| gitaly_migrate(:operation_user_delete_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_operations_client.user_delete_branch(branch_name, user) gitaly_operations_client.user_delete_branch(branch_name, user)
else else
...@@ -810,7 +810,7 @@ module Gitlab ...@@ -810,7 +810,7 @@ module Gitlab
end end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
gitaly_migrate(:revert) do |is_enabled| gitaly_migrate(:revert, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
args = { args = {
user: user, user: user,
commit: commit, commit: commit,
...@@ -876,7 +876,7 @@ module Gitlab ...@@ -876,7 +876,7 @@ module Gitlab
# Delete the specified branch from the repository # Delete the specified branch from the repository
def delete_branch(branch_name) def delete_branch(branch_name)
gitaly_migrate(:delete_branch) do |is_enabled| gitaly_migrate(:delete_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_ref_client.delete_branch(branch_name) gitaly_ref_client.delete_branch(branch_name)
else else
...@@ -903,7 +903,7 @@ module Gitlab ...@@ -903,7 +903,7 @@ module Gitlab
# create_branch("feature") # create_branch("feature")
# create_branch("other-feature", "master") # create_branch("other-feature", "master")
def create_branch(ref, start_point = "HEAD") def create_branch(ref, start_point = "HEAD")
gitaly_migrate(:create_branch) do |is_enabled| gitaly_migrate(:create_branch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_ref_client.create_branch(ref, start_point) gitaly_ref_client.create_branch(ref, start_point)
else else
...@@ -1014,7 +1014,7 @@ module Gitlab ...@@ -1014,7 +1014,7 @@ module Gitlab
end end
def languages(ref = nil) def languages(ref = nil)
Gitlab::GitalyClient.migrate(:commit_languages) do |is_enabled| gitaly_migrate(:commit_languages, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_commit_client.languages(ref) gitaly_commit_client.languages(ref)
else else
......
...@@ -38,7 +38,7 @@ module Gitlab ...@@ -38,7 +38,7 @@ module Gitlab
end end
def project_link def project_link
"[#{project.name_with_namespace}](#{project.web_url})" "[#{project.full_name}](#{project.web_url})"
end end
def author_profile_link def author_profile_link
......
...@@ -53,7 +53,7 @@ module Gitlab ...@@ -53,7 +53,7 @@ module Gitlab
end end
def pretext def pretext
"Issue *#{@resource.to_reference}* from #{project.name_with_namespace}" "Issue *#{@resource.to_reference}* from #{project.full_name}"
end end
end end
end end
......
...@@ -10,6 +10,7 @@ module Gitlab ...@@ -10,6 +10,7 @@ module Gitlab
INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.freeze
NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze NOTIFICATION_CHANNEL = 'workhorse:notifications'.freeze
ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze
# Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32 # Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
# bytes https://tools.ietf.org/html/rfc4868#section-2.6 # bytes https://tools.ietf.org/html/rfc4868#section-2.6
...@@ -17,6 +18,8 @@ module Gitlab ...@@ -17,6 +18,8 @@ module Gitlab
class << self class << self
def git_http_ok(repository, is_wiki, user, action, show_all_refs: false) def git_http_ok(repository, is_wiki, user, action, show_all_refs: false)
raise "Unsupported action: #{action}" unless ALLOWED_GIT_HTTP_ACTIONS.include?(action.to_s)
project = repository.project project = repository.project
repo_path = repository.path_to_repo repo_path = repository.path_to_repo
params = { params = {
...@@ -31,24 +34,7 @@ module Gitlab ...@@ -31,24 +34,7 @@ module Gitlab
token: Gitlab::GitalyClient.token(project.repository_storage) token: Gitlab::GitalyClient.token(project.repository_storage)
} }
params[:Repository] = repository.gitaly_repository.to_h params[:Repository] = repository.gitaly_repository.to_h
feature_enabled = case action.to_s
when 'git_receive_pack'
Gitlab::GitalyClient.feature_enabled?(
:post_receive_pack,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT
)
when 'git_upload_pack'
true
when 'info_refs'
true
else
raise "Unsupported action: #{action}"
end
if feature_enabled
params[:GitalyServer] = server params[:GitalyServer] = server
end
params params
end end
......
# A rouge plugin for CommonMark markdown engine.
# Used to highlight code generated by CommonMark.
module Rouge
module Plugins
module CommonMark
def code_block(code, language)
lexer = Lexer.find_fancy(language, code) || Lexers::PlainText
formatter = rouge_formatter(lexer)
formatter.format(lexer.lex(code))
end
# override this method for custom formatting behavior
def rouge_formatter(lexer)
Formatters::HTMLLegacy.new(css_class: "highlight #{lexer.tag}")
end
end
end
end
...@@ -50,7 +50,7 @@ module SystemCheck ...@@ -50,7 +50,7 @@ module SystemCheck
if should_sanitize? if should_sanitize?
"#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... " "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
else else
"#{project.name_with_namespace.color(:yellow)} ... " "#{project.full_name.color(:yellow)} ... "
end end
end end
......
...@@ -98,10 +98,8 @@ describe Projects::MilestonesController do ...@@ -98,10 +98,8 @@ describe Projects::MilestonesController do
it 'shows group milestone' do it 'shows group milestone' do
post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
group_milestone = assigns(:milestone) expect(flash[:notice]).to eq("#{milestone.title} promoted to group milestone")
expect(response).to redirect_to(project_milestones_path(project))
expect(response).to redirect_to(group_milestone_path(project.group, group_milestone.iid))
expect(flash[:notice]).to eq('Milestone has been promoted to group milestone.')
end end
end end
......
...@@ -24,6 +24,16 @@ describe 'Admin::Hooks' do ...@@ -24,6 +24,16 @@ describe 'Admin::Hooks' do
visit admin_hooks_path visit admin_hooks_path
expect(page).to have_content(system_hook.url) expect(page).to have_content(system_hook.url)
end end
it 'renders plugins list as well' do
allow(Gitlab::Plugin).to receive(:files).and_return(['foo.rb', 'bar.clj'])
visit admin_hooks_path
expect(page).to have_content('Plugins')
expect(page).to have_content('foo.rb')
expect(page).to have_content('bar.clj')
end
end end
describe 'New Hook' do describe 'New Hook' do
......
...@@ -145,6 +145,18 @@ feature 'Login' do ...@@ -145,6 +145,18 @@ feature 'Login' do
expect { enter_code(codes.sample) } expect { enter_code(codes.sample) }
.to change { user.reload.otp_backup_codes.size }.by(-1) .to change { user.reload.otp_backup_codes.size }.by(-1)
end end
it 'invalidates backup codes twice in a row' do
random_code = codes.delete(codes.sample)
expect { enter_code(random_code) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
gitlab_sign_out
gitlab_sign_in(user)
expect { enter_code(codes.sample) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
end
end end
context 'with invalid code' do context 'with invalid code' do
......
import Vue from 'vue';
import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
import eventHub from '~/pages/projects/labels/event_hub';
import axios from '~/lib/utils/axios_utils';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('Promote label modal', () => {
let vm;
const Component = Vue.extend(promoteLabelModal);
const labelMockData = {
labelTitle: 'Documentation',
labelColor: '#5cb85c',
labelTextColor: '#ffffff',
url: `${gl.TEST_HOST}/dummy/promote/labels`,
};
describe('Modal title and description', () => {
beforeEach(() => {
vm = mountComponent(Component, labelMockData);
});
afterEach(() => {
vm.$destroy();
});
it('contains the proper description', () => {
expect(vm.text).toContain('Promoting this label will make it available for all projects inside the group');
});
it('contains a label span with the color', () => {
const labelFromTitle = vm.$el.querySelector('.modal-header .label.color-label');
expect(labelFromTitle.style.backgroundColor).not.toBe(null);
expect(labelFromTitle.textContent).toContain(vm.labelTitle);
});
});
describe('When requesting a label promotion', () => {
beforeEach(() => {
vm = mountComponent(Component, {
...labelMockData,
});
spyOn(eventHub, '$emit');
});
afterEach(() => {
vm.$destroy();
});
it('redirects when a label is promoted', (done) => {
const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(labelMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestStarted', labelMockData.url);
return Promise.resolve({
request: {
responseURL,
},
});
});
vm.onSubmit()
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { labelUrl: labelMockData.url, successful: true });
})
.then(done)
.catch(done.fail);
});
it('displays an error if promoting a label failed', (done) => {
const dummyError = new Error('promoting label failed');
dummyError.response = { status: 500 };
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(labelMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestStarted', labelMockData.url);
return Promise.reject(dummyError);
});
vm.onSubmit()
.catch((error) => {
expect(error).toBe(dummyError);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { labelUrl: labelMockData.url, successful: false });
})
.then(done)
.catch(done.fail);
});
});
});
import Vue from 'vue';
import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
import axios from '~/lib/utils/axios_utils';
import mountComponent from '../../../../helpers/vue_mount_component_helper';
describe('Promote milestone modal', () => {
let vm;
const Component = Vue.extend(promoteMilestoneModal);
const milestoneMockData = {
milestoneTitle: 'v1.0',
url: `${gl.TEST_HOST}/dummy/promote/milestones`,
};
describe('Modal title and description', () => {
beforeEach(() => {
vm = mountComponent(Component, milestoneMockData);
});
afterEach(() => {
vm.$destroy();
});
it('contains the proper description', () => {
expect(vm.text).toContain('Promoting this milestone will make it available for all projects inside the group.');
});
it('contains the correct title', () => {
expect(vm.title).toEqual('Promote v1.0 to group milestone?');
});
});
describe('When requesting a milestone promotion', () => {
beforeEach(() => {
vm = mountComponent(Component, {
...milestoneMockData,
});
spyOn(eventHub, '$emit');
});
afterEach(() => {
vm.$destroy();
});
it('redirects when a milestone is promoted', (done) => {
const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestStarted', milestoneMockData.url);
return Promise.resolve({
request: {
responseURL,
},
});
});
vm.onSubmit()
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', { milestoneUrl: milestoneMockData.url, successful: true });
})
.then(done)
.catch(done.fail);
});
it('displays an error if promoting a milestone failed', (done) => {
const dummyError = new Error('promoting milestone failed');
dummyError.response = { status: 500 };
spyOn(axios, 'post').and.callFake((url) => {
expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestStarted', milestoneMockData.url);
return Promise.reject(dummyError);
});
vm.onSubmit()
.catch((error) => {
expect(error).toBe(dummyError);
expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', { milestoneUrl: milestoneMockData.url, successful: false });
})
.then(done)
.catch(done.fail);
});
});
});
...@@ -10,6 +10,7 @@ describe('clipboard button', () => { ...@@ -10,6 +10,7 @@ describe('clipboard button', () => {
vm = mountComponent(Component, { vm = mountComponent(Component, {
text: 'copy me', text: 'copy me',
title: 'Copy this value into Clipboard!', title: 'Copy this value into Clipboard!',
cssClass: 'btn-danger',
}); });
}); });
...@@ -28,4 +29,8 @@ describe('clipboard button', () => { ...@@ -28,4 +29,8 @@ describe('clipboard button', () => {
expect(vm.$el.getAttribute('data-placement')).toEqual('top'); expect(vm.$el.getAttribute('data-placement')).toEqual('top');
expect(vm.$el.getAttribute('data-container')).toEqual(null); expect(vm.$el.getAttribute('data-container')).toEqual(null);
}); });
it('should render provided classname', () => {
expect(vm.$el.classList).toContain('btn-danger');
});
}); });
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Charts do describe Gitlab::Ci::Charts do
context "yearchart" do
let(:project) { create(:project) }
let(:chart) { Gitlab::Ci::Charts::YearChart.new(project) }
subject { chart.to }
it 'goes until the end of the current month (including the whole last day of the month)' do
is_expected.to eq(Date.today.end_of_month.end_of_day)
end
it 'starts at the beginning of the current year' do
expect(chart.from).to eq(chart.to.years_ago(1).beginning_of_month.beginning_of_day)
end
end
context "monthchart" do
let(:project) { create(:project) }
let(:chart) { Gitlab::Ci::Charts::MonthChart.new(project) }
subject { chart.to }
it 'includes the whole current day' do
is_expected.to eq(Date.today.end_of_day)
end
it 'starts one month ago' do
expect(chart.from).to eq(1.month.ago.beginning_of_day)
end
end
context "weekchart" do
let(:project) { create(:project) }
let(:chart) { Gitlab::Ci::Charts::WeekChart.new(project) }
subject { chart.to }
it 'includes the whole current day' do
is_expected.to eq(Date.today.end_of_day)
end
it 'starts one week ago' do
expect(chart.from).to eq(1.week.ago.beginning_of_day)
end
end
context "pipeline_times" do context "pipeline_times" do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) } let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) }
......
...@@ -77,6 +77,13 @@ describe Gitlab::ContributionsCalendar do ...@@ -77,6 +77,13 @@ describe Gitlab::ContributionsCalendar do
expect(calendar(contributor).activity_dates[today]).to eq(1) expect(calendar(contributor).activity_dates[today]).to eq(1)
end end
it "counts the discussions on merge requests and issues" do
create_event(public_project, today, 0, Event::COMMENTED, :discussion_note_on_merge_request)
create_event(public_project, today, 2, Event::COMMENTED, :discussion_note_on_issue)
expect(calendar(contributor).activity_dates[today]).to eq(2)
end
context "when events fall under different dates depending on the time zone" do context "when events fall under different dates depending on the time zone" do
before do before do
create_event(public_project, today, 1) create_event(public_project, today, 1)
......
...@@ -142,15 +142,15 @@ describe Environment do ...@@ -142,15 +142,15 @@ describe Environment do
let(:commit) { project.commit.parent } let(:commit) { project.commit.parent }
it 'returns deployment id for the environment' do it 'returns deployment id for the environment' do
expect(environment.first_deployment_for(commit)).to eq deployment1 expect(environment.first_deployment_for(commit.id)).to eq deployment1
end end
it 'return nil when no deployment is found' do it 'return nil when no deployment is found' do
expect(environment.first_deployment_for(head_commit)).to eq nil expect(environment.first_deployment_for(head_commit.id)).to eq nil
end end
it 'returns a UTF-8 ref' do it 'returns a UTF-8 ref' do
expect(environment.first_deployment_for(commit).ref).to be_utf8 expect(environment.first_deployment_for(commit.id).ref).to be_utf8
end end
end end
......
...@@ -49,6 +49,22 @@ describe Event do ...@@ -49,6 +49,22 @@ describe Event do
end end
end end
end end
describe 'after_create :track_user_interacted_projects' do
let(:event) { build(:push_event, project: project, author: project.owner) }
it 'passes event to UserInteractedProject.track' do
expect(UserInteractedProject).to receive(:available?).and_return(true)
expect(UserInteractedProject).to receive(:track).with(event)
event.save
end
it 'does not call UserInteractedProject.track if its not yet available' do
expect(UserInteractedProject).to receive(:available?).and_return(false)
expect(UserInteractedProject).not_to receive(:track)
event.save
end
end
end end
describe "Push event" do describe "Push event" do
......
...@@ -1036,7 +1036,7 @@ describe Repository do ...@@ -1036,7 +1036,7 @@ describe Repository do
end end
end end
context 'with Gitaly disabled', :skip_gitaly_mock do context 'with Gitaly disabled', :disable_gitaly do
context 'when pre hooks were successful' do context 'when pre hooks were successful' do
it 'runs without errors' do it 'runs without errors' do
hook = double(trigger: [true, nil]) hook = double(trigger: [true, nil])
...@@ -1970,7 +1970,7 @@ describe Repository do ...@@ -1970,7 +1970,7 @@ describe Repository do
it_behaves_like 'adding tag' it_behaves_like 'adding tag'
end end
context 'when Gitaly operation_user_add_tag feature is disabled', :skip_gitaly_mock do context 'when Gitaly operation_user_add_tag feature is disabled', :disable_gitaly do
it_behaves_like 'adding tag' it_behaves_like 'adding tag'
it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do
...@@ -2029,7 +2029,7 @@ describe Repository do ...@@ -2029,7 +2029,7 @@ describe Repository do
end end
end end
context 'with gitaly disabled', :skip_gitaly_mock do context 'with gitaly disabled', :disable_gitaly do
it_behaves_like "user deleting a branch" it_behaves_like "user deleting a branch"
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
......
require 'spec_helper'
describe UserInteractedProject do
describe '.track' do
subject { described_class.track(event) }
let(:event) { build(:event) }
Event::ACTIONS.each do |action|
context "for all actions (event types)" do
let(:event) { build(:event, action: action) }
it 'creates a record' do
expect { subject }.to change { described_class.count }.from(0).to(1)
end
end
end
it 'sets project accordingly' do
subject
expect(described_class.first.project).to eq(event.project)
end
it 'sets user accordingly' do
subject
expect(described_class.first.user).to eq(event.author)
end
it 'only creates a record once per user/project' do
expect do
subject
described_class.track(event)
end.to change { described_class.count }.from(0).to(1)
end
describe 'with an event without a project' do
let(:event) { build(:event, project: nil) }
it 'ignores the event' do
expect { subject }.not_to change { described_class.count }
end
end
end
describe '.available?' do
before do
described_class.instance_variable_set('@available_flag', nil)
end
it 'checks schema version and properly caches positive result' do
expect(ActiveRecord::Migrator).to receive(:current_version).and_return(described_class::REQUIRED_SCHEMA_VERSION - 1 - rand(1000))
expect(described_class.available?).to be_falsey
expect(ActiveRecord::Migrator).to receive(:current_version).and_return(described_class::REQUIRED_SCHEMA_VERSION + rand(1000))
expect(described_class.available?).to be_truthy
expect(ActiveRecord::Migrator).not_to receive(:current_version)
expect(described_class.available?).to be_truthy # cached response
end
end
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:user_id) }
end
...@@ -335,21 +335,8 @@ describe API::Internal do ...@@ -335,21 +335,8 @@ describe API::Internal do
end end
context "git push" do context "git push" do
context "gitaly disabled", :disable_gitaly do context 'project as namespace/project' do
it "has the correct payload" do it do
push(key, project)
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gitaly"]).to be_nil
expect(user).not_to have_an_activity_record
end
end
context "gitaly enabled" do
it "has the correct payload" do
push(key, project) push(key, project)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
...@@ -365,17 +352,6 @@ describe API::Internal do ...@@ -365,17 +352,6 @@ describe API::Internal do
expect(user).not_to have_an_activity_record expect(user).not_to have_an_activity_record
end end
end end
context 'project as namespace/project' do
it do
push(key, project)
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
end
end
end end
end end
......
...@@ -147,9 +147,9 @@ describe MergeRequestWidgetEntity do ...@@ -147,9 +147,9 @@ describe MergeRequestWidgetEntity do
allow(resource).to receive(:diff_head_sha) { 'sha' } allow(resource).to receive(:diff_head_sha) { 'sha' }
end end
context 'when no diff head commit' do context 'when diff head commit is empty' do
it 'returns nil' do it 'returns nil' do
allow(resource).to receive(:diff_head_commit) { nil } allow(resource).to receive(:diff_head_sha) { '' }
expect(subject[:diff_head_sha]).to be_nil expect(subject[:diff_head_sha]).to be_nil
end end
...@@ -157,8 +157,6 @@ describe MergeRequestWidgetEntity do ...@@ -157,8 +157,6 @@ describe MergeRequestWidgetEntity do
context 'when diff head commit present' do context 'when diff head commit present' do
it 'returns diff head commit short id' do it 'returns diff head commit short id' do
allow(resource).to receive(:diff_head_commit) { double }
expect(subject[:diff_head_sha]).to eq('sha') expect(subject[:diff_head_sha]).to eq('sha')
end end
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