Commit 318aeffc authored by Filipa Lacerda's avatar Filipa Lacerda

[ci skip] Merge branch 'master' into...

[ci skip] Merge branch 'master' into 43770-change-clear-runners-cache-ujs-action-to-an-axios-request

* master: (68 commits)
  Upgrade Workhorse to 4.0.0
  naming things
  Update GitLab Pages to v0.7.0
  Minor fixes in API doc
  Use Project#full_name instead of name_with_namespace
  Fix tests not completely disabling Gitaly
  Move OperationService#UserRemoveBranch
  Move OperationService#UserCreateBranch
  Move CommitService#Languages to OPT_OUT
  Move RefService#CreateBranch to OPT_OUT
  Move RefService#DeleteBranch to OPT_OUT
  Move OperationService#UserRevert to OPT_OUT
  Move OperationService#UserAddTag to OPT_OUT
  Move CommitService::CommitPatch to OPT_OUT
  Change to Pacific Time Zone
  Merge branch 'pages-6-1-gitlab-10-5' into 'security-10-5'
  Merge branch 'sh-fix-otp-backup-invalidation-10-5' into 'security-10-5'
  Remove wrong assumption about Runners cache GC
  Add CommonMark markdown engine
  add nginx_status monitoring details
  ...
parents b9f9e6fa 67185097
......@@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.5.3 (2018-03-01)
### Security (1 change)
- Ensure that OTP backup codes are always invalidated.
## 10.5.2 (2018-02-25)
### Fixed (7 changes)
......@@ -219,6 +226,13 @@ entry.
- Adds empty state illustration for pending job.
## 10.4.5 (2018-03-01)
### Security (1 change)
- Ensure that OTP backup codes are always invalidated.
## 10.4.4 (2018-02-16)
### Security (1 change)
......@@ -443,6 +457,13 @@ entry.
- Use a background migration for issues.closed_at.
## 10.3.8 (2018-03-01)
### Security (1 change)
- Ensure that OTP backup codes are always invalidated.
## 10.3.7 (2018-02-05)
### Security (4 changes)
......
......@@ -126,6 +126,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'
......@@ -412,9 +413,7 @@ end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
# Explicitly lock grpc as we know 1.9 is bad
# 1.10 is still being tested. See gitlab-org/gitaly#1059
gem 'grpc', '~> 1.8.3'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
......
......@@ -131,6 +131,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)
......@@ -343,9 +345,9 @@ GEM
google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0)
googleauth (0.5.3)
googleauth (0.6.2)
faraday (~> 0.12)
jwt (~> 1.4)
jwt (>= 1.4, < 3.0)
logging (~> 2.0)
memoist (~> 0.12)
multi_json (~> 1.11)
......@@ -369,7 +371,7 @@ GEM
rake
grape_logging (1.7.0)
grape
grpc (1.8.3)
grpc (1.10.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7)
......@@ -503,7 +505,7 @@ GEM
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
multi_json (1.12.2)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
mustermann (1.0.0)
......@@ -646,7 +648,7 @@ GEM
pry (~> 0.10)
pry-rails (0.3.5)
pry (>= 0.9.10)
public_suffix (3.0.0)
public_suffix (3.0.2)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.8)
rack-accept (0.4.5)
......@@ -797,6 +799,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)
......@@ -858,10 +862,10 @@ GEM
sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
signet (0.7.3)
signet (0.8.1)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (~> 1.5)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simple_po_parser (1.1.2)
simplecov (0.14.1)
......@@ -1019,6 +1023,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)
......@@ -1073,7 +1078,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
grpc (~> 1.8.3)
grpc (~> 1.10.0)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
......
......@@ -71,7 +71,7 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature freeze on the 7th for the release on the 22nd
After 7th at 23:59 (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
After 7th at 23:59 (Pacific Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
......
......@@ -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>
......
......@@ -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>
......
......@@ -6,8 +6,14 @@
constructor(options) {
this.options = options || {};
this.options.cursorBlink = options.cursorBlink || true;
this.options.screenKeys = options.screenKeys || true;
if (!Object.prototype.hasOwnProperty.call(this.options, 'cursorBlink')) {
this.options.cursorBlink = true;
}
if (!Object.prototype.hasOwnProperty.call(this.options, 'screenKeys')) {
this.options.screenKeys = true;
}
this.container = document.querySelector(options.selector);
this.setSocketUrl();
......
......@@ -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"
......
......@@ -272,7 +272,7 @@
.divider {
height: 1px;
margin: 6px 0;
margin: #{$grid-size / 2} 0;
padding: 0;
background-color: $dropdown-divider-color;
......
......@@ -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!
......
......@@ -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"
......
......@@ -187,7 +187,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)
......
......@@ -70,9 +70,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
......
......@@ -117,7 +117,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
......
module DeploymentPlatform
def deployment_platform
# EE would override this and utilize the extra argument
def deployment_platform(environment: nil)
@deployment_platform ||=
find_cluster_platform_kubernetes ||
find_kubernetes_service_integration ||
......
......@@ -223,6 +223,10 @@ module Issuable
def to_ability_name
model_name.singular
end
def parent_class
::Project
end
end
def today?
......
......@@ -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
......@@ -225,7 +225,7 @@ class Environment < ActiveRecord::Base
end
def deployment_platform
project.deployment_platform
project.deployment_platform(environment: self)
end
private
......
......@@ -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) }
......@@ -389,4 +390,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
......@@ -375,15 +375,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
......@@ -646,7 +658,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?
......
......@@ -81,7 +81,7 @@ class Note < ActiveRecord::Base
validates :author, presence: true
validates :discussion_id, presence: true, format: { with: /\A\h{40}\z/ }
validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
validate unless: [:for_commit?, :importing?, :skip_project_check?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:project, 'does not match noteable project')
end
......@@ -228,7 +228,7 @@ class Note < ActiveRecord::Base
end
def skip_project_check?
for_personal_snippet?
!for_project_noteable?
end
def commit
......@@ -308,6 +308,11 @@ class Note < ActiveRecord::Base
self.noteable.supports_discussions? && !part_of_discussion?
end
def can_create_todo?
# Skip system notes, and notes on project snippet
!system? && !for_snippet?
end
def discussion_class(noteable = nil)
# When commit notes are rendered on an MR's Discussion page, they are
# displayed in one discussion instead of individually.
......
......@@ -651,14 +651,15 @@ class Repository
end
def last_commit_for_path(sha, path)
commit_by(oid: last_commit_id_for_path(sha, path))
commit = raw_repository.last_commit_for_path(sha, path)
::Commit.new(commit, @project) if commit
end
def last_commit_id_for_path(sha, path)
key = path.blank? ? "last_commit_id_for_path:#{sha}" : "last_commit_id_for_path:#{sha}:#{Digest::SHA1.hexdigest(path)}"
cache.fetch(key) do
raw_repository.last_commit_id_for_path(sha, path)
last_commit_for_path(sha, path)&.id
end
end
......
......@@ -168,5 +168,9 @@ class Snippet < ActiveRecord::Base
def search_code(query)
fuzzy_search(query, [:content])
end
def parent_class
::Project
end
end
end
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
......@@ -38,7 +38,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
......
......@@ -26,14 +26,19 @@ module Notes
if project
project.notes.find_discussion(discussion_id)
else
# only PersonalSnippets can have discussions without project association
discussion = Note.find_discussion(discussion_id)
noteable = discussion.noteable
return nil unless noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable)
return nil unless noteable_without_project?(noteable)
discussion
end
end
def noteable_without_project?(noteable)
return true if noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable)
false
end
end
end
......@@ -11,7 +11,7 @@ module Notes
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
return if @note.for_personal_snippet?
return unless @note.for_project_noteable?
@note.create_cross_references!
execute_note_hooks
......
......@@ -280,7 +280,7 @@ module NotificationRecipientService
add_participants(note.author)
add_mentions(note.author, target: note)
unless note.for_personal_snippet?
if note.for_project_noteable?
# Merge project watchers
add_project_watchers
......
......@@ -85,7 +85,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
......
......@@ -241,8 +241,7 @@ class TodoService
end
def handle_note(note, author, skip_users = [])
# Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet?
return unless note.can_create_todo?
project = note.project
target = note.noteable
......
......@@ -657,9 +657,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'
......@@ -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"
......
......@@ -48,8 +48,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.
......@@ -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: Set margins around dropdown dividers to 4px
merge_request: 17517
author:
type: fixed
---
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: Upgrade GitLab Workhorse to 4.0.0
merge_request:
author:
type: added
---
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)
......
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 AddGroupIdToBoards < ActiveRecord::Migration
class AddGroupIdToBoardsCe < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
......
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
......@@ -1843,6 +1843,13 @@ ActiveRecord::Schema.define(version: 20180307012445) 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
......@@ -2115,6 +2122,8 @@ ActiveRecord::Schema.define(version: 20180307012445) 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
......@@ -37,6 +37,7 @@ following locations:
- [Group milestones](group_milestones.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
- [Discussions](discussions.md) (threaded comments)
- [Notification settings](notification_settings.md)
- [Open source license templates](templates/licenses.md)
- [Pages Domains](pages_domains.md)
......
This diff is collapsed.
This diff is collapsed.
......@@ -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
......
......@@ -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
# ----------------------------------------
......
......@@ -136,6 +136,7 @@ module API
mount ::API::MergeRequests
mount ::API::Namespaces
mount ::API::Notes
mount ::API::Discussions
mount ::API::NotificationSettings
mount ::API::PagesDomains
mount ::API::Pipelines
......
module API
class Discussions < Grape::API
include PaginationParams
helpers ::API::Helpers::NotesHelpers
before { authenticate! }
NOTEABLE_TYPES = [Issue, Snippet].freeze
NOTEABLE_TYPES.each do |noteable_type|
parent_type = noteable_type.parent_class.to_s.underscore
noteables_str = noteable_type.to_s.underscore.pluralize
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc "Get a list of #{noteable_type.to_s.downcase} discussions" do
success Entities::Discussion
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
return not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
notes = noteable.notes
.inc_relations_for_view
.includes(:noteable)
.fresh
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
discussions = Kaminari.paginate_array(Discussion.build_collection(notes, noteable))
present paginate(discussions), with: Entities::Discussion
end
desc "Get a single #{noteable_type.to_s.downcase} discussion" do
success Entities::Discussion
end
params do
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
return not_found!("Discussion")
end
discussion = Discussion.build(notes, noteable)
present discussion, with: Entities::Discussion
end
desc "Create a new #{noteable_type.to_s.downcase} discussion" do
success Entities::Discussion
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :body, type: String, desc: 'The content of a note'
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
opts = {
note: params[:body],
created_at: params[:created_at],
type: 'DiscussionNote',
noteable_type: noteables_str.classify,
noteable_id: noteable.id
}
note = create_note(noteable, opts)
if note.valid?
present note.discussion, with: Entities::Discussion
else
bad_request!("Note #{note.errors.messages}")
end
end
desc "Get comments in a single #{noteable_type.to_s.downcase} discussion" do
success Entities::Discussion
end
params do
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
return not_found!("Notes")
end
present notes, with: Entities::Note
end
desc "Add a comment to a #{noteable_type.to_s.downcase} discussion" do
success Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :body, type: String, desc: 'The content of a note'
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
return not_found!("Discussion") if notes.empty?
return bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion?
opts = {
note: params[:body],
type: 'DiscussionNote',
in_reply_to_discussion_id: params[:discussion_id],
created_at: params[:created_at]
}
note = create_note(noteable, opts)
if note.valid?
present note, with: Entities::Note
else
bad_request!("Note #{note.errors.messages}")
end
end
desc "Get a comment in a #{noteable_type.to_s.downcase} discussion" do
success Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
end
get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
get_note(noteable, params[:note_id])
end
desc "Edit a comment in a #{noteable_type.to_s.downcase} discussion" do
success Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
requires :body, type: String, desc: 'The content of a note'
end
put ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
update_note(noteable, params[:note_id])
end
desc "Delete a comment in a #{noteable_type.to_s.downcase} discussion" do
success Entities::Note
end
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :discussion_id, type: String, desc: 'The ID of a discussion'
requires :note_id, type: Integer, desc: 'The ID of a note'
end
delete ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
delete_note(noteable, params[:note_id])
end
end
end
helpers do
def readable_discussion_notes(noteable, discussion_id)
notes = noteable.notes
.where(discussion_id: discussion_id)
.inc_relations_for_view
.includes(:noteable)
.fresh
notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
end
end
end
end
......@@ -644,6 +644,7 @@ module API
NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
expose :id
expose :type
expose :note, as: :body
expose :attachment_identifier, as: :attachment
expose :author, using: Entities::UserBasic
......@@ -655,6 +656,12 @@ module API
expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) }
end
class Discussion < Grape::Entity
expose :id
expose :individual_note?, as: :individual_note
expose :notes, using: Entities::Note
end
class AwardEmoji < Grape::Entity
expose :id
expose :name
......
......@@ -111,13 +111,6 @@ module API
def gitaly_payload(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,
address: Gitlab::GitalyClient.address(project.repository_storage),
......
module API
module Helpers
module NotesHelpers
def update_note(noteable, note_id)
note = noteable.notes.find(params[:note_id])
authorize! :admin_note, note
opts = {
note: params[:body]
}
parent = noteable_parent(noteable)
project = parent if parent.is_a?(Project)
note = ::Notes::UpdateService.new(project, current_user, opts).execute(note)
if note.valid?
present note, with: Entities::Note
else
bad_request!("Failed to save note #{note.errors.messages}")
end
end
def delete_note(noteable, note_id)
note = noteable.notes.find(note_id)
authorize! :admin_note, note
parent = noteable_parent(noteable)
project = parent if parent.is_a?(Project)
destroy_conditionally!(note) do |note|
::Notes::DestroyService.new(project, current_user).execute(note)
end
end
def get_note(noteable, note_id)
note = noteable.notes.with_metadata.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
if can_read_note
present note, with: Entities::Note
else
not_found!("Note")
end
end
def noteable_read_ability_name(noteable)
"read_#{noteable.class.to_s.underscore}".to_sym
end
def find_noteable(parent, noteables_str, noteable_id)
public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
end
def noteable_parent(noteable)
public_send("user_#{noteable.class.parent_class.to_s.underscore}") # rubocop:disable GitlabSecurity/PublicSend
end
def create_note(noteable, opts)
noteables_str = noteable.model_name.to_s.underscore.pluralize
return not_found!(noteables_str) unless can?(current_user, noteable_read_ability_name(noteable), noteable)
authorize! :create_note, noteable
parent = noteable_parent(noteable)
if opts[:created_at]
opts.delete(:created_at) unless current_user.admin? || parent.owner == current_user
end
project = parent if parent.is_a?(Project)
::Notes::CreateService.new(project, current_user, opts).execute
end
end
end
end
module API
class Notes < Grape::API
include PaginationParams
helpers ::API::Helpers::NotesHelpers
before { authenticate! }
NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
NOTEABLE_TYPES.each do |noteable_type|
NOTEABLE_TYPES.each do |noteable_type|
parent_type = noteable_type.parent_class.to_s.underscore
noteables_str = noteable_type.to_s.underscore.pluralize
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
noteables_str = noteable_type.to_s.underscore.pluralize
desc 'Get a list of project +noteable+ notes' do
desc "Get a list of #{noteable_type.to_s.downcase} notes" do
success Entities::Note
end
params do
......@@ -25,7 +29,7 @@ module API
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/notes" do
noteable = find_project_noteable(noteables_str, params[:noteable_id])
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
......@@ -46,7 +50,7 @@ module API
end
end
desc 'Get a single +noteable+ note' do
desc "Get a single #{noteable_type.to_s.downcase} note" do
success Entities::Note
end
params do
......@@ -54,18 +58,11 @@ module API
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
noteable = find_project_noteable(noteables_str, params[:noteable_id])
note = noteable.notes.with_metadata.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
if can_read_note
present note, with: Entities::Note
else
not_found!("Note")
end
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
get_note(noteable, params[:note_id])
end
desc 'Create a new +noteable+ note' do
desc "Create a new #{noteable_type.to_s.downcase} note" do
success Entities::Note
end
params do
......@@ -74,34 +71,25 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
noteable = find_project_noteable(noteables_str, params[:noteable_id])
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
noteable_id: noteable.id
noteable_id: noteable.id,
created_at: params[:created_at]
}
if can?(current_user, noteable_read_ability_name(noteable), noteable)
authorize! :create_note, noteable
note = create_note(noteable, opts)
if params[:created_at] && (current_user.admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if note.valid?
present note, with: Entities.const_get(note.class.name)
else
not_found!("Note #{note.errors.messages}")
end
if note.valid?
present note, with: Entities.const_get(note.class.name)
else
not_found!("Note")
bad_request!("Note #{note.errors.messages}")
end
end
desc 'Update an existing +noteable+ note' do
desc "Update an existing #{noteable_type.to_s.downcase} note" do
success Entities::Note
end
params do
......@@ -110,24 +98,12 @@ module API
requires :body, type: String, desc: 'The content of a note'
end
put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
authorize! :admin_note, note
opts = {
note: params[:body]
}
note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
if note.valid?
present note, with: Entities::Note
else
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
update_note(noteable, params[:note_id])
end
desc 'Delete a +noteable+ note' do
desc "Delete a #{noteable_type.to_s.downcase} note" do
success Entities::Note
end
params do
......@@ -135,25 +111,11 @@ module API
requires :note_id, type: Integer, desc: 'The ID of a note'
end
delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
destroy_conditionally!(note) do |note|
::Notes::DestroyService.new(user_project, current_user).execute(note)
end
delete_note(noteable, params[:note_id])
end
end
end
helpers do
def find_project_noteable(noteables_str, noteable_id)
public_send("find_project_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
end
def noteable_read_ability_name(noteable)
"read_#{noteable.class.to_s.underscore}".to_sym
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 Filter
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)
super text, context, result
@text = @text.delete "\r"
super(text, context, result)
@renderer = renderer(context[:markdown_engine]).new
@text = @text.delete("\r")
end
def call
html = self.class.renderer.render(@text)
html.rstrip!
html
@renderer.render(@text).rstrip
end
private
DEFAULT_ENGINE = :redcarpet
def engine(engine_from_context)
engine_from_context ||= DEFAULT_ENGINE
engine_from_context.to_s.classify
end
def self.renderer
Thread.current[:banzai_markdown_renderer] ||= begin
renderer = Banzai::Renderer::HTML.new
Redcarpet::Markdown.new(renderer, REDCARPET_OPTIONS)
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
......
require 'rouge/plugins/common_mark'
require 'rouge/plugins/redcarpet'
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
if label.valid?
@labels[label_params[:title]] = label
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
......
......@@ -23,7 +23,7 @@ module Gitlab
mr_events = event_counts(date_from, :merge_requests)
.having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest")
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])
events = Event.find_by_sql(union.to_sql).map(&:attributes)
......
......@@ -31,7 +31,7 @@ module Gitlab
# TODO: do we still need it?
project_id: project.id,
project_name: project.name_with_namespace,
project_name: project.full_name,
user: {
id: user.try(:id),
......
......@@ -347,7 +347,7 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324
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
@repository.gitaly_commit_client.patch(id)
else
......
......@@ -228,7 +228,7 @@ module Gitlab
end
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
gitaly_repository_client.has_local_branches?
else
......@@ -715,7 +715,7 @@ module Gitlab
end
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
gitaly_add_branch(branch_name, user, target)
else
......@@ -725,7 +725,7 @@ module Gitlab
end
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
gitaly_add_tag(tag_name, user: user, target: target, message: message)
else
......@@ -735,7 +735,7 @@ module Gitlab
end
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
gitaly_operations_client.user_delete_branch(branch_name, user)
else
......@@ -810,7 +810,7 @@ module Gitlab
end
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 = {
user: user,
commit: commit,
......@@ -876,7 +876,7 @@ module Gitlab
# Delete the specified branch from the repository
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
gitaly_ref_client.delete_branch(branch_name)
else
......@@ -903,7 +903,7 @@ module Gitlab
# create_branch("feature")
# create_branch("other-feature", "master")
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
gitaly_ref_client.create_branch(ref, start_point)
else
......@@ -1010,7 +1010,7 @@ module Gitlab
end
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
gitaly_commit_client.languages(ref)
else
......@@ -1443,12 +1443,12 @@ module Gitlab
end
end
def last_commit_id_for_path(sha, path)
def last_commit_for_path(sha, path)
gitaly_migrate(:last_commit_for_path) do |is_enabled|
if is_enabled
last_commit_for_path_by_gitaly(sha, path).id
last_commit_for_path_by_gitaly(sha, path)
else
last_commit_id_for_path_by_shelling_out(sha, path)
last_commit_for_path_by_rugged(sha, path)
end
end
end
......@@ -1896,7 +1896,7 @@ module Gitlab
end
def last_commit_for_path_by_rugged(sha, path)
sha = last_commit_id_for_path(sha, path)
sha = last_commit_id_for_path_by_shelling_out(sha, path)
commit(sha)
end
......
......@@ -38,7 +38,7 @@ module Gitlab
end
def project_link
"[#{project.name_with_namespace}](#{project.web_url})"
"[#{project.full_name}](#{project.web_url})"
end
def author_profile_link
......
......@@ -53,7 +53,7 @@ module Gitlab
end
def pretext
"Issue *#{@resource.to_reference}* from #{project.name_with_namespace}"
"Issue *#{@resource.to_reference}* from #{project.full_name}"
end
end
end
......
......@@ -10,6 +10,7 @@ module Gitlab
INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'.freeze
INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'.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
# bytes https://tools.ietf.org/html/rfc4868#section-2.6
......@@ -17,6 +18,8 @@ module Gitlab
class << self
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
repo_path = repository.path_to_repo
params = {
......@@ -31,24 +34,7 @@ module Gitlab
token: Gitlab::GitalyClient.token(project.repository_storage)
}
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
end
params[:GitalyServer] = server
params
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
if should_sanitize?
"#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
else
"#{project.name_with_namespace.color(:yellow)} ... "
"#{project.full_name.color(:yellow)} ... "
end
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.
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