Commit 26626c34 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'master' into pawel/prometheus-business-metrics-ee-2273

parents 4e85ce1c 0ba593ec
......@@ -136,6 +136,7 @@ gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4'
gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
......
......@@ -139,6 +139,8 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
colorize (0.7.7)
commonmarker (0.17.8)
ruby-enum (~> 0.5)
concord (0.1.5)
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
......@@ -827,6 +829,8 @@ GEM
rubocop (>= 0.51)
rubocop-rspec (1.22.1)
rubocop (>= 0.52.1)
ruby-enum (0.7.2)
i18n
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.16.2)
......@@ -1050,6 +1054,7 @@ DEPENDENCIES
charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
commonmarker (~> 0.17)
concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0)
creole (~> 0.5.0)
......
......@@ -186,7 +186,7 @@
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
css-class="btn btn-default js-clipboard-btn"
class="js-clipboard-btn"
/>
</span>
</div>
......
import { __ } from './locale';
import _ from 'underscore';
import { __, sprintf } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
class ImporterStatus {
constructor(jobsUrl, importUrl) {
constructor({ jobsUrl, importUrl, ciCdOnly }) {
this.jobsUrl = jobsUrl;
this.importUrl = importUrl;
this.ciCdOnly = ciCdOnly;
this.initStatusPage();
this.setAutoUpdate();
}
......@@ -45,6 +48,7 @@ class ImporterStatus {
repo_id: id,
target_namespace: targetNamespace,
new_name: newName,
ci_cd_only: this.ciCdOnly,
})
.then(({ data }) => {
const job = $(`tr#repo_${id}`);
......@@ -54,7 +58,13 @@ class ImporterStatus {
$('table.import-jobs tbody').prepend(job);
job.addClass('active');
job.find('.import-actions').html('<i class="fa fa-spinner fa-spin" aria-label="importing"></i> started');
const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
job.find('.import-actions').html(sprintf(
_.escape(__('%{loadingIcon} Started')), {
loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(connectingVerb)}"></i>`,
},
false,
));
})
.catch(() => flash(__('An error occurred while importing project')));
}
......@@ -71,13 +81,16 @@ class ImporterStatus {
switch (job.import_status) {
case 'finished':
jobItem.removeClass('active').addClass('success');
statusField.html('<span><i class="fa fa-check"></i> done</span>');
statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
break;
case 'scheduled':
statusField.html(`${spinner} scheduled`);
statusField.html(`${spinner} ${__('Scheduled')}`);
break;
case 'started':
statusField.html(`${spinner} started`);
statusField.html(`${spinner} ${__('Started')}`);
break;
case 'failed':
statusField.html(__('Failed'));
break;
default:
statusField.html(job.import_status);
......@@ -98,7 +111,11 @@ function initImporterStatus() {
if (importerStatus) {
const data = importerStatus.dataset;
return new ImporterStatus(data.jobsImportPath, data.importPath);
return new ImporterStatus({
jobsUrl: data.jobsImportPath,
importUrl: data.importPath,
ciCdOnly: convertPermissionToBoolean(data.ciCdOnly),
});
}
}
......
......@@ -35,6 +35,7 @@
<clipboard-button
title="Copy file path to clipboard"
:text="diffFile.submoduleLink"
css-class="btn-default btn-transparent btn-clipboard"
/>
</span>
</div>
......@@ -79,6 +80,7 @@
<clipboard-button
title="Copy file path to clipboard"
:text="diffFile.filePath"
css-class="btn-default btn-transparent btn-clipboard"
/>
<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 Translate from '~/vue_shared/translate';
import deleteMilestoneModal from './components/delete_milestone_modal.vue';
import eventHub from './event_hub';
import initDeleteMilestoneModal from './delete_milestone_modal_init';
import initPromoteMilestoneModal from './promote_milestone_modal_init';
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');
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,
});
},
});
initDeleteMilestoneModal();
initPromoteMilestoneModal();
};
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 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 @@
v-if="repo.location"
:text="clipboardText"
:title="repo.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
<div class="controls hidden-xs pull-right">
......
......@@ -90,6 +90,7 @@
v-if="item.location"
:title="item.location"
:text="clipboardText(item.location)"
css-class="btn-default btn-transparent btn-clipboard"
/>
</td>
<td>
......
......@@ -67,6 +67,7 @@
<clipboard-button
:text="branchNameClipboardData"
:title="__('Copy branch name to clipboard')"
css-class="btn-default btn-transparent btn-clipboard"
/>
{{ s__("mrWidget|into") }}
......
......@@ -31,7 +31,7 @@
cssClass: {
type: String,
required: false,
default: 'btn btn-default btn-transparent btn-clipboard',
default: 'btn-default',
},
},
};
......@@ -40,6 +40,7 @@
<template>
<button
type="button"
class="btn"
:class="cssClass"
:title="title"
:data-clipboard-text="text"
......
......@@ -2,14 +2,17 @@
background-color: $modal-body-bg;
padding: #{3 * $grid-size} #{2 * $grid-size};
.page-title {
margin-top: 0;
.page-title,
.modal-title {
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
}
}
.page-title {
margin-top: 0;
}
}
.modal-body {
......
......@@ -56,6 +56,7 @@ module AuthenticatesWithTwoFactor
session.delete(:otp_user_id)
remember_me(user) if user_params[:remember_me] == '1'
user.save!
sign_in(user)
else
user.increment_failed_attempts!
......
class Import::GithubController < Import::BaseController
prepend ::EE::Import::GithubController
before_action :verify_import_enabled
before_action :provider_auth, only: [:status, :jobs, :create]
......@@ -42,7 +44,9 @@ class Import::GithubController < Import::BaseController
target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path)
if can?(current_user, :create_projects, target_namespace)
project = Gitlab::LegacyGithubImport::ProjectCreator.new(repo, project_name, target_namespace, current_user, access_params, type: provider).execute
project = Gitlab::LegacyGithubImport::ProjectCreator
.new(repo, project_name, target_namespace, current_user, access_params, type: provider)
.execute(extra_project_attrs)
if project.persisted?
render json: ProjectSerializer.new.represent(project)
......@@ -73,15 +77,15 @@ class Import::GithubController < Import::BaseController
end
def new_import_url
public_send("new_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend
public_send("new_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
def status_import_url
public_send("status_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend
public_send("status_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
def callback_import_url
public_send("callback_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend
public_send("callback_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend
end
def provider_unauthorized
......@@ -116,4 +120,12 @@ class Import::GithubController < Import::BaseController
def client_options
{}
end
def extra_project_attrs
{}
end
def extra_import_params
{}
end
end
......@@ -112,12 +112,14 @@ class Projects::LabelsController < Projects::ApplicationController
begin
return render_404 unless promote_service.execute(@label)
flash[:notice] = "#{@label.title} promoted to group label."
respond_to do |format|
format.html do
redirect_to(project_labels_path(@project),
notice: 'Label was promoted to a Group Label')
redirect_to(project_labels_path(@project), status: 303)
end
format.json do
render json: { url: project_labels_path(@project) }
end
format.js
end
rescue ActiveRecord::RecordInvalid => e
Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label"
......
......@@ -191,7 +191,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
begin
@merge_request.environments_for(current_user).map do |environment|
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 =
if environment.stop_action? && can?(current_user, :create_deployment, environment)
......
......@@ -75,9 +75,17 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def promote
promoted_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)
Milestones::PromoteService.new(project, current_user).execute(milestone)
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
redirect_to milestone, alert: error.message
end
......
......@@ -36,6 +36,42 @@ module ImportHelper
_('Please wait while we import the repository for you. Refresh at will.')
end
def import_github_title
_('Import repositories from GitHub')
end
def import_github_authorize_message
_('To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:')
end
def import_github_personal_access_token_message
personal_access_token_link = link_to _('Personal Access Token'), 'https://github.com/settings/tokens'
if github_import_configured?
_('Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link }
else
_('To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link }
end
end
def import_configure_github_admin_message
github_integration_link = link_to 'GitHub integration', help_page_path('integration/github')
if current_user.admin?
_('Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link }
else
_('Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link }
end
end
def import_githubish_choose_repository_message
_('Choose which repositories you want to import.')
end
def import_all_githubish_repositories_button_label
_('Import all repositories')
end
private
def github_project_url(full_path)
......
......@@ -121,7 +121,7 @@ class Notify < BaseMailer
if Gitlab::IncomingEmail.enabled? && @sent_notification
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
......
......@@ -99,8 +99,8 @@ class Environment < ActiveRecord::Base
folder_name == "production"
end
def first_deployment_for(commit)
ref = project.repository.ref_name_for_sha(ref_path, commit.sha)
def first_deployment_for(commit_sha)
ref = project.repository.ref_name_for_sha(ref_path, commit_sha)
return nil unless ref
......
......@@ -65,6 +65,7 @@ class Event < ActiveRecord::Base
# Callbacks
after_create :reset_project_activity
after_create :set_last_repository_updated_at, if: :push?
after_create :track_user_interacted_projects
# Scopes
scope :recent, -> { reorder(id: :desc) }
......@@ -395,4 +396,11 @@ class Event < ActiveRecord::Base
Project.unscoped.where(id: project_id)
.update_all(last_repository_updated_at: created_at)
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
......@@ -385,15 +385,27 @@ class MergeRequest < ActiveRecord::Base
end
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
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
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
# When importing a pull request from GitHub, the old and new branches may no
......@@ -673,7 +685,7 @@ class MergeRequest < ActiveRecord::Base
!ProtectedBranch.protected?(source_project, source_branch) &&
!source_project.root_ref?(source_branch) &&
Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head
diff_head_sha == source_branch_head.try(:sha)
end
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
# Diff sha's
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
expose :merge_commit_message
......
......@@ -87,7 +87,7 @@ module Projects
end
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?
@project.write_repository_config
......
......@@ -697,9 +697,11 @@
.checkbox
= f.label :version_check_enabled do
= f.check_box :version_check_enabled
Version check enabled
Enable version check
.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
.col-sm-offset-2.col-sm-10
- can_be_configured = @application_setting.usage_ping_can_be_configured?
......
= form_errors(hook)
.form-group
= form.label :url, 'URL', class: 'control-label'
.col-sm-10
= form.text_field :url, class: 'form-control'
= form.label :url, 'URL', class: 'label-light'
= form.text_field :url, class: 'form-control'
.form-group
= form.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= form.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
= form.label :token, 'Secret Token', class: 'label-light'
= form.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group
= form.label :url, 'Trigger', class: 'control-label'
.col-sm-10.prepend-top-10
%div
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.
= form.label :url, 'Trigger', class: 'label-light'
%ul.list-unstyled
%li
.help-block
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.
.prepend-top-default
= form.check_box :repository_update_events, class: 'pull-left'
......@@ -24,21 +23,21 @@
%strong Repository update events
%p.light
This URL will be triggered when repository is updated
%div
%li
= form.check_box :push_events, class: 'pull-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This URL will be triggered for each branch updated to the repository
%div
%li
= form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
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'
.prepend-left-20
= form.label :merge_requests_events, class: 'list-label' do
......@@ -46,9 +45,8 @@
%p.light
This URL will be triggered when a merge request is created/updated/merged
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox'
.col-sm-10
.checkbox
= form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification
%strong Enable SSL verification
= form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
.checkbox
= form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification
%strong Enable SSL verification
- page_title 'System Hooks'
%h3.page-title
System hooks
.row.prepend-top-default
.col-lg-4
%h4.prepend-top-0
= page_title
%p
#{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.
%p.light
#{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.
.col-lg-8.append-bottom-default
= form_for @hook, as: :hook, url: admin_hooks_path do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
= f.submit 'Add system hook', class: 'btn btn-create'
%hr
%hr
= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Add system hook', class: 'btn btn-create'
%hr
- if @hooks.any?
.panel.panel-default
.panel-heading
System hooks (#{@hooks.count})
%ul.content-list
- @hooks.each do |hook|
%li
.controls
= render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: hook, button_class: 'btn-sm'
= link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url
%div
- SystemHook.triggers.each_value do |event|
- if hook.public_send(event)
%span.label.label-gray= event.to_s.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
- if @hooks.any?
.panel.panel-default
.panel-heading
System hooks (#{@hooks.count})
%ul.content-list
- @hooks.each do |hook|
%li
.controls
= render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: hook, button_class: 'btn-sm'
= link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url
%div
- SystemHook.triggers.each_value do |event|
- if hook.public_send(event)
%span.label.label-gray= event.to_s.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
= render 'shared/plugins/index'
......@@ -2,11 +2,11 @@
- provider_title = Gitlab::ImportSources.title(provider)
%p.light
Select projects you want to import.
= import_githubish_choose_repository_message
%hr
%p
= button_tag class: "btn btn-import btn-success js-import-all" do
Import all projects
= import_all_githubish_repositories_button_label
= icon("spinner spin", class: "loading-icon")
.table-responsive
......@@ -16,9 +16,9 @@
%colgroup.import-jobs-status-col
%thead
%tr
%th From #{provider_title}
%th To GitLab
%th Status
%th= _('From %{provider_title}') % { provider_title: provider_title }
%th= _('To GitLab')
%th= _('Status')
%tbody
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
......@@ -30,10 +30,12 @@
- if project.import_status == 'finished'
%span
%i.fa.fa-check
done
= _('Done')
- elsif project.import_status == 'started'
%i.fa.fa-spinner.fa-spin
started
= _('Started')
- elsif project.import_status == 'failed'
= _('Failed')
- else
= project.human_import_status_name
......@@ -55,7 +57,9 @@
= text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
= has_ci_cd_only_params? ? _('Connect') : _('Import')
= icon("spinner spin", class: "loading-icon")
.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } }
.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}",
import_path: "#{url_for([:import, provider])}",
ci_cd_only: "#{has_ci_cd_only_params?}" } }
- page_title "GitHub Import"
- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import')
- page_title title
- breadcrumb_title title
- header_title "Projects", root_path
%h3.page-title
= icon 'github', text: 'Import Projects from GitHub'
= icon 'github', text: import_github_title
- if github_import_configured?
%p
To import a GitHub project, you first need to authorize GitLab to access
the list of your GitHub repositories:
= import_github_authorize_message
= link_to 'List your GitHub repositories', status_import_github_path, class: 'btn btn-success'
= link_to _('List your GitHub repositories'), status_import_github_path, class: 'btn btn-success'
%hr
%p
- if github_import_configured?
Alternatively,
- else
To import a GitHub project,
you can use a
= succeed '.' do
= link_to 'Personal Access Token', 'https://github.com/settings/tokens'
When you create your Personal Access Token,
you will need to select the <code>repo</code> scope, so we can display a
list of your public and private repositories which are available for import.
= import_github_personal_access_token_message
= form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do
.form-group
= text_field_tag :personal_access_token, '', class: 'form-control', placeholder: "Personal Access Token", size: 40
= submit_tag 'List your GitHub repositories', class: 'btn btn-success'
= text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40
= submit_tag _('List your GitHub repositories'), class: 'btn btn-success'
-# EE-specific start
= hidden_field_tag :ci_cd_only, params[:ci_cd_only]
-# EE-specific end
- unless github_import_configured?
%hr
%p
Note:
- if current_user.admin?
As an administrator you may like to configure
- else
Consider asking your GitLab administrator to configure
= link_to 'GitHub integration', help_page_path("integration/github")
which will allow login via GitHub and allow importing projects without
generating a Personal Access Token.
= import_configure_github_admin_message
- page_title "GitHub Import"
- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import')
- page_title title
- breadcrumb_title title
- header_title "Projects", root_path
%h3.page-title
= icon 'github', text: 'Import Projects from GitHub'
= icon 'github', text: import_github_title
= render 'import/githubish_status', provider: 'github'
......@@ -4,6 +4,7 @@
- can_admin_label = can?(current_user, :admin_label, @project)
- if @labels.exists? || @prioritized_labels.exists?
#promote-label-modal
%div{ class: container_class }
.top-area.adjust
.nav-text
......
......@@ -13,6 +13,7 @@
.milestones
#delete-milestone-modal
#promote-milestone-modal
%ul.content-list
= render @milestones
......
......@@ -27,8 +27,15 @@
Edit
- 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
Promote
%button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
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?
= link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
......
......@@ -77,7 +77,7 @@
= icon('gitlab', text: 'GitLab export')
%div
- if github_import_enabled?
= link_to new_import_github_path, class: 'btn import_github' do
= link_to new_import_github_path, class: 'btn js-import-github' do
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
......
......@@ -53,8 +53,16 @@
.pull-right.hidden-xs.hidden-sm
- 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
%span.sr-only Promote to Group
%button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
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')
- 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
......
......@@ -51,8 +51,15 @@
\
- 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
Promote
%button.js-promote-project-milestone-button.btn.btn-xs.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
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"
......
- 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.
......@@ -134,6 +134,7 @@
- object_storage:object_storage_migrate_uploads
- admin_emails
- create_github_webhook
- elastic_batch_project_indexer
- elastic_commit_indexer
- elastic_indexer
......
......@@ -66,7 +66,7 @@ class EmailsOnPushWorker
# These are input errors and won't be corrected even if Sidekiq retries
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
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
author: Riccardo Padovani
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)
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::Formatters::HTMLGitlab)
......
......@@ -73,6 +73,7 @@
# EE-specific queues
- [ldap_group_sync, 2]
- [create_github_webhook, 2]
- [chat_notification, 2]
- [geo, 1]
- [repository_remove_remote, 1]
......
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
......@@ -1963,6 +1963,7 @@ ActiveRecord::Schema.define(version: 20180307164427) do
t.integer "jobs_cache_index"
t.boolean "mirror_overwrites_diverged_branches"
t.string "external_authorization_classification_label"
t.string "external_webhook_token"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......@@ -2385,6 +2386,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", ["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|
t.boolean "name_synced", default: false
t.boolean "email_synced", default: false
......@@ -2722,6 +2730,8 @@ ActiveRecord::Schema.define(version: 20180307164427) do
add_foreign_key "u2f_registrations", "users"
add_foreign_key "user_callouts", "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 "users_star_projects", "projects", name: "fk_22cd27ddfc", 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:
- [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.
- [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`.
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
new key is generated and the old cache is not valid anymore. Eventually, the
Runner's garbage collector will remove it form the filesystem.
new key is generated and the old cache is not valid anymore.
## How shared Runners pick jobs
......
......@@ -8,20 +8,26 @@ under **Admin area > Settings > Usage statistics**.
## 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)
are collected.
* Green: You are running the latest version of GitLab.
* 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
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).
![Orange version check example](img/update-available.png)
In any case, you will see a message informing you of the state and the
importance of the update.
GitLab Inc. collects your instance's version and hostname (through the HTTP
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`)
for all signed in users.
This information is used, among other things, to identify to which versions
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
......
## CI/CD for external repositories
>[Introduced][ee-4642] in [GitLab Premium][eep] 10.6.
Instead of importing the repo directly to GitLab, you can connect your
external repository to get GitLab CI/CD benefits.
This will set up [repository mirroring](../../workflow/repository_mirroring.md)
and create a stripped-down version of a project that has issues, merge requests,
container registry, wiki, and snippets disabled but
[can be re-enabled later on](settings/index.md#sharing-and-permissions).
1. From your GitLab dashboard click **New project**
1. Switch to the **CI/CD for external repo** tab
1. Choose **GitHub** or **Repo by URL**
1. The next steps are similar to the [import flow](import/index.md)
![CI/CD for external repository project creation](img/ci_cd_for_external_repo.png)
[ee-4642]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4642
[eep]: https://about.gitlab.com/products/
......@@ -126,6 +126,12 @@ If you want, you can import all your GitHub projects in one go by hitting
You can also choose a different name for the project and a different namespace,
if you have the privileges to do so.
## Mirroring
[Project mirroring](../../../workflow/repository_mirroring.md) can be set up to keep your imported project in sync. Additionally you can configure GitLab to send pipeline status updates back GitHub with the [GitHub Project Integration](../integrations/github.md).
If you import you project using "CI/CD for external repo" then both of the above will be automatically configured.
## Making the import process go faster
For large projects it may take a while to import all data. To reduce the time
......
......@@ -16,6 +16,8 @@ In addition to the specific migration documentation above, you can import any
Git repository via HTTP from the New Project page. Be aware that if the
repository is too large the import can timeout.
There is also the option of [connecting your external repository to get CI/CD benefits](../ci_cd_for_external_repo.md).
## Migrating from self-hosted GitLab to GitLab.com
You can copy your repos by changing the remote and pushing to the new server,
......
......@@ -10,21 +10,3 @@ You can import your existing repositories by providing the Git URL:
1. Once complete, you will be redirected to your newly created project
![Import project by repo URL](img/import_projects_from_repo_url.png)
## CI/CD for external repositories
>[Introduced][ee-4642] in [GitLab Premium][eep] 10.6.
Instead of importing the repo directly to GitLab, you can connect your
external repository to get GitLab CI/CD benefits.
This will set up [repository mirroring](../../../workflow/repository_mirroring.md) and create a stripped-down version of a project
that has issues, merge requests, container registry, wiki, and snippets disabled
but [can be re-enabled later on](../settings/index.md#sharing-and-permissions).
1. From your GitLab dashboard click **New project**
1. Switch to the **CI/CD for external repo** tab
1. Follow the same import project steps (see above)
[ee-4642]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4642
[eep]: https://about.gitlab.com/products/
......@@ -115,6 +115,13 @@ Read through the documentation on [project settings](settings/index.md).
- [Export a project from GitLab](settings/import_export.md#exporting-a-project-and-its-data)
- [Importing and exporting projects between GitLab instances](settings/import_export.md)
## CI/CD for external repositories
Instead of importing a repository directly to GitLab, you can connect your repository
as a CI/CD project.
Read through the documentation on [CI/CD for external repositories](ci_cd_for_external_repo.md).
## Project's members
Learn how to [add members to your projects](members/index.md).
......
......@@ -2,6 +2,8 @@
GitLab provides integration for updating pipeline statuses on GitHub. This is especially useful if using GitLab for CI/CD only.
This project integration is separate from the [instance wide GitHub integration][gh-integration] and is automatically configured on [GitHub import][gh-import].
![Pipeline status update on GitHub](img/github_status_check_pipeline_update.png)
## Configuration
......@@ -28,3 +30,5 @@ This integration requires a [GitHub API token](https://github.com/settings/token
![Configure GitHub Project Integration](img/github_configuration.png)
[gh-import]: ../import/github.md#mirroring
[gh-integration]: ../../../integration/github.md
module EE
module Import
module GithubController
extend ::Gitlab::Utils::Override
override :extra_project_attrs
def extra_project_attrs
super.merge(ci_cd_only: params[:ci_cd_only])
end
override :extra_import_params
def extra_import_params
extra_params = super
ci_cd_only = ::Gitlab::Utils.to_boolean(params[:ci_cd_only])
extra_params[:ci_cd_only] = true if ci_cd_only
extra_params
end
end
end
end
......@@ -44,5 +44,71 @@ module EE
super
end
end
override :import_github_title
def import_github_title
if has_ci_cd_only_params?
_('Connect repositories from GitHub')
else
super
end
end
override :import_github_authorize_message
def import_github_authorize_message
if has_ci_cd_only_params?
_('To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:')
else
super
end
end
override :import_github_personal_access_token_message
def import_github_personal_access_token_message
if has_ci_cd_only_params?
personal_access_token_link = link_to _('Personal Access Token'), 'https://github.com/settings/tokens'
if github_import_configured?
_('Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect.').html_safe % { personal_access_token_link: personal_access_token_link }
else
_('To connect GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to connect.').html_safe % { personal_access_token_link: personal_access_token_link }
end
else
super
end
end
override :import_configure_github_admin_message
def import_configure_github_admin_message
if has_ci_cd_only_params?
github_integration_link = link_to 'GitHub integration', help_page_path('integration/github')
if current_user.admin?
_('Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link }
else
_('Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token.').html_safe % { github_integration_link: github_integration_link }
end
else
super
end
end
override :import_githubish_choose_repository_message
def import_githubish_choose_repository_message
if has_ci_cd_only_params?
_('Choose which repositories you want to connect and run CI/CD pipelines.')
else
super
end
end
override :import_all_githubish_repositories_button_label
def import_all_githubish_repositories_button_label
if has_ci_cd_only_params?
_('Connect all repositories')
else
super
end
end
end
end
......@@ -86,6 +86,12 @@ module EE
end
end
def ensure_external_webhook_token
return if external_webhook_token.present?
self.external_webhook_token = Devise.friendly_token
end
def shared_runners_limit_namespace
if Feature.enabled?(:shared_runner_minutes_on_root_namespace)
root_namespace
......
module CiCd
class GithubIntegrationSetupService
attr_reader :project
def initialize(project)
@project = project
end
def execute
github_integration.save
end
private
def github_integration
@github_integration ||= project.build_github_service(github_params)
end
def github_params
GithubParams.new(project).configuration_params
end
class GithubParams
def initialize(project)
@project = project
end
def repository_url
"#{import_uri.scheme}://#{import_uri.host}/#{repo_full_name}"
end
def configuration_params
{
active: true,
repository_url: repository_url,
token: github_access_token
}
end
private
def github_access_token
@project.import_data&.credentials&.dig(:user)
end
def repo_full_name
@project.import_source
end
def import_uri
URI.parse(@project.import_url)
end
end
end
end
module CiCd
class GithubSetupService
attr_reader :project
def initialize(project)
@project = project
end
def execute
create_webhook
setup_project_integration
end
private
def create_webhook
::CreateGithubWebhookWorker.perform_async(project.id)
end
def setup_project_integration
::CiCd::GithubIntegrationSetupService.new(project).execute
end
end
end
module CiCd
class SetupProject < ::BaseService
def execute
return if project.import_url.blank?
update_project
disable_project_features
setup_external_service
end
private
def update_project
project.update_attributes(
container_registry_enabled: false,
mirror: true,
mirror_trigger_builds: true,
mirror_overwrites_diverged_branches: true,
only_mirror_protected_branches: false,
mirror_user_id: current_user.id
)
end
def disable_project_features
project.project_feature.update_attributes(
issues_access_level: ProjectFeature::DISABLED,
merge_requests_access_level: ProjectFeature::DISABLED,
wiki_access_level: ProjectFeature::DISABLED,
snippets_access_level: ProjectFeature::DISABLED
)
end
def setup_external_service
return unless requires_extra_setup?
service_class.new(@project).execute
end
def requires_extra_setup?
return false if project.import_type.blank?
Gitlab::ImportSources.importer(project.import_type).try(:requires_ci_cd_setup?)
end
def service_class
"CiCd::#{@project.import_type.classify}SetupService".constantize
end
end
end
......@@ -9,7 +9,7 @@ module EE
mirror = params.delete(:mirror)
mirror_user_id = params.delete(:mirror_user_id)
mirror_trigger_builds = params.delete(:mirror_trigger_builds)
ci_cd_only = params.delete(:ci_cd_only)
ci_cd_only = ::Gitlab::Utils.to_boolean(params.delete(:ci_cd_only))
project = super do |project|
# Repository size limit comes as MB from the view
......@@ -61,7 +61,7 @@ module EE
def setup_ci_cd_project
return unless ::License.feature_available?(:ci_cd_projects)
::Projects::SetupCiCd.new(project, current_user).execute
::CiCd::SetupProject.new(project, current_user).execute
end
def log_audit_event(project)
......
......@@ -8,11 +8,18 @@
= _('Run CI/CD pipelines for external repositories')
%p
= _('Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled.')
%p
- more_info_link = link_to _('More info'), help_page_path('user/project/integrations/github')
= _('If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}').html_safe % { more_info_link: more_info_link }
.form-group.import-btn-container.prepend-top-20.clearfix
= f.label :visibility_level, class: 'label-light' do
= s_('ImportButtons|Connect repositories from')
.import-buttons
%div
- if github_import_enabled?
= link_to new_import_github_path(ci_cd_only: true), class: 'btn js-import-github' do
= icon('github', text: 'GitHub')
%div
- if git_import_enabled?
%button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
......
class CreateGithubWebhookWorker
include ApplicationWorker
include GrapeRouteHelpers::NamedRouteMatcher
attr_reader :project
def perform(project_id)
@project = Project.find(project_id)
create_webhook
end
def create_webhook
client.create_hook(
project.import_source,
'web',
{
url: webhook_url,
content_type: 'json',
secret: webhook_token,
insecure_ssl: 1
},
{
events: ['push'],
active: true
}
)
end
private
def client
@client ||= Gitlab::LegacyGithubImport::Client.new(access_token)
end
def access_token
@access_token ||= project.import_data.credentials[:user]
end
def webhook_url
"#{Settings.gitlab.url}#{api_v4_projects_mirror_pull_path(id: project.id)}"
end
def webhook_token
project.ensure_external_webhook_token
project.save if project.changed?
project.external_webhook_token
end
end
---
title: Add GitHub support to CI/CD for external repositories
merge_request: 4688
author:
type: added
---
title: GitHub CI/CD import sets up pipeline notification integration
merge_request: 4687
author:
type: added
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddExternalWebhookTokenToProjects < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :projects, :external_webhook_token, :string
end
end
require_dependency 'declarative_policy'
module API
class ProjectMirror < Grape::API
helpers do
def github_webhook_signature
@github_webhook_signature ||= headers['X-Hub-Signature']
end
def authenticate_from_github_webhook!
return unless github_webhook_signature
unless valid_github_signature?
Guest.can?(:read_project, project) ? unauthorized! : not_found!
end
end
def valid_github_signature?
request.body.rewind
token = project.external_webhook_token
payload_body = request.body.read
signature = 'sha1=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), token, payload_body)
Rack::Utils.secure_compare(signature, github_webhook_signature)
end
def authenticate_with_webhook_token!
if github_webhook_signature
not_found! unless project
authenticate_from_github_webhook!
else
authenticate!
authorize_admin_project
end
end
def project
@project ||= github_webhook_signature ? find_project(params[:id]) : user_project
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Triggers a pull mirror operation'
post ":id/mirror/pull" do
authenticate_with_webhook_token!
return render_api_error!('The project is not mirrored', 400) unless project.mirror?
project.force_import_job!
status 200
end
end
end
end
module EE
module Gitlab
module GithubImport
module ParallelImporter
extend ActiveSupport::Concern
class_methods do
def requires_ci_cd_setup?
true
end
end
end
end
end
end
......@@ -37,12 +37,23 @@ feature 'New project' do
end
context 'CI/CD for external repositories', :js do
let(:repo) do
OpenStruct.new(
id: 123,
login: 'some-github-repo',
owner: OpenStruct.new(login: 'some-github-repo'),
name: 'some-github-repo',
full_name: 'my-user/some-github-repo',
clone_url: 'https://github.com/my-user/some-github-repo.git'
)
end
context 'when licensed' do
before do
stub_licensed_features(ci_cd_projects: true)
end
it 'shows CI/CD tab' do
it 'shows CI/CD tab and pane' do
visit new_project_path
expect(page).to have_css('#ci-cd-project-tab')
......@@ -52,6 +63,24 @@ feature 'New project' do
expect(page).to have_css('#ci-cd-project-pane')
end
it '"Import project" tab creates projects with features enabled' do
visit new_project_path
find('#import-project-tab').click
page.within '#import-project-pane' do
first('.js-import-git-toggle-button').click
fill_in 'project_import_url', with: 'http://foo.git'
fill_in 'project_path', with: 'import-project-with-features1'
choose 'project_visibility_level_20'
click_button 'Create project'
created_project = Project.last
expect(current_path).to eq(project_path(created_project))
expect(created_project.project_feature).to be_issues_enabled
end
end
it 'creates CI/CD project from repo URL' do
visit new_project_path
find('#ci-cd-project-tab').click
......@@ -70,6 +99,62 @@ feature 'New project' do
expect(created_project.project_feature).not_to be_issues_enabled
end
end
it 'creates CI/CD project from GitHub' do
visit new_project_path
find('#ci-cd-project-tab').click
page.within '#ci-cd-project-pane' do
find('.js-import-github').click
end
expect(page).to have_text('Connect repositories from GitHub')
allow_any_instance_of(Gitlab::LegacyGithubImport::Client).to receive(:repos).and_return([repo])
fill_in 'personal_access_token', with: 'fake-token'
click_button 'List your GitHub repositories'
wait_for_requests
# Mock the POST `/import/github`
allow_any_instance_of(Gitlab::LegacyGithubImport::Client).to receive(:repo).and_return(repo)
project = create(:project, name: 'some-github-repo', creator: user, import_type: 'github', import_status: 'finished', import_url: repo.clone_url)
allow_any_instance_of(CiCd::SetupProject).to receive(:setup_external_service)
CiCd::SetupProject.new(project, user).execute
allow_any_instance_of(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:execute).with(hash_including(ci_cd_only: true))
.and_return(project)
click_button 'Connect'
wait_for_requests
expect(page).to have_text('Started')
wait_for_requests
expect(page).to have_text('Done')
created_project = Project.last
expect(created_project.name).to eq('some-github-repo')
expect(created_project.mirror).to eq(true)
expect(created_project.project_feature).not_to be_issues_enabled
end
it 'stays on GitHub import page after access token failure' do
visit new_project_path
find('#ci-cd-project-tab').click
page.within '#ci-cd-project-pane' do
find('.js-import-github').click
end
allow_any_instance_of(Gitlab::LegacyGithubImport::Client).to receive(:repos).and_raise(Octokit::Unauthorized)
fill_in 'personal_access_token', with: 'unauthorized-fake-token'
click_button 'List your GitHub repositories'
expect(page).to have_text('Access denied to your GitHub account.')
expect(page).to have_current_path(new_import_github_path(ci_cd_only: true))
end
end
context 'when unlicensed' do
......
......@@ -38,7 +38,7 @@ describe Geo::LfsObjectRegistryFinder, :geo do
create(:geo_file_registry, :lfs, file_id: lfs_object_1.id)
create(:geo_file_registry, :lfs, file_id: lfs_object_2.id)
create(:geo_file_registry, :lfs, file_id: lfs_object_3.id)
lfs_object_1.update!(file_store: ObjectStorage::Store::REMOTE)
lfs_object_1.update_column(:file_store, ObjectStorage::Store::REMOTE)
expect(subject.count_synced_lfs_objects).to eq 2
end
......@@ -72,7 +72,7 @@ describe Geo::LfsObjectRegistryFinder, :geo do
create(:geo_file_registry, :lfs, file_id: lfs_object_1.id)
create(:geo_file_registry, :lfs, file_id: lfs_object_2.id)
create(:geo_file_registry, :lfs, file_id: lfs_object_3.id)
lfs_object_1.update!(file_store: ObjectStorage::Store::REMOTE)
lfs_object_1.update_column(:file_store, ObjectStorage::Store::REMOTE)
expect(subject.count_synced_lfs_objects).to eq 1
end
......@@ -98,7 +98,7 @@ describe Geo::LfsObjectRegistryFinder, :geo do
create(:geo_file_registry, :lfs, file_id: lfs_object_1.id, success: false)
create(:geo_file_registry, :lfs, file_id: lfs_object_2.id, success: false)
create(:geo_file_registry, :lfs, file_id: lfs_object_3.id, success: false)
lfs_object_1.update!(file_store: ObjectStorage::Store::REMOTE)
lfs_object_1.update_column(:file_store, ObjectStorage::Store::REMOTE)
expect(subject.count_failed_lfs_objects).to eq 2
end
......@@ -132,7 +132,7 @@ describe Geo::LfsObjectRegistryFinder, :geo do
create(:geo_file_registry, :lfs, file_id: lfs_object_1.id, success: false)
create(:geo_file_registry, :lfs, file_id: lfs_object_2.id, success: false)
create(:geo_file_registry, :lfs, file_id: lfs_object_3.id, success: false)
lfs_object_1.update!(file_store: ObjectStorage::Store::REMOTE)
lfs_object_1.update_column(:file_store, ObjectStorage::Store::REMOTE)
expect(subject.count_failed_lfs_objects).to eq 1
end
......
require 'spec_helper'
describe Gitlab::LegacyGithubImport::ProjectCreator do
let(:user) { create(:user) }
let(:namespace) { create(:group, owner: user) }
let(:repo) do
OpenStruct.new(
login: 'vim',
name: 'vim',
full_name: 'asd/vim',
clone_url: 'https://gitlab.com/asd/vim.git'
)
end
subject(:service) do
described_class.new(repo, repo.name, namespace, user, github_access_token: 'asdffg')
end
before do
namespace.add_owner(user)
stub_licensed_features(ci_cd_projects: true)
allow_any_instance_of(EE::Project).to receive(:add_import_job)
allow_any_instance_of(CiCd::SetupProject).to receive(:setup_external_service)
end
describe '#execute' do
context 'creating a CI/CD only project' do
let(:params) { { ci_cd_only: true } }
it 'creates a project' do
expect { service.execute(params) }.to change(Project, :count).by(1)
end
it 'calls the service to setup the project' do
expect(CiCd::SetupProject).to receive_message_chain(:new, :execute)
service.execute(params)
end
end
context 'creating a regular project' do
let(:params) { {} }
it 'creates a project' do
expect { service.execute(params) }.to change(Project, :count).by(1)
end
it "doesn't apply any special setup" do
expect(CiCd::SetupProject).not_to receive(:new)
service.execute(params)
end
end
end
end
......@@ -102,6 +102,18 @@ describe Project do
end
end
describe '#ensure_external_webhook_token' do
let(:project) { create(:project, :repository) }
it "sets external_webhook_token when it's missing" do
project.update_attribute(:external_webhook_token, nil)
expect(project.external_webhook_token).to be_blank
project.ensure_external_webhook_token
expect(project.external_webhook_token).to be_present
end
end
describe 'hard failing a mirror' do
it 'sends a notification' do
project = create(:project, :mirror, :import_started)
......
# -*- coding: utf-8 -*-
require 'spec_helper'
describe API::ProjectMirror do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
describe 'POST /projects/:id/mirror/pull' do
context 'when the project is not mirrored' do
it 'returns error' do
allow(project).to receive(:mirror?).and_return(false)
post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(400)
end
end
context 'when the project is mirrored' do
before do
allow_any_instance_of(Projects::UpdateMirrorService).to receive(:execute).and_return(status: :success)
end
context 'when import state is' do
def project_in_state(state)
project = create(:project, :repository, :mirror, state, namespace: user.namespace)
project.mirror_data.update_attributes(next_execution_timestamp: 10.minutes.from_now)
project
end
it 'none it triggers the pull mirroring operation' do
project = project_in_state(:import_none)
expect(UpdateAllMirrorsWorker).to receive(:perform_async).once
post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200)
end
it 'failed it triggers the pull mirroring operation' do
project = project_in_state(:import_failed)
expect(UpdateAllMirrorsWorker).to receive(:perform_async).once
post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200)
end
it 'finished it triggers the pull mirroring operation' do
project = project_in_state(:import_finished)
expect(UpdateAllMirrorsWorker).to receive(:perform_async).once
post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200)
end
it 'scheduled does not trigger the pull mirroring operation and returns 200' do
project = project_in_state(:import_scheduled)
expect(UpdateAllMirrorsWorker).not_to receive(:perform_async)
post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200)
end
it 'started does not trigger the pull mirroring operation and returns 200' do
project = project_in_state(:import_started)
expect(UpdateAllMirrorsWorker).not_to receive(:perform_async)
post api("/projects/#{project.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200)
end
end
context 'when user' do
let(:project_mirrored) { create(:project, :repository, :mirror, :import_finished, namespace: user.namespace) }
def project_member(role, user)
create(:project_member, role, user: user, project: project_mirrored)
end
context 'is unauthenticated' do
it 'returns authentication error' do
post api("/projects/#{project_mirrored.id}/mirror/pull")
expect(response).to have_gitlab_http_status(401)
end
end
context 'is authenticated as developer' do
it 'returns forbidden error' do
project_member(:developer, user2)
post api("/projects/#{project_mirrored.id}/mirror/pull", user2)
expect(response).to have_gitlab_http_status(403)
end
end
context 'is authenticated as reporter' do
it 'returns forbidden error' do
project_member(:reporter, user2)
post api("/projects/#{project_mirrored.id}/mirror/pull", user2)
expect(response).to have_gitlab_http_status(403)
end
end
context 'is authenticated as guest' do
it 'returns forbidden error' do
project_member(:guest, user2)
post api("/projects/#{project_mirrored.id}/mirror/pull", user2)
expect(response).to have_gitlab_http_status(403)
end
end
context 'is authenticated as master' do
it 'triggers the pull mirroring operation' do
project_member(:master, user2)
post api("/projects/#{project_mirrored.id}/mirror/pull", user2)
expect(response).to have_gitlab_http_status(200)
end
end
context 'is authenticated as owner' do
it 'triggers the pull mirroring operation' do
post api("/projects/#{project_mirrored.id}/mirror/pull", user)
expect(response).to have_gitlab_http_status(200)
end
end
end
context 'authenticating from GitHub signature' do
let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
let(:project_mirrored) { create(:project, :repository, :mirror, :import_finished, visibility: visibility) }
def do_post
post api("/projects/#{project_mirrored.id}/mirror/pull"), {}, { 'X-Hub-Signature' => 'signature' }
end
context "when it's valid" do
before do
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:project).and_return(project_mirrored)
allow(endpoint).to receive(:valid_github_signature?).and_return(true)
end
end
it 'syncs the mirror' do
expect(project_mirrored).to receive(:force_import_job!)
do_post
end
end
context "when it's invalid" do
before do
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:project).and_return(project_mirrored)
allow(endpoint).to receive(:valid_github_signature?).and_return(false)
end
end
after do
Grape::Endpoint.before_each nil
end
it "doesn't sync the mirror" do
expect(project_mirrored).not_to receive(:force_import_job!)
post api("/projects/#{project_mirrored.id}/mirror/pull"), {}, { 'X-Hub-Signature' => 'signature' }
end
context 'with a public project' do
let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
it 'returns a 401 status' do
do_post
expect(response).to have_gitlab_http_status(401)
end
end
context 'with an internal project' do
let(:visibility) { Gitlab::VisibilityLevel::INTERNAL }
it 'returns a 404 status' do
do_post
expect(response).to have_gitlab_http_status(404)
end
end
context 'with a private project' do
let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
it 'returns a 404 status' do
do_post
expect(response).to have_gitlab_http_status(404)
end
end
end
end
end
end
end
require 'spec_helper'
describe CiCd::GithubIntegrationSetupService do
let(:repo_full_name) { "MyUser/my-project" }
let(:api_token) { "abcdefghijk123" }
let(:import_url) { "https://#{api_token}@github.com/#{repo_full_name}.git" }
let(:credentials) { { user: api_token } }
let(:project) do
create(:project, import_source: repo_full_name,
import_url: import_url,
import_data_attributes: { credentials: credentials } )
end
subject { described_class.new(project) }
before do
subject.execute
end
describe 'sets up GitHub service integration' do
let(:integration) { project.github_service }
specify 'with API token' do
expect(integration.token).to eq api_token
end
specify 'with repo URL' do
expect(integration.repository_url).to eq 'https://github.com/MyUser/my-project'
end
end
end
require 'spec_helper'
describe CiCd::GithubSetupService do
let(:project) { create(:project) }
subject do
described_class.new(project)
end
describe '#execute' do
it 'creates the webhook in the background' do
expect(CreateGithubWebhookWorker).to receive(:perform_async).with(project.id)
subject.execute
end
it 'sets up GithubService project integration' do
allow(subject).to receive(:create_webhook)
subject.execute
expect(project.github_service).to be_active
end
end
end
require 'spec_helper'
describe CiCd::SetupProject do
let(:user) { create(:user) }
let(:project) { create(:project, creator: user, import_type: 'github', import_url: 'http://foo.com') }
subject do
described_class.new(project, project.creator)
end
before do
allow(CiCd::GithubSetupService).to receive_message_chain(:new, :execute)
end
it 'sets up pull mirroring on the project' do
subject.execute
expect(project.mirror).to be_truthy
expect(project.mirror_trigger_builds).to be_truthy
expect(project.mirror_user_id).to eq(user.id)
end
it 'disables some features' do
subject.execute
project_feature = project.project_feature
expect(project.container_registry_enabled).to be_falsey
expect(project_feature).not_to be_issues_enabled
expect(project_feature).not_to be_merge_requests_enabled
expect(project_feature).not_to be_wiki_enabled
expect(project_feature.snippets_access_level).to eq(ProjectFeature::DISABLED)
end
context 'when import_url is blank' do
before do
project.update_attribute(:import_url, nil)
end
it "doesn't update the project" do
expect(project).not_to receive(:update_project)
expect(project).not_to receive(:disable_project_features)
subject.execute
end
end
describe '#setup_external_service' do
context 'when import_type is missing' do
it "does not invoke the service class" do
project.update_attribute(:import_type, nil)
expect(CiCd::GithubSetupService).not_to receive(:new)
subject.execute
end
end
context "when importer doesn't require extra setup" do
it "does not invoke the service class" do
allow(Gitlab::GithubImport::ParallelImporter).to receive(:requires_ci_cd_setup?).and_return(false)
expect(CiCd::GithubSetupService).not_to receive(:new)
subject.execute
end
end
context 'whem importer requires extra setup' do
it 'invokes the custom service class' do
expect(CiCd::GithubSetupService).to receive_message_chain(:new, :execute)
subject.execute
end
end
end
end
......@@ -23,7 +23,7 @@ describe Projects::CreateService, '#execute' do
end
it 'calls the service to setup CI/CD on the project' do
expect(Projects::SetupCiCd).to receive_message_chain(:new, :execute)
expect(CiCd::SetupProject).to receive_message_chain(:new, :execute)
create_project(user, opts)
end
......@@ -35,7 +35,7 @@ describe Projects::CreateService, '#execute' do
end
it "doesn't call the service to setup CI/CD on the project" do
expect(Projects::SetupCiCd).not_to receive(:new)
expect(CiCd::SetupProject).not_to receive(:new)
create_project(user, opts)
end
......
require 'spec_helper'
describe CreateGithubWebhookWorker do
include GrapeRouteHelpers::NamedRouteMatcher
let(:project) do
create(:project,
import_source: 'foo/bar',
import_type: 'github',
import_data_attributes: { credentials: { user: 'gh_token' } })
end
subject do
described_class.new
end
describe '#perform' do
before do
project.ensure_external_webhook_token
project.save
end
it 'creates the webhook' do
expect_any_instance_of(Gitlab::LegacyGithubImport::Client).to receive(:create_hook)
.with(
'foo/bar',
'web',
{
url: "http://localhost#{api_v4_projects_mirror_pull_path(id: project.id)}",
content_type: 'json',
secret: project.external_webhook_token,
insecure_ssl: 1
},
{
events: ['push'],
active: true
}
)
subject.perform(project.id)
end
end
end
......@@ -82,7 +82,7 @@ module SharedProject
step 'I should see project "Shop" activity feed' do
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
step 'I should see project settings' do
......@@ -113,12 +113,12 @@ module SharedProject
step 'I should not see project "Archive"' do
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
step 'I should see project "Archive"' do
project = Project.find_by(name: "Archive")
expect(page).to have_content project.name_with_namespace
expect(page).to have_content project.full_name
end
# ----------------------------------------
......
......@@ -194,6 +194,7 @@ module API
mount ::API::Ldap
mount ::API::LdapGroupLinks
mount ::API::License
mount ::API::ProjectMirror
mount ::API::ProjectPushRule
## EE-specific API V4 endpoints END
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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