Commit 3465e1e5 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into issue-edit-inline

parents 5227ea5b f47e86fe
---
engines:
brakeman:
enabled: true
bundler-audit:
enabled: true
duplication:
enabled: true
config:
languages:
- ruby
- javascript
eslint:
enabled: true
fixme:
enabled: true
rubocop:
enabled: true
ratings:
paths:
- Gemfile.lock
- "**.erb"
- "**.haml"
- "**.rb"
- "**.rhtml"
- "**.slim"
- "**.inc"
- "**.js"
- "**.jsx"
- "**.module"
exclude_paths:
- config/
- db/
- features/
- node_modules/
- spec/
- vendor/
- lib/api/v3/
......@@ -150,6 +150,7 @@ stages:
# Trigger a package build on omnibus-gitlab repository
build-package:
image: ruby:2.3-alpine
before_script: []
services: []
variables:
......@@ -486,25 +487,6 @@ lint:javascript:report:
paths:
- eslint-report.html
# Trigger docs build
# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process
trigger_docs:
stage: post-test
image: "alpine"
<<: *dedicated-runner
before_script:
- apk update && apk add curl
variables:
GIT_STRATEGY: "none"
cache: {}
artifacts: {}
script:
- "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
- if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
only:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
pages:
before_script: []
stage: pages
......
......@@ -57,7 +57,7 @@ linters:
# Reports when you define the same property twice in a single rule set.
DuplicateProperty:
enabled: false
enabled: true
# Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks:
......@@ -75,7 +75,7 @@ linters:
# when adding lines to the file, since SCM systems such as git won't
# think that you touched the last line.
FinalNewline:
enabled: false
enabled: true
# HEX colors should use three-character values where possible.
HexLength:
......
......@@ -341,7 +341,7 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
grpc (1.2.5)
grpc (1.3.4)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
......
This diff is collapsed.
......@@ -123,7 +123,7 @@ import ShortcutsBlob from './shortcuts_blob';
break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
if (gl.FilteredSearchManager) {
if (gl.FilteredSearchManager && document.querySelector('.filtered-search')) {
new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
}
Issuable.init();
......
......@@ -468,8 +468,8 @@ GitLabDropdown = (function() {
// Process the data to make sure rendered data
// matches the correct layout
if (this.fullData && hasMultiSelect && this.options.processData) {
const inputValue = this.filterInput.val();
if (this.fullData && hasMultiSelect && this.options.processData && inputValue.length === 0) {
this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this));
}
......@@ -740,6 +740,12 @@ GitLabDropdown = (function() {
$input.attr('id', this.options.inputId);
}
if (this.options.multiSelect) {
Object.keys(selectedObject).forEach((attribute) => {
$input.attr(`data-${attribute}`, selectedObject[attribute]);
});
}
if (this.options.inputMeta) {
$input.attr('data-meta', selectedObject[this.options.inputMeta]);
}
......
......@@ -42,3 +42,13 @@ export function formatRelevantDigits(number) {
export function bytesToKiB(number) {
return number / BYTES_IN_KIB;
}
/**
* Utility function that calculates MiB of the given bytes.
*
* @param {Number} number bytes
* @return {Number} MiB
*/
export function bytesToMiB(number) {
return number / (BYTES_IN_KIB * BYTES_IN_KIB);
}
......@@ -285,7 +285,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash();
const anchor = hash && $container.find(`[id="${hash}"]`);
if (anchor) {
if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
notes.toggleDiffNote({
......
This diff is collapsed.
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../../../lib/utils/poll';
import PipelineService from '../../services/pipeline_service';
import PipelineStore from '../../stores/pipeline_store';
import stageColumnComponent from './stage_column_component.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../flash';
export default {
props: {
isLoading: {
type: Boolean,
required: true,
},
pipeline: {
type: Object,
required: true,
},
},
components: {
stageColumnComponent,
loadingIcon,
},
data() {
const DOMdata = document.getElementById('js-pipeline-graph-vue').dataset;
const store = new PipelineStore();
return {
isLoading: false,
endpoint: DOMdata.endpoint,
store,
state: store.state,
};
computed: {
graph() {
return this.pipeline.details && this.pipeline.details.stages;
},
created() {
this.service = new PipelineService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'getPipeline',
successCallback: this.successCallback,
errorCallback: this.errorCallback,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
},
methods: {
successCallback(response) {
const data = response.json();
this.isLoading = false;
this.store.storeGraph(data.details.stages);
},
errorCallback() {
this.isLoading = false;
return new Flash('An error occurred while fetching the pipeline.');
},
capitalizeStageName(name) {
return name.charAt(0).toUpperCase() + name.slice(1);
},
......@@ -101,7 +65,7 @@
v-if="!isLoading"
class="stage-column-list">
<stage-column-component
v-for="(stage, index) in state.graph"
v-for="(stage, index) in graph"
:title="capitalizeStageName(stage.name)"
:jobs="stage.groups"
:key="stage.name"
......
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
props: [
'pipeline',
],
computed: {
user() {
return !!this.pipeline.user;
},
},
components: {
userAvatarLink,
},
template: `
<td>
<a
:href="pipeline.path"
class="js-pipeline-url-link">
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<user-avatar-link
v-if="user"
class="js-pipeline-url-user"
:link-href="pipeline.user.web_url"
:img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name"
/>
<span
v-if="!user"
class="js-pipeline-url-api api">
API
</span>
<span
v-if="pipeline.flags.latest"
class="js-pipeline-url-lastest label label-success has-tooltip"
title="Latest pipeline for this branch"
data-original-title="Latest pipeline for this branch">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
class="js-pipeline-url-yaml label label-danger has-tooltip"
:title="pipeline.yaml_errors"
:data-original-title="pipeline.yaml_errors">
yaml invalid
</span>
<span
v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck label label-warning">
stuck
</span>
</td>
`,
};
<script>
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
export default {
props: {
pipeline: {
type: Object,
required: true,
},
},
components: {
userAvatarLink,
},
mixins: [
tooltipMixin,
],
computed: {
user() {
return this.pipeline.user;
},
},
};
</script>
<template>
<td>
<a
:href="pipeline.path"
class="js-pipeline-url-link">
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<user-avatar-link
v-if="user"
class="js-pipeline-url-user"
:link-href="pipeline.user.web_url"
:img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name"
/>
<span
v-if="!user"
class="js-pipeline-url-api api">
API
</span>
<span
v-if="pipeline.flags.latest"
class="js-pipeline-url-lastest label label-success"
title="Latest pipeline for this branch"
ref="tooltip">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
class="js-pipeline-url-yaml label label-danger"
:title="pipeline.yaml_errors"
ref="tooltip">
yaml invalid
</span>
<span
v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck label label-warning">
stuck
</span>
</td>
</template>
import Vue from 'vue';
import pipelineGraph from './components/graph/graph_component.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-pipeline-graph-vue',
components: {
pipelineGraph,
},
render: createElement => createElement('pipeline-graph'),
}));
import Vue from 'vue';
import PipelinesMediator from './pipeline_details_mediatior';
import pipelineGraph from './components/graph/graph_component.vue';
document.addEventListener('DOMContentLoaded', () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
mediator.fetchPipeline();
const pipelineGraphApp = new Vue({
el: '#js-pipeline-graph-vue',
data() {
return {
mediator,
};
},
components: {
pipelineGraph,
},
render(createElement) {
return createElement('pipeline-graph', {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
},
});
},
});
return pipelineGraphApp;
});
/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../lib/utils/poll';
import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
export default class pipelinesMediator {
constructor(options = {}) {
this.options = options;
this.store = new PipelineStore();
this.service = new PipelineService(options.endpoint);
this.state = {};
this.state.isLoading = false;
}
fetchPipeline() {
this.poll = new Poll({
resource: this.service,
method: 'getPipeline',
successCallback: this.successCallback.bind(this),
errorCallback: this.errorCallback.bind(this),
});
if (!Visibility.hidden()) {
this.state.isLoading = true;
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
}
successCallback(response) {
const data = response.json();
this.state.isLoading = false;
this.store.storePipeline(data);
}
errorCallback() {
this.state.isLoading = false;
return new Flash('An error occurred while fetching the pipeline.');
}
}
......@@ -2,10 +2,10 @@ export default class PipelineStore {
constructor() {
this.state = {};
this.state.graph = [];
this.state.pipeline = {};
}
storeGraph(graph = []) {
this.state.graph = graph;
storePipeline(pipeline = {}) {
this.state.pipeline = pipeline;
}
}
......@@ -4,7 +4,7 @@
window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
WRAPPER = '<div class="diff-content diff-wrap-lines"></div>';
WRAPPER = '<div class="diff-content"></div>';
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
......
......@@ -35,6 +35,7 @@ function UsersSelect(currentUser, els) {
options.showCurrentUser = $dropdown.data('current-user');
options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter');
options.perPage = $dropdown.data('per-page');
showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove');
......@@ -214,7 +215,36 @@ function UsersSelect(currentUser, els) {
glDropdown.options.processData(term, users, callback);
}.bind(this));
},
processData: function(term, users, callback) {
processData: function(term, data, callback) {
let users = data;
// Only show assigned user list when there is no search term
if ($dropdown.hasClass('js-multiselect') && term.length === 0) {
const selectedInputs = getSelectedUserInputs();
// Potential duplicate entries when dealing with issue board
// because issue board is also managed by vue
const selectedUsers = _.uniq(selectedInputs, false, a => a.value)
.filter((input) => {
const userId = parseInt(input.value, 10);
const inUsersArray = users.find(u => u.id === userId);
return !inUsersArray && userId !== 0;
})
.map((input) => {
const userId = parseInt(input.value, 10);
const { avatarUrl, avatar_url, name, username } = input.dataset;
return {
avatar_url: avatarUrl || avatar_url,
id: userId,
name,
username,
};
});
users = data.concat(selectedUsers);
}
let anyUser;
let index;
let j;
......@@ -645,7 +675,7 @@ UsersSelect.prototype.users = function(query, options, callback) {
url: url,
data: {
search: query,
per_page: 20,
per_page: options.perPage || 20,
active: true,
project_id: options.projectId || null,
group_id: options.groupId || null,
......
import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import MemoryGraph from '../../vue_shared/components/memory_graph';
import MRWidgetService from '../services/mr_widget_service';
......@@ -9,8 +11,8 @@ export default {
},
data() {
return {
// memoryFrom: 0,
// memoryTo: 0,
memoryFrom: 0,
memoryTo: 0,
memoryMetrics: [],
deploymentTime: 0,
hasMetrics: false,
......@@ -35,18 +37,38 @@ export default {
shouldShowMetricsUnavailable() {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
},
memoryChangeType() {
const memoryTo = Number(this.memoryTo);
const memoryFrom = Number(this.memoryFrom);
if (memoryTo > memoryFrom) {
return 'increased';
} else if (memoryTo < memoryFrom) {
return 'decreased';
}
return 'unchanged';
},
},
methods: {
getMegabytes(bytesString) {
const valueInBytes = Number(bytesString).toFixed(2);
return (bytesToMiB(valueInBytes)).toFixed(2);
},
computeGraphData(metrics, deploymentTime) {
this.loadingMetrics = false;
const { memory_values } = metrics;
// if (memory_previous.length > 0) {
// this.memoryFrom = Number(memory_previous[0].value[1]).toFixed(2);
// }
//
// if (memory_current.length > 0) {
// this.memoryTo = Number(memory_current[0].value[1]).toFixed(2);
// }
const { memory_before, memory_after, memory_values } = metrics;
// Both `memory_before` and `memory_after` objects
// have peculiar structure where accessing only a specific
// index yeilds correct value that we can use to show memory delta.
if (memory_before.length > 0) {
this.memoryFrom = this.getMegabytes(memory_before[0].value[1]);
}
if (memory_after.length > 0) {
this.memoryTo = this.getMegabytes(memory_after[0].value[1]);
}
if (memory_values.length > 0) {
this.hasMetrics = true;
......@@ -102,7 +124,7 @@ export default {
<p
v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info">
Deployment memory usage:
Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
</p>
<p
v-if="shouldShowLoadFailure"
......
......@@ -13,7 +13,7 @@ export default {
},
data() {
return {
removeSourceBranch: true,
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false,
......@@ -69,6 +69,9 @@ export default {
|| this.isMakingRequest
|| this.mr.preventMerge);
},
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch;
},
shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1;
......@@ -252,8 +255,9 @@ export default {
<template v-if="isMergeAllowed()">
<label class="spacing">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isMergeButtonDisabled"
:disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
......
......@@ -50,7 +50,7 @@ export default class MergeRequestStore {
this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = (data.merge_params || {}).should_remove_source_branch || false;
this.shouldRemoveSourceBranch = data.remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path;
......
<script>
import ciIconBadge from './ci_badge_link.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
*
* Used in:
* - job show page
* - pipeline show page
*/
export default {
props: {
status: {
type: Object,
required: true,
},
itemName: {
type: String,
required: true,
},
itemId: {
type: Number,
required: true,
},
time: {
type: String,
required: true,
},
user: {
type: Object,
required: true,
},
actions: {
type: Array,
required: false,
default: () => [],
},
},
mixins: [
tooltipMixin,
],
components: {
ciIconBadge,
timeagoTooltip,
userAvatarLink,
},
computed: {
userAvatarAltText() {
return `${this.user.name}'s avatar`;
},
},
methods: {
onClickAction(action) {
this.$emit('postAction', action);
},
},
};
</script>
<template>
<header class="page-content-header top-area">
<section class="header-main-content">
<ci-icon-badge :status="status" />
<strong>
{{itemName}} #{{itemId}}
</strong>
triggered
<timeago-tooltip :time="time" />
by
<user-avatar-link
:link-href="user.web_url"
:img-src="user.avatar_url"
:img-alt="userAvatarAltText"
:tooltip-text="user.name"
:img-size="24"
/>
<a
:href="user.web_url"
:title="user.email"
class="js-user-link commit-committer-link"
ref="tooltip">
{{user.name}}
</a>
</section>
<section
class="header-action-button nav-controls"
v-if="actions.length">
<template
v-for="action in actions">
<a
v-if="action.type === 'link'"
:href="action.path"
:class="action.cssClass">
{{action.label}}
</a>
<button
v-else="action.type === 'button'"
@click="onClickAction(action)"
:class="action.cssClass"
type="button">
{{action.label}}
</button>
</template>
</section>
</header>
</template>
......@@ -4,7 +4,7 @@ import PipelinesActionsComponent from '../../pipelines/components/pipelines_acti
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url.vue';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit';
......
<script>
import tooltipMixin from '../mixins/tooltip';
import timeagoMixin from '../mixins/timeago';
import '../../lib/utils/datetime_utility';
/**
* Port of ruby helper time_ago_with_tooltip
*/
export default {
props: {
time: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
required: false,
default: 'top',
},
shortFormat: {
type: Boolean,
required: false,
default: false,
},
cssClass: {
type: String,
required: false,
default: '',
},
},
mixins: [
tooltipMixin,
timeagoMixin,
],
computed: {
timeagoCssClass() {
return this.shortFormat ? 'js-short-timeago' : 'js-timeago';
},
},
};
</script>
<template>
<time
:class="[timeagoCssClass, cssClass]"
class="js-timeago js-timeago-render"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
data-container="body"
ref="tooltip">
{{timeFormated(time)}}
</time>
</template>
import '../../lib/utils/datetime_utility';
/**
* Mixin with time ago methods used in some vue components
*/
export default {
methods: {
timeFormated(time) {
const timeago = gl.utils.getTimeago();
return timeago.format(time);
},
tooltipTitle(time) {
return gl.utils.formatDate(time);
},
},
};
......@@ -34,6 +34,7 @@
@import "framework/selects.scss";
@import "framework/sidebar.scss";
@import "framework/tables.scss";
@import "framework/notes.scss";
@import "framework/timeline.scss";
@import "framework/typography.scss";
@import "framework/zen.scss";
......
......@@ -10,7 +10,7 @@
top: 0;
margin-top: 3px;
padding: $gl-padding;
z-index: 9;
z-index: 300;
width: 300px;
font-size: 14px;
background-color: $white-light;
......
......@@ -23,7 +23,6 @@
.row-content-block {
margin-top: 0;
margin-bottom: -$gl-padding;
background-color: $gray-light;
padding: $gl-padding;
margin-bottom: 0;
......
gl-emoji {
display: inline-block;
display: inline-flex;
vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
......
......@@ -66,10 +66,10 @@
&.video {
background: $file-image-bg;
text-align: center;
padding: 30px;
img,
video {
padding: 20px;
max-width: 80%;
}
}
......
......@@ -36,6 +36,10 @@
border-radius: 0;
}
}
&:empty {
margin: 0;
}
}
@media (max-width: $screen-sm-max) {
......
......@@ -11,7 +11,6 @@
> li {
padding: 10px 15px;
min-height: 20px;
border-bottom: 1px solid $list-border-light;
border-bottom: 1px solid $list-border;
&::after {
......
@mixin notes-media($condition, $breakpoint-width) {
@media (#{$condition}-width: ($breakpoint-width)) {
@content;
}
// Diff is side by side
.notes_content.parallel & {
// We hide at double what we normally hide at because
// there are two columns of notes
@media (#{$condition}-width: (2 * $breakpoint-width)) {
@content;
}
}
}
......@@ -96,7 +96,6 @@
.select2-search-field input {
padding: 5px $gl-padding / 2;
font-size: 13px;
height: auto;
font-family: inherit;
font-size: inherit;
......
......@@ -3,9 +3,9 @@
margin: 0;
padding: 0;
.note-text {
p:last-child {
margin-bottom: 0 !important;
&::before {
@include notes-media('max', $screen-xs-max) {
background: none;
}
}
......@@ -29,6 +29,16 @@
.timeline-entry-inner {
position: relative;
@include notes-media('max', $screen-xs-max) {
.timeline-icon {
display: none;
}
.timeline-content {
margin-left: 0;
}
}
}
&:target,
......@@ -46,24 +56,6 @@
}
}
@media (max-width: $screen-xs-max) {
.timeline {
&::before {
background: none;
}
}
.timeline-entry .timeline-entry-inner {
.timeline-icon {
display: none;
}
.timeline-content {
margin-left: 0;
}
}
}
.discussion .timeline-entry {
margin: 0;
border-right: none;
......
......@@ -21,6 +21,10 @@
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
// Single code lines should wrap
code {
font-family: $monospace_font;
......@@ -157,7 +161,7 @@
ul,
ol {
padding: 0;
margin: 0 0 16px !important;
margin: 0 0 16px;
}
ul:dir(rtl),
......
......@@ -247,7 +247,6 @@ $dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1);
$file-mode-changed: #777;
$file-mode-changed: #777;
$diff-image-bg: #ddd;
$diff-image-info-color: grey;
$diff-swipe-border: #999;
$diff-view-modes-color: grey;
......
......@@ -72,7 +72,9 @@
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
// scss-lint:disable DuplicateProperty
height: calc(100vh - 222px);
// scss-lint:enable DuplicateProperty
min-height: 475px;
transition: width .2s;
......
......@@ -29,65 +29,53 @@
}
}
.build-page {
pre.trace {
background: $builds-trace-bg;
color: $white-light;
font-family: $monospace_font;
white-space: pre-wrap;
overflow: auto;
overflow-y: hidden;
font-size: 12px;
.fa-spinner {
font-size: 24px;
margin-left: 20px;
}
}
.environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
border-radius: $border-radius-default;
@keyframes blinking-scroll-button {
0% { opacity: 0.2; }
25% { opacity: 0.5; }
50% { opacity: 0.7; }
100% { opacity: 1; }
}
svg {
position: relative;
top: 1px;
margin-right: 5px;
}
.build-page {
.sticky {
position: absolute;
left: 0;
right: 0;
}
.truncated-info {
text-align: center;
border-bottom: 1px solid;
background-color: $black;
height: 45px;
padding: 15px;
.build-trace-container {
position: absolute;
top: 225px;
left: 15px;
bottom: 10px;
background: $black;
color: $gray-darkest;
font-family: $monospace_font;
font-size: 12px;
&.affix {
top: 0;
&.sidebar-expanded {
right: 305px;
}
// with sidebar
&.affix.sidebar-expanded {
right: 312px;
left: 22px;
&.sidebar-collapsed {
right: 16px;
}
// without sidebar
&.affix.sidebar-collapsed {
right: 20px;
left: 20px;
code {
background: $black;
color: $gray-darkest;
}
&.affix-top {
position: absolute;
.top-bar {
top: 0;
height: 35px;
display: flex;
justify-content: flex-end;
border-bottom: 1px outset $white-light;
.truncated-info {
margin: 0 auto;
right: 5px;
left: 5px;
}
align-self: center;
.truncated-info-size {
margin: 0 5px;
......@@ -99,59 +87,82 @@
text-decoration: underline;
}
}
}
}
.scroll-controls {
height: 100%;
.controllers {
display: flex;
align-self: center;
font-size: 15px;
.scroll-step {
width: 31px;
margin: 0 0 0 auto;
svg {
height: 15px;
display: block;
fill: $white-light;
}
.scroll-link,
.autoscroll-container {
right: 25px;
z-index: 1;
a,
.btn-scroll {
margin: 0 8px;
color: $white-light;
}
.scroll-link {
position: fixed;
display: block;
margin-bottom: 10px;
.btn-scroll.animate {
.first-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .3s;
}
&.scroll-top .gitlab-icon-scroll-up-hover,
&.scroll-top:hover .gitlab-icon-scroll-up,
&.scroll-bottom .gitlab-icon-scroll-down-hover,
&.scroll-bottom:hover .gitlab-icon-scroll-down {
display: none;
.second-triangle {
animation: blinking-scroll-button 1s ease infinite;
animation-delay: .2s;
}
&.scroll-top:hover .gitlab-icon-scroll-up-hover,
&.scroll-bottom:hover .gitlab-icon-scroll-down-hover {
display: inline-block;
.third-triangle {
animation: blinking-scroll-button 1s ease infinite;
}
&.scroll-top {
top: 10px;
&:disabled {
opacity: 1;
}
}
&.scroll-bottom {
bottom: -2px;
.btn-scroll:disabled {
opacity: 0.35;
cursor: not-allowed;
}
}
}
.autoscroll-container {
position: absolute;
.bash {
top: 35px;
left: 10px;
bottom: 0;
overflow-y: hidden;
padding-bottom: 20px;
padding-right: 20px;
}
&.sidebar-expanded {
.environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
border-radius: $border-radius-default;
.scroll-link,
.autoscroll-container {
right: ($gutter_width + ($gl-padding * 2));
svg {
position: relative;
top: 1px;
margin-right: 5px;
}
}
.build-loader-animation {
position: relative;
width: 6px;
height: 6px;
margin: auto auto 12px 2px;
border-radius: 50%;
animation: blinking-dots 1s linear infinite;
}
}
.status-message {
......@@ -223,32 +234,6 @@
}
}
.build-trace {
background: $black;
color: $gray-darkest;
white-space: pre;
overflow-x: auto;
font-size: 12px;
position: relative;
.fa-spinner {
font-size: 24px;
}
.bash {
display: block;
}
.build-loader-animation {
position: relative;
width: 6px;
height: 6px;
margin: auto auto 12px 2px;
border-radius: 50%;
animation: blinking-dots 1s linear infinite;
}
}
.right-sidebar.build-sidebar {
padding: $gl-padding 0;
......
......@@ -36,7 +36,6 @@
pre.commit-message {
background: none;
padding: 0;
margin: 0;
border: none;
margin: 20px 0;
border-radius: 0;
......
......@@ -94,7 +94,6 @@
.old_line,
.new_line {
margin: 0;
padding: 0;
border: none;
padding: 0 5px;
border-right: 1px solid;
......@@ -151,14 +150,10 @@
}
}
}
.text-file.diff-wrap-lines table .line_holder td span {
white-space: pre-wrap;
}
}
.image {
background: $diff-image-bg;
background: $file-image-bg;
text-align: center;
padding: 30px;
......
......@@ -64,6 +64,10 @@
}
}
.btn .text-center {
display: inline;
}
.commit-title {
margin: 0;
}
......
......@@ -431,7 +431,7 @@
}
.detail-page-description {
padding: 16px 0 0;
padding: 16px 0;
small {
color: $gray-darkest;
......@@ -441,7 +441,7 @@
.edited-text {
color: $gray-darkest;
display: block;
margin: 0 0 16px;
margin: 16px 0 0;
.author_link {
color: $gray-darkest;
......
......@@ -204,7 +204,6 @@ ul.related-merge-requests > li {
.dropdown-toggle {
.fa-caret-down {
pointer-events: none;
margin-left: 0;
color: inherit;
margin-left: 0;
}
......
......@@ -28,7 +28,7 @@
.note-edit-form {
.note-form-actions {
position: relative;
margin: $gl-padding 0;
margin: $gl-padding 0 0;
}
.note-preview-holder {
......@@ -124,10 +124,18 @@
}
.discussion-form {
padding: $gl-padding-top $gl-padding;
padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light;
}
.discussion-notes .disabled-comment {
padding: 6px 0;
}
.notes-form > li {
border: 0;
}
.note-edit-form {
display: none;
font-size: 14px;
......
......@@ -14,24 +14,11 @@ ul.notes {
margin: 0;
padding: 0;
.timeline-icon {
float: left;
svg {
width: 16px;
height: 16px;
fill: $gray-darkest;
position: absolute;
left: 0;
top: 16px;
}
}
.timeline-content {
margin-left: 55px;
&.timeline-content-form {
@media (max-width: $screen-sm-max) {
@include notes-media('max', $screen-sm-max) {
margin-left: 0;
}
}
......@@ -56,21 +43,22 @@ ul.notes {
position: relative;
}
.note {
padding: $gl-padding $gl-btn-padding 0;
> li {
padding: $gl-padding $gl-btn-padding;
display: block;
position: relative;
border-bottom: 1px solid $white-normal;
&:last-child {
// Override `.timeline > li:last-child { border-bottom: none; }`
border-bottom: 1px solid $white-normal;
}
&.being-posted {
pointer-events: none;
opacity: 0.5;
.dummy-avatar {
display: inline-block;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: $kdb-border;
border: 1px solid darken($kdb-border, 25%);
}
......@@ -126,13 +114,13 @@ ul.notes {
.note-awards {
.js-awards-block {
margin-bottom: 16px;
margin-top: 16px;
}
}
.note-header {
@media (max-width: $screen-xs-min) {
@include notes-media('max', $screen-xs-min) {
.inline {
display: block;
}
......@@ -161,10 +149,10 @@ ul.notes {
.system-note {
font-size: 14px;
padding: 0;
padding-left: 0;
clear: both;
@media (min-width: $screen-sm-min) {
@include notes-media('min', $screen-sm-min) {
margin-left: 65px;
}
......@@ -198,11 +186,22 @@ ul.notes {
}
}
.timeline-content {
padding: 14px 10px;
.timeline-icon {
float: left;
@media (min-width: $screen-sm-min) {
margin-left: 20px;
svg {
width: 16px;
height: 16px;
fill: $gray-darkest;
position: absolute;
left: 0;
top: 2px;
}
}
.timeline-content {
@include notes-media('min', $screen-sm-min) {
margin-left: 30px;
}
}
......@@ -371,7 +370,7 @@ ul.notes {
display: flex;
justify-content: space-between;
@media (max-width: $screen-xs-max) {
@include notes-media('max', $screen-xs-max) {
flex-flow: row wrap;
}
}
......@@ -385,10 +384,16 @@ ul.notes {
padding-bottom: 0;
}
.note-header-author-name {
@include notes-media('max', $screen-xs-max) {
display: none;
}
}
.note-headline-light {
display: inline;
@media (max-width: $screen-xs-min) {
@include notes-media('max', $screen-xs-min) {
display: block;
}
}
......@@ -430,7 +435,7 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
@media (max-width: $screen-xs-max) {
@include notes-media('max', $screen-xs-max) {
float: none;
margin-left: 0;
}
......@@ -441,7 +446,7 @@ ul.notes {
}
.discussion-actions {
@media (max-width: $screen-md-max) {
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
......@@ -455,7 +460,7 @@ ul.notes {
display: inline;
line-height: 20px;
@media (min-width: $screen-sm-min) {
@include notes-media('min', $screen-sm-min) {
margin-left: 10px;
line-height: 24px;
}
......@@ -590,10 +595,15 @@ ul.notes {
.discussion-body,
.diff-file {
.notes .note {
padding: 10px 15px;
padding-left: $gl-padding;
padding-right: $gl-padding;
&.system-note {
padding: 0;
padding-left: 0;
@media (min-width: $screen-sm-min) {
margin-left: 70px;
}
}
}
}
......@@ -607,17 +617,11 @@ ul.notes {
}
.disabled-comment {
margin-left: -$gl-padding-top;
margin-right: -$gl-padding-top;
background-color: $gray-light;
border-radius: $border-radius-base;
border: 1px solid $border-gray-normal;
color: $note-disabled-comment-color;
line-height: 200px;
.disabled-comment-text {
line-height: normal;
}
padding: 90px 0;
a {
color: $gl-link-color;
......@@ -625,7 +629,7 @@ ul.notes {
}
.line-resolve-all-container {
@media (min-width: $screen-sm-min) {
@include notes-media('min', $screen-sm-min) {
margin-right: 0;
padding-left: $gl-padding;
}
......@@ -667,7 +671,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
padding: 6px 10px;
padding: 5px 10px 6px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
......@@ -680,6 +684,10 @@ ul.notes {
.line-resolve-btn {
margin-right: 5px;
svg {
vertical-align: middle;
}
}
}
......@@ -716,6 +724,10 @@ ul.notes {
}
}
.line-resolve-text {
vertical-align: middle;
}
.discussion-next-btn {
svg {
margin: 0;
......@@ -732,11 +744,6 @@ ul.notes {
// Merge request notes in diffs
.diff-file {
// Diff is side by side
.notes_content.parallel .note-header .note-headline-light {
display: block;
position: relative;
}
// Diff is inline
.notes_content .note-header .note-headline-light {
display: inline-block;
......
......@@ -88,6 +88,10 @@
}
}
.btn .text-center {
display: inline;
}
.tooltip {
white-space: nowrap;
}
......
......@@ -247,7 +247,6 @@
font-size: 13px;
font-weight: 600;
line-height: 13px;
padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
padding: 6px 14px;
text-align: center;
......@@ -384,10 +383,6 @@ a.deploy-project-label {
}
}
.last-push-widget {
margin-top: -1px;
}
.fork-namespaces {
.row {
-webkit-flex-wrap: wrap;
......
class Admin::HookLogsController < Admin::ApplicationController
include HooksExecution
before_action :hook, only: [:show, :retry]
before_action :hook_log, only: [:show, :retry]
respond_to :html
def show
end
def retry
status, message = hook.execute(hook_log.request_data, hook_log.trigger)
set_hook_execution_notice(status, message)
redirect_to edit_admin_hook_path(@hook)
end
private
def hook
@hook ||= SystemHook.find(params[:hook_id])
end
def hook_log
@hook_log ||= hook.web_hook_logs.find(params[:id])
end
end
class Admin::HooksController < Admin::ApplicationController
before_action :hook, only: :edit
include HooksExecution
before_action :hook_logs, only: :edit
def index
@hooks = SystemHook.all
......@@ -36,15 +38,9 @@ class Admin::HooksController < Admin::ApplicationController
end
def test
data = {
event_name: "project_create",
name: "Ruby",
path: "ruby",
project_id: 1,
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
hook.execute(data, 'system_hooks')
status, message = hook.execute(sample_hook_data, 'system_hooks')
set_hook_execution_notice(status, message)
redirect_back_or_default
end
......@@ -55,6 +51,11 @@ class Admin::HooksController < Admin::ApplicationController
@hook ||= SystemHook.find(params[:id])
end
def hook_logs
@hook_logs ||=
Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
end
def hook_params
params.require(:hook).permit(
:enable_ssl_verification,
......@@ -65,4 +66,15 @@ class Admin::HooksController < Admin::ApplicationController
:url
)
end
def sample_hook_data
{
event_name: "project_create",
name: "Ruby",
path: "ruby",
project_id: 1,
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
end
end
......@@ -283,12 +283,8 @@ class ApplicationController < ActionController::Base
request.base_url
end
def set_locale
Gitlab::I18n.set_locale(current_user)
yield
ensure
Gitlab::I18n.reset_locale
def set_locale(&block)
Gitlab::I18n.with_user_locale(current_user, &block)
end
def sessionless_sign_in(user)
......
......@@ -9,7 +9,7 @@ class AutocompleteController < ApplicationController
@users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
@users = @users.active
@users = @users.reorder(:name)
@users = @users.page(params[:page])
@users = @users.page(params[:page]).per(params[:per_page])
if params[:todo_filter].present? && current_user
@users = @users.todo_authors(current_user.id, params[:todo_state_filter])
......
......@@ -8,17 +8,6 @@ module DiffForPath
return render_404 unless diff_file
diff_commit = commit_for_diff(diff_file)
blob = diff_file.blob(diff_commit)
locals = {
diff_file: diff_file,
diff_commit: diff_commit,
diff_refs: diffs.diff_refs,
blob: blob,
project: project
}
render json: { html: view_to_html_string('projects/diffs/_content', locals) }
render json: { html: view_to_html_string('projects/diffs/_content', diff_file: diff_file) }
end
end
module HooksExecution
extend ActiveSupport::Concern
private
def set_hook_execution_notice(status, message)
if status && status >= 200 && status < 400
flash[:notice] = "Hook executed successfully: HTTP #{status}"
elsif status
flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}"
else
flash[:alert] = "Hook execution failed: #{message}"
end
end
end
......@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format|
format.html { @last_push = current_user.recent_push }
format.html
format.atom do
load_events
render layout: false
......@@ -25,7 +25,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(starred: true)).
includes(:forked_from_project, :tags).page(params[:page])
@last_push = current_user.recent_push
@groups = []
respond_to do |format|
......
......@@ -9,8 +9,6 @@ class DashboardController < Dashboard::ApplicationController
respond_to :html
def activity
@last_push = current_user.recent_push
respond_to do |format|
format.html
......
......@@ -64,6 +64,8 @@ class GroupsController < Groups::ApplicationController
end
def subgroups
return not_found unless Group.supports_nested_groups?
@nested_groups = GroupsFinder.new(current_user, parent: group).execute
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end
......@@ -165,7 +167,6 @@ class GroupsController < Groups::ApplicationController
def user_actions
if current_user
@last_push = current_user.recent_push
@notification_setting = current_user.notification_settings_for(group)
end
end
......
......@@ -51,13 +51,9 @@ class Projects::CompareController < Projects::ApplicationController
if @compare
@commits = @compare.commits
@start_commit = @compare.start_commit
@commit = @compare.commit
@base_commit = @compare.base_commit
@diffs = @compare.diffs(diff_options)
environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit }
environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@diff_notes_disabled = true
......
class Projects::HookLogsController < Projects::ApplicationController
include HooksExecution
before_action :authorize_admin_project!
before_action :hook, only: [:show, :retry]
before_action :hook_log, only: [:show, :retry]
respond_to :html
layout 'project_settings'
def show
end
def retry
status, message = hook.execute(hook_log.request_data, hook_log.trigger)
set_hook_execution_notice(status, message)
redirect_to edit_namespace_project_hook_path(@project.namespace, @project, @hook)
end
private
def hook
@hook ||= @project.hooks.find(params[:hook_id])
end
def hook_log
@hook_log ||= hook.web_hook_logs.find(params[:id])
end
end
class Projects::HooksController < Projects::ApplicationController
include HooksExecution
# Authorize
before_action :authorize_admin_project!
before_action :hook, only: :edit
before_action :hook_logs, only: :edit
respond_to :html
......@@ -34,13 +36,7 @@ class Projects::HooksController < Projects::ApplicationController
if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user)
if status && status >= 200 && status < 400
flash[:notice] = "Hook executed successfully: HTTP #{status}"
elsif status
flash[:alert] = "Hook executed successfully but returned HTTP #{status} #{message}"
else
flash[:alert] = "Hook execution failed: #{message}"
end
set_hook_execution_notice(status, message)
else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
end
......@@ -60,6 +56,11 @@ class Projects::HooksController < Projects::ApplicationController
@hook ||= @project.hooks.find(params[:id])
end
def hook_logs
@hook_logs ||=
Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
end
def hook_params
params.require(:hook).permit(
:job_events,
......
......@@ -14,7 +14,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_commit_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :check_if_can_be_merged, only: :show
......@@ -130,8 +129,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diff_notes_disabled = true
end
define_commit_vars
render_diff_for_path(@diffs)
end
......@@ -500,11 +497,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
def define_commit_vars
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
end
def define_diff_vars
@merge_request_diff =
if params[:diff_id]
......@@ -569,7 +561,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
......
......@@ -74,6 +74,6 @@ class Projects::RefsController < Projects::ApplicationController
private
def validate_ref_id
return not_found! if params[:id].present? && params[:id] !~ Gitlab::Regex.git_reference_regex
return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
end
end
......@@ -15,16 +15,6 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer))
end
def image_diff_class(diff)
if diff.deleted_file
"deleted"
elsif diff.new_file
"added"
else
nil
end
end
def commit_to_html(commit, ref, project)
render 'projects/commits/commit',
commit: commit,
......
......@@ -102,14 +102,14 @@ module DiffHelper
].join(' ').html_safe
end
def commit_for_diff(diff_file)
return diff_file.content_commit if diff_file.content_commit
if diff_file.deleted_file
@base_commit || @commit.parent || @commit
else
@commit
def diff_file_blob_raw_path(diff_file)
namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path))
end
def diff_file_old_blob_raw_path(diff_file)
sha = diff_file.old_content_sha
return unless sha
namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_content_sha, diff_file.old_path))
end
def diff_file_html_data(project, diff_file_path, diff_commit_id)
......@@ -120,8 +120,8 @@ module DiffHelper
}
end
def editable_diff?(diff)
!diff.deleted_file && @merge_request && @merge_request.source_project
def editable_diff?(diff_file)
!diff_file.deleted_file? && @merge_request && @merge_request.source_project
end
private
......
......@@ -69,13 +69,12 @@ module LabelsHelper
end
def render_colored_label(label, label_suffix = '', tooltip: true)
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
text_color = text_color_for_bg(label.color)
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) +
%(style="background-color: #{label_color}; color: #{text_color}" ) +
%(style="background-color: #{label.color}; color: #{text_color}" ) +
%(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>)
......
......@@ -85,6 +85,12 @@ module ProjectsHelper
@nav_tabs ||= get_project_nav_tabs(@project, current_user)
end
def project_search_tabs?(tab)
abilities = Array(search_tab_ability_map[tab])
abilities.any? { |ability| can?(current_user, ability, @project) }
end
def project_nav_tab?(name)
project_nav_tabs.include? name
end
......@@ -116,6 +122,7 @@ module ProjectsHelper
def last_push_event
return unless current_user
return current_user.recent_push unless @project
project_ids = [@project.id]
if fork = current_user.fork_of(@project)
......@@ -203,7 +210,17 @@ module ProjectsHelper
nav_tabs << :container_registry
end
tab_ability_map = {
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
end
end
nav_tabs.flatten
end
def tab_ability_map
{
environments: :read_environment,
milestones: :read_milestone,
pipelines: :read_pipeline,
......@@ -215,14 +232,15 @@ module ProjectsHelper
team: :read_project_member,
wiki: :read_wiki
}
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
end
end
nav_tabs.flatten
def search_tab_ability_map
@search_tab_ability_map ||= tab_ability_map.merge(
blobs: :download_code,
commits: :download_code,
merge_requests: :read_merge_request,
notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet]
)
end
def project_lfs_status(project)
......
......@@ -13,6 +13,7 @@ module SubmoduleHelper
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2
project.rstrip!
project.sub!(/\.git\z/, '')
if self_url?(url, namespace, project)
......
class BaseMailer < ActionMailer::Base
around_action :render_with_default_locale
helper ApplicationHelper
helper MarkupHelper
......@@ -14,6 +16,10 @@ class BaseMailer < ActionMailer::Base
private
def render_with_default_locale(&block)
Gitlab::I18n.with_default_locale(&block)
end
def default_sender_address
address = Mail::Address.new(Gitlab.config.gitlab.email_from)
address.display_name = Gitlab.config.gitlab.email_display_name
......
......@@ -10,9 +10,9 @@ module Ci
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? }
validates :ref, presence: { unless: :importing_or_inactive? }
validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :description, presence: true
before_save :set_next_run_at
......@@ -32,10 +32,6 @@ module Ci
update_attribute(:active, false)
end
def importing_or_inactive?
importing? || inactive?
end
def runnable_by_owner?
Ability.allowed?(owner, :create_pipeline, project)
end
......
......@@ -33,14 +33,4 @@ module NoteOnDiff
def created_at_diff?(diff_refs)
false
end
private
def noteable_diff_refs
if noteable.respond_to?(:diff_sha_refs)
noteable.diff_sha_refs
else
noteable.diff_refs
end
end
end
......@@ -84,89 +84,6 @@ module Routable
joins(:route).where(wheres.join(' OR '))
end
end
# Builds a relation to find multiple objects that are nested under user membership
#
# Usage:
#
# Klass.member_descendants(1)
#
# Returns an ActiveRecord::Relation.
def member_descendants(user_id)
joins(:route).
joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
INNER JOIN members ON members.source_id = r2.source_id
AND members.source_type = r2.source_type").
where('members.user_id = ?', user_id)
end
# Builds a relation to find multiple objects that are nested under user
# membership. Includes the parent, as opposed to `#member_descendants`
# which only includes the descendants.
#
# Usage:
#
# Klass.member_self_and_descendants(1)
#
# Returns an ActiveRecord::Relation.
def member_self_and_descendants(user_id)
joins(:route).
joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
OR routes.path = r2.path
INNER JOIN members ON members.source_id = r2.source_id
AND members.source_type = r2.source_type").
where('members.user_id = ?', user_id)
end
# Returns all objects in a hierarchy, where any node in the hierarchy is
# under the user membership.
#
# Usage:
#
# Klass.member_hierarchy(1)
#
# Examples:
#
# Given the following group tree...
#
# _______group_1_______
# | |
# | |
# nested_group_1 nested_group_2
# | |
# | |
# nested_group_1_1 nested_group_2_1
#
#
# ... the following results are returned:
#
# * the user is a member of group 1
# => 'group_1',
# 'nested_group_1', nested_group_1_1',
# 'nested_group_2', 'nested_group_2_1'
#
# * the user is a member of nested_group_2
# => 'group1',
# 'nested_group_2', 'nested_group_2_1'
#
# * the user is a member of nested_group_2_1
# => 'group1',
# 'nested_group_2', 'nested_group_2_1'
#
# Returns an ActiveRecord::Relation.
def member_hierarchy(user_id)
paths = member_self_and_descendants(user_id).pluck('routes.path')
return none if paths.empty?
wheres = paths.map do |path|
"#{connection.quote(path)} = routes.path
OR
#{connection.quote(path)} LIKE CONCAT(routes.path, '/%')"
end
joins(:route).where(wheres.join(' OR '))
end
end
def full_name
......
......@@ -3,7 +3,11 @@ module SelectForProjectAuthorization
module ClassMethods
def select_for_project_authorization
select("members.user_id, projects.id AS project_id, members.access_level")
select("projects.id AS project_id, members.access_level")
end
def select_as_master_for_project_authorization
select(["projects.id AS project_id", "#{Gitlab::Access::MASTER} AS access_level"])
end
end
end
......@@ -63,7 +63,7 @@ class DiffNote < Note
return false unless supported?
return true if for_commit?
diff_refs ||= noteable_diff_refs
diff_refs ||= noteable.diff_refs
self.position.diff_refs == diff_refs
end
......@@ -99,7 +99,7 @@ class DiffNote < Note
self.project,
nil,
old_diff_refs: self.position.diff_refs,
new_diff_refs: noteable_diff_refs,
new_diff_refs: noteable.diff_refs,
paths: self.position.paths
).execute(self)
end
......
......@@ -38,6 +38,10 @@ class Group < Namespace
after_save :update_two_factor_requirement
class << self
def supports_nested_groups?
Gitlab::Database.postgresql?
end
# Searches for groups matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
......@@ -78,7 +82,7 @@ class Group < Namespace
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
.where('project_namespace.share_with_group_lock = ?', false)
.select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
.select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
end
......
......@@ -2,6 +2,6 @@ class ServiceHook < WebHook
belongs_to :service
def execute(data)
super(data, 'service_hook')
WebHookService.new(self, data, 'service_hook').execute
end
end
......@@ -3,8 +3,4 @@ class SystemHook < WebHook
default_value_for :push_events, false
default_value_for :repository_update_events, true
def async_execute(data, hook_name)
Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name)
end
end
class WebHook < ActiveRecord::Base
include Sortable
include HTTParty
default_value_for :push_events, true
default_value_for :issues_events, false
......@@ -13,52 +12,18 @@ class WebHook < ActiveRecord::Base
default_value_for :repository_update_events, false
default_value_for :enable_ssl_verification, true
has_many :web_hook_logs, dependent: :destroy
scope :push_hooks, -> { where(push_events: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true) }
# HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout
validates :url, presence: true, url: true
def execute(data, hook_name)
parsed_url = URI.parse(url)
if parsed_url.userinfo.blank?
response = WebHook.post(url,
body: data.to_json,
headers: build_headers(hook_name),
verify: enable_ssl_verification)
else
post_url = url.gsub("#{parsed_url.userinfo}@", '')
auth = {
username: CGI.unescape(parsed_url.user),
password: CGI.unescape(parsed_url.password)
}
response = WebHook.post(post_url,
body: data.to_json,
headers: build_headers(hook_name),
verify: enable_ssl_verification,
basic_auth: auth)
end
[response.code, response.to_s]
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
logger.error("WebHook Error => #{e}")
[false, e.to_s]
WebHookService.new(self, data, hook_name).execute
end
def async_execute(data, hook_name)
Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name)
end
private
def build_headers(hook_name)
headers = {
'Content-Type' => 'application/json',
'X-Gitlab-Event' => hook_name.singularize.titleize
}
headers['X-Gitlab-Token'] = token if token.present?
headers
WebHookService.new(self, data, hook_name).async_execute
end
end
class WebHookLog < ActiveRecord::Base
belongs_to :web_hook
serialize :request_headers, Hash
serialize :request_data, Hash
serialize :response_headers, Hash
validates :web_hook, presence: true
def success?
response_status =~ /^2/
end
end
......@@ -133,6 +133,10 @@ class Label < ActiveRecord::Base
template
end
def color
super || DEFAULT_COLOR
end
def text_color
LabelsHelper.text_color_for_bg(self.color)
end
......
......@@ -61,7 +61,7 @@ class LegacyDiffNote < Note
return true if for_commit?
return true unless diff_line
return false unless noteable
return false if diff_refs && diff_refs != noteable_diff_refs
return false if diff_refs && diff_refs != noteable.diff_refs
noteable_diff = find_noteable_diff
......
......@@ -245,19 +245,6 @@ class MergeRequest < ActiveRecord::Base
end
end
# MRs created before 8.4 don't store a MergeRequestDiff#base_commit_sha,
# but we need to get a commit for the "View file @ ..." link by deleted files,
# so we find the likely one if we can't get the actual one.
# This will not be the actual base commit if the target branch was merged into
# the source branch after the merge request was created, but it is good enough
# for the specific purpose of linking to a commit.
# It is not good enough for use in `Gitlab::Git::DiffRefs`, which needs the
# true base commit, so we can't simply have `#diff_base_commit` fall back on
# this method.
def likely_diff_base_commit
first_commit.try(:parent) || first_commit
end
def diff_start_commit
if persisted?
merge_request_diff.start_commit
......@@ -322,22 +309,15 @@ class MergeRequest < ActiveRecord::Base
end
def diff_refs
return unless diff_start_commit || diff_base_commit
if persisted?
merge_request_diff.diff_refs
else
Gitlab::Diff::DiffRefs.new(
base_sha: diff_base_sha,
start_sha: diff_start_sha,
head_sha: diff_head_sha
)
end
# Return diff_refs instance trying to not touch the git repository
def diff_sha_refs
if merge_request_diff && merge_request_diff.diff_refs_by_sha?
merge_request_diff.diff_refs
else
diff_refs
end
end
def branch_merge_base_sha
......@@ -870,7 +850,7 @@ class MergeRequest < ActiveRecord::Base
end
def has_complete_diff_refs?
diff_sha_refs && diff_sha_refs.complete?
diff_refs && diff_refs.complete?
end
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
......
......@@ -150,6 +150,29 @@ class MergeRequestDiff < ActiveRecord::Base
)
end
# MRs created before 8.4 don't store their true diff refs (start and base),
# but we need to get a commit SHA for the "View file @ ..." link by a file,
# so we use an approximation of the diff refs if we can't get the actual one.
#
# These will not be the actual diff refs if the target branch was merged into
# the source branch after the merge request was created, but it is good enough
# for the specific purpose of linking to a commit.
#
# It is not good enough for highlighting diffs, so we can't simply pass
# these as `diff_refs.`
def fallback_diff_refs
real_refs = diff_refs
return real_refs if real_refs
likely_base_commit_sha = (first_commit&.parent || first_commit)&.sha
Gitlab::Diff::DiffRefs.new(
base_sha: likely_base_commit_sha,
start_sha: safe_start_commit_sha,
head_sha: head_commit_sha
)
end
def diff_refs_by_sha?
base_commit_sha? && head_commit_sha? && start_commit_sha?
end
......
......@@ -107,7 +107,7 @@ class Milestone < ActiveRecord::Base
end
def participants
User.joins(assigned_issues: :milestone).where("milestones.id = ?", id)
User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
end
def self.sort(method)
......
......@@ -176,26 +176,20 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any?
end
# Scopes the model on ancestors of the record
# Returns all the ancestors of the current namespaces.
def ancestors
if parent_id
path = route ? route.path : full_path
paths = []
return self.class.none unless parent_id
until path.blank?
path = path.rpartition('/').first
paths << path
Gitlab::GroupHierarchy.
new(self.class.where(id: parent_id)).
base_and_ancestors
end
self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
else
self.class.none
end
end
# Scopes the model on direct and indirect children of the record
# Returns all the descendants of the current namespace.
def descendants
self.class.joins(:route).merge(Route.inside_path(route.path)).reorder('routes.path ASC')
Gitlab::GroupHierarchy.
new(self.class.where(parent_id: id)).
base_and_descendants
end
def user_ids_for_project_authorizations
......
......@@ -205,8 +205,8 @@ class Project < ActiveRecord::Base
presence: true,
dynamic_path: true,
length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_format_regex,
message: Gitlab::Regex.project_path_regex_message },
format: { with: Gitlab::PathRegex.project_path_format_regex,
message: Gitlab::PathRegex.project_path_format_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true
......@@ -380,11 +380,9 @@ class Project < ActiveRecord::Base
end
def reference_pattern
name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
%r{
((?<namespace>#{name_pattern})\/)?
(?<project>#{name_pattern})
((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
(?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
}x
end
......
......@@ -6,6 +6,12 @@ class ProjectAuthorization < ActiveRecord::Base
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
def self.select_from_union(union)
select(['project_id', 'MAX(access_level) AS access_level']).
from("(#{union.to_sql}) #{ProjectAuthorization.table_name}").
group(:project_id)
end
def self.insert_authorizations(rows, per_batch = 1000)
rows.each_slice(per_batch) do |slice|
tuples = slice.map do |tuple|
......
......@@ -2,9 +2,10 @@ class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
validates :project_key, presence: true, if: :activated?
prop_accessor :username, :password, :url, :project_key,
prop_accessor :username, :password, :url, :api_url, :project_key,
:jira_issue_transition_id, :title, :description
before_update :reset_password
......@@ -25,20 +26,18 @@ class JiraService < IssueTrackerService
super do
self.properties = {
title: issues_tracker['title'],
url: issues_tracker['url']
url: issues_tracker['url'],
api_url: issues_tracker['api_url']
}
end
end
def reset_password
# don't reset the password if a new one is provided
if url_changed? && !password_touched?
self.password = nil
end
self.password = nil if reset_password?
end
def options
url = URI.parse(self.url)
url = URI.parse(client_url)
{
username: self.username,
......@@ -87,7 +86,8 @@ class JiraService < IssueTrackerService
def fields
[
{ type: 'text', name: 'url', title: 'URL', placeholder: 'https://jira.example.com' },
{ type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com' },
{ type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
{ type: 'text', name: 'project_key', placeholder: 'Project Key' },
{ type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' },
......@@ -186,7 +186,7 @@ class JiraService < IssueTrackerService
end
def test_settings
return unless url.present?
return unless client_url.present?
# Test settings by getting the project
jira_request { jira_project.present? }
end
......@@ -236,13 +236,13 @@ class JiraService < IssueTrackerService
end
def send_message(issue, message, remote_link_props)
return unless url.present?
return unless client_url.present?
jira_request do
if issue.comments.build.save!(body: message)
remote_link = issue.remotelink.build
remote_link.save!(remote_link_props)
result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}."
end
Rails.logger.info(result_message)
......@@ -295,7 +295,20 @@ class JiraService < IssueTrackerService
yield
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{e.message}"
nil
end
def client_url
api_url.present? ? api_url : url
end
def reset_password?
# don't reset the password if a new one is provided
return false if password_touched?
return true if api_url_changed?
return false if api_url.present?
url_changed?
end
end
......@@ -77,6 +77,14 @@ class KubernetesService < DeploymentService
]
end
def actual_namespace
if namespace.present?
namespace
else
default_namespace
end
end
# Check we can connect to the Kubernetes API
def test(*args)
kubeclient = build_kubeclient!
......@@ -91,7 +99,7 @@ class KubernetesService < DeploymentService
variables = [
{ key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false },
{ key: 'KUBE_NAMESPACE', value: namespace_variable, public: true }
{ key: 'KUBE_NAMESPACE', value: actual_namespace, public: true }
]
if ca_pem.present?
......@@ -110,7 +118,7 @@ class KubernetesService < DeploymentService
with_reactive_cache do |data|
pods = data.fetch(:pods, nil)
filter_pods(pods, app: environment.slug).
flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }.
each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end
end
......@@ -124,7 +132,7 @@ class KubernetesService < DeploymentService
# Store as hashes, rather than as third-party types
pods = begin
kubeclient.get_pods(namespace: namespace).as_json
kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err
raise err unless err.error_code == 404
[]
......@@ -142,20 +150,12 @@ class KubernetesService < DeploymentService
default_namespace || TEMPLATE_PLACEHOLDER
end
def namespace_variable
if namespace.present?
namespace
else
default_namespace
end
end
def default_namespace
"#{project.path}-#{project.id}" if project.present?
end
def build_kubeclient!(api_path: 'api', api_version: 'v1')
raise "Incomplete settings" unless api_url && namespace && token
raise "Incomplete settings" unless api_url && actual_namespace && token
::Kubeclient::Client.new(
join_api_url(api_path),
......
......@@ -10,9 +10,12 @@ class User < ActiveRecord::Base
include Sortable
include CaseSensitivity
include TokenAuthenticatable
include IgnorableColumn
DEFAULT_NOTIFICATION_LEVEL = :participating
ignore_column :authorized_projects_populated
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token
......@@ -218,7 +221,6 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
......@@ -368,7 +370,7 @@ class User < ActiveRecord::Base
def reference_pattern
%r{
#{Regexp.escape(reference_prefix)}
(?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
(?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
}x
end
......@@ -510,23 +512,16 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
def nested_groups
Group.member_descendants(id)
end
# Returns a relation of groups the user has access to, including their parent
# and child groups (recursively).
def all_expanded_groups
Group.member_hierarchy(id)
Gitlab::GroupHierarchy.new(groups).all_groups
end
def expanded_groups_requiring_two_factor_authentication
all_expanded_groups.where(require_two_factor_authentication: true)
end
def nested_groups_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id)
end
def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute
end
......@@ -535,18 +530,15 @@ class User < ActiveRecord::Base
project_authorizations.where(project_id: project_ids).delete_all
end
def set_authorized_projects_column
unless authorized_projects_populated
update_column(:authorized_projects_populated, true)
end
end
def authorized_projects(min_access_level = nil)
refresh_authorized_projects unless authorized_projects_populated
# We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association
# We're overriding an association, so explicitly call super with no
# arguments or it would be passed as `force_reload` to the association
projects = super()
projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level
if min_access_level
projects = projects.
where('project_authorizations.access_level >= ?', min_access_level)
end
projects
end
......@@ -919,13 +911,13 @@ class User < ActiveRecord::Base
end
def assigned_open_merge_requests_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do
Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do
MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do
IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
......
......@@ -39,6 +39,7 @@ class MergeRequestEntity < IssuableEntity
expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
expose :remove_source_branch?, as: :remove_source_branch
expose :project_archived do |merge_request|
merge_request.project.archived?
......
......@@ -28,6 +28,7 @@ module Issues
notification_service.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue.assignees, issue)
end
issue
......
......@@ -8,6 +8,7 @@ module Issues
create_note(issue)
notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue.assignees, issue)
end
issue
......
......@@ -13,6 +13,7 @@ module MergeRequests
notification_service.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request.assignees, merge_request)
end
merge_request
......
......@@ -13,6 +13,7 @@ module MergeRequests
create_note(merge_request)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request.assignees, merge_request)
end
private
......
......@@ -10,6 +10,7 @@ module MergeRequests
execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked
invalidate_cache_counts(merge_request.assignees, merge_request)
end
merge_request
......
......@@ -12,7 +12,7 @@ class SearchService
@project =
if params[:project_id].present?
the_project = Project.find_by(id: params[:project_id])
can?(current_user, :download_code, the_project) ? the_project : nil
can?(current_user, :read_project, the_project) ? the_project : nil
else
nil
end
......
......@@ -73,12 +73,11 @@ module Users
# remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = [])
return if remove.empty? && add.empty? && user.authorized_projects_populated
return if remove.empty? && add.empty?
User.transaction do
user.remove_project_authorizations(remove) unless remove.empty?
ProjectAuthorization.insert_authorizations(add) unless add.empty?
user.set_authorized_projects_column
end
# Since we batch insert authorization rows, Rails' associations may get
......@@ -101,38 +100,13 @@ module Users
end
def fresh_authorizations
ProjectAuthorization.
unscoped.
select('project_id, MAX(access_level) AS access_level').
from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}").
group(:project_id)
klass = if Group.supports_nested_groups?
Gitlab::ProjectAuthorizations::WithNestedGroups
else
Gitlab::ProjectAuthorizations::WithoutNestedGroups
end
private
# Returns a union query of projects that the user is authorized to access
def project_authorizations_union
relations = [
# Personal projects
user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
# Projects the user is a member of
user.projects.select_for_project_authorization,
# Projects of groups the user is a member of
user.groups_projects.select_for_project_authorization,
# Projects of subgroups of groups the user is a member of
user.nested_groups_projects.select_for_project_authorization,
# Projects shared with groups the user is a member of
user.groups.joins(:shared_projects).select_for_project_authorization,
# Projects shared with subgroups of groups the user is a member of
user.nested_groups.joins(:shared_projects).select_for_project_authorization
]
Gitlab::SQL::Union.new(relations)
klass.new(user).calculate
end
end
end
class WebHookService
class InternalErrorResponse
attr_reader :body, :headers, :code
def initialize
@headers = HTTParty::Response::Headers.new({})
@body = ''
@code = 'internal error'
end
end
include HTTParty
# HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout
attr_accessor :hook, :data, :hook_name
def initialize(hook, data, hook_name)
@hook = hook
@data = data
@hook_name = hook_name
end
def execute
start_time = Time.now
response = if parsed_url.userinfo.blank?
make_request(hook.url)
else
make_request_with_auth
end
log_execution(
trigger: hook_name,
url: hook.url,
request_data: data,
response: response,
execution_duration: Time.now - start_time
)
[response.code, response.to_s]
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
log_execution(
trigger: hook_name,
url: hook.url,
request_data: data,
response: InternalErrorResponse.new,
execution_duration: Time.now - start_time,
error_message: e.to_s
)
Rails.logger.error("WebHook Error => #{e}")
[nil, e.to_s]
end
def async_execute
Sidekiq::Client.enqueue(WebHookWorker, hook.id, data, hook_name)
end
private
def parsed_url
@parsed_url ||= URI.parse(hook.url)
end
def make_request(url, basic_auth = false)
self.class.post(url,
body: data.to_json,
headers: build_headers(hook_name),
verify: hook.enable_ssl_verification,
basic_auth: basic_auth)
end
def make_request_with_auth
post_url = hook.url.gsub("#{parsed_url.userinfo}@", '')
basic_auth = {
username: CGI.unescape(parsed_url.user),
password: CGI.unescape(parsed_url.password)
}
make_request(post_url, basic_auth)
end
def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
# logging for ServiceHook's is not available
return if hook.is_a?(ServiceHook)
WebHookLog.create(
web_hook: hook,
trigger: trigger,
url: url,
execution_duration: execution_duration,
request_headers: build_headers(hook_name),
request_data: request_data,
response_headers: format_response_headers(response),
response_body: response.body,
response_status: response.code,
internal_error_message: error_message
)
end
def build_headers(hook_name)
@headers ||= begin
{
'Content-Type' => 'application/json',
'X-Gitlab-Event' => hook_name.singularize.titleize
}.tap do |hash|
hash['X-Gitlab-Token'] = hook.token if hook.token.present?
end
end
end
# Make response headers more stylish
# Net::HTTPHeader has downcased hash with arrays: { 'content-type' => ['text/html; charset=utf-8'] }
# This method format response to capitalized hash with strings: { 'Content-Type' => 'text/html; charset=utf-8' }
def format_response_headers(response)
response.headers.each_capitalized.to_h
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.
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.
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.
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.
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.
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.
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.
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.
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