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: ...@@ -150,6 +150,7 @@ stages:
# Trigger a package build on omnibus-gitlab repository # Trigger a package build on omnibus-gitlab repository
build-package: build-package:
image: ruby:2.3-alpine
before_script: [] before_script: []
services: [] services: []
variables: variables:
...@@ -486,25 +487,6 @@ lint:javascript:report: ...@@ -486,25 +487,6 @@ lint:javascript:report:
paths: paths:
- eslint-report.html - 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: pages:
before_script: [] before_script: []
stage: pages stage: pages
......
...@@ -11,11 +11,11 @@ linters: ...@@ -11,11 +11,11 @@ linters:
# !global, !important, and !optional flags. # !global, !important, and !optional flags.
BangFormat: BangFormat:
enabled: false enabled: false
# Whether or not to prefer `border: 0` over `border: none`. # Whether or not to prefer `border: 0` over `border: none`.
BorderZero: BorderZero:
enabled: false enabled: false
# Reports when you define a rule set using a selector with chained classes # Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes). # (a.k.a. adjoining classes).
ChainedClasses: ChainedClasses:
...@@ -25,13 +25,13 @@ linters: ...@@ -25,13 +25,13 @@ linters:
# (e.g. `color: green` is a color keyword) # (e.g. `color: green` is a color keyword)
ColorKeyword: ColorKeyword:
enabled: false enabled: false
# Prefer color literals (keywords or hexadecimal codes) to be used only in # Prefer color literals (keywords or hexadecimal codes) to be used only in
# variable declarations. They should be referred to via variables everywhere # variable declarations. They should be referred to via variables everywhere
# else. # else.
ColorVariable: ColorVariable:
enabled: true enabled: true
# Which form of comments to prefer in CSS. # Which form of comments to prefer in CSS.
Comment: Comment:
enabled: false enabled: false
...@@ -39,7 +39,7 @@ linters: ...@@ -39,7 +39,7 @@ linters:
# Reports @debug statements (which you probably left behind accidentally). # Reports @debug statements (which you probably left behind accidentally).
DebugStatement: DebugStatement:
enabled: false enabled: false
# Rule sets should be ordered as follows: # Rule sets should be ordered as follows:
# - @extend declarations # - @extend declarations
# - @include declarations without inner @content # - @include declarations without inner @content
...@@ -54,19 +54,19 @@ linters: ...@@ -54,19 +54,19 @@ linters:
# more information. # more information.
DisableLinterReason: DisableLinterReason:
enabled: true enabled: true
# Reports when you define the same property twice in a single rule set. # Reports when you define the same property twice in a single rule set.
DuplicateProperty: DuplicateProperty:
enabled: false enabled: true
# Separate rule, function, and mixin declarations with empty lines. # Separate rule, function, and mixin declarations with empty lines.
EmptyLineBetweenBlocks: EmptyLineBetweenBlocks:
enabled: true enabled: true
# Reports when you have an empty rule set. # Reports when you have an empty rule set.
EmptyRule: EmptyRule:
enabled: true enabled: true
# Reports when you have an @extend directive. # Reports when you have an @extend directive.
ExtendDirective: ExtendDirective:
enabled: false enabled: false
...@@ -75,49 +75,49 @@ linters: ...@@ -75,49 +75,49 @@ linters:
# when adding lines to the file, since SCM systems such as git won't # when adding lines to the file, since SCM systems such as git won't
# think that you touched the last line. # think that you touched the last line.
FinalNewline: FinalNewline:
enabled: false enabled: true
# HEX colors should use three-character values where possible. # HEX colors should use three-character values where possible.
HexLength: HexLength:
enabled: false enabled: false
# HEX color values should use lower-case colors to differentiate between # HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`. # letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
HexNotation: HexNotation:
enabled: true enabled: true
# Avoid using ID selectors. # Avoid using ID selectors.
IdSelector: IdSelector:
enabled: false enabled: false
# The basenames of @imported SCSS partials should not begin with an # The basenames of @imported SCSS partials should not begin with an
# underscore and should not include the filename extension. # underscore and should not include the filename extension.
ImportPath: ImportPath:
enabled: false enabled: false
# Avoid using !important in properties. It is usually indicative of a # Avoid using !important in properties. It is usually indicative of a
# misunderstanding of CSS specificity and can lead to brittle code. # misunderstanding of CSS specificity and can lead to brittle code.
ImportantRule: ImportantRule:
enabled: false enabled: false
# Indentation should always be done in increments of 2 spaces. # Indentation should always be done in increments of 2 spaces.
Indentation: Indentation:
enabled: true enabled: true
width: 2 width: 2
# Don't write leading zeros for numeric values with a decimal point. # Don't write leading zeros for numeric values with a decimal point.
LeadingZero: LeadingZero:
enabled: false enabled: false
# Reports when you define the same selector twice in a single sheet. # Reports when you define the same selector twice in a single sheet.
MergeableSelector: MergeableSelector:
enabled: false enabled: false
# Functions, mixins, variables, and placeholders should be declared # Functions, mixins, variables, and placeholders should be declared
# with all lowercase letters and hyphens instead of underscores. # with all lowercase letters and hyphens instead of underscores.
NameFormat: NameFormat:
enabled: false enabled: false
# Avoid nesting selectors too deeply. # Avoid nesting selectors too deeply.
NestingDepth: NestingDepth:
enabled: false enabled: false
...@@ -129,12 +129,12 @@ linters: ...@@ -129,12 +129,12 @@ linters:
# Sort properties in a strict order. # Sort properties in a strict order.
PropertySortOrder: PropertySortOrder:
enabled: false enabled: false
# Reports when you use an unknown or disabled CSS property # Reports when you use an unknown or disabled CSS property
# (ignoring vendor-prefixed properties). # (ignoring vendor-prefixed properties).
PropertySpelling: PropertySpelling:
enabled: false enabled: false
# Configure which units are allowed for property values. # Configure which units are allowed for property values.
PropertyUnits: PropertyUnits:
enabled: false enabled: false
...@@ -144,25 +144,25 @@ linters: ...@@ -144,25 +144,25 @@ linters:
# be declared with one colon. # be declared with one colon.
PseudoElement: PseudoElement:
enabled: true enabled: true
# Avoid qualifying elements in selectors (also known as "tag-qualifying"). # Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement: QualifyingElement:
enabled: false enabled: false
# Don't write selectors with a depth of applicability greater than 3. # Don't write selectors with a depth of applicability greater than 3.
SelectorDepth: SelectorDepth:
enabled: false enabled: false
# Selectors should always use hyphenated-lowercase, rather than camelCase or # Selectors should always use hyphenated-lowercase, rather than camelCase or
# snake_case. # snake_case.
SelectorFormat: SelectorFormat:
enabled: false enabled: false
convention: hyphenated_lowercase convention: hyphenated_lowercase
# Prefer the shortest shorthand form possible for properties that support it. # Prefer the shortest shorthand form possible for properties that support it.
Shorthand: Shorthand:
enabled: true enabled: true
# Each property should have its own line, except in the special case of # Each property should have its own line, except in the special case of
# single line rulesets. # single line rulesets.
SingleLinePerProperty: SingleLinePerProperty:
...@@ -173,11 +173,11 @@ linters: ...@@ -173,11 +173,11 @@ linters:
# individual selector occupy a single line. # individual selector occupy a single line.
SingleLinePerSelector: SingleLinePerSelector:
enabled: true enabled: true
# Commas in lists should be followed by a space. # Commas in lists should be followed by a space.
SpaceAfterComma: SpaceAfterComma:
enabled: false enabled: false
# Properties should be formatted with a single space separating the colon # Properties should be formatted with a single space separating the colon
# from the property's value. # from the property's value.
SpaceAfterPropertyColon: SpaceAfterPropertyColon:
...@@ -197,12 +197,12 @@ linters: ...@@ -197,12 +197,12 @@ linters:
# colon. # colon.
SpaceAfterVariableName: SpaceAfterVariableName:
enabled: false enabled: false
# Operators should be formatted with a single space on both sides of an # Operators should be formatted with a single space on both sides of an
# infix operator. # infix operator.
SpaceAroundOperator: SpaceAroundOperator:
enabled: true enabled: true
# Opening braces should be preceded by a single space. # Opening braces should be preceded by a single space.
SpaceBeforeBrace: SpaceBeforeBrace:
enabled: true enabled: true
...@@ -210,7 +210,7 @@ linters: ...@@ -210,7 +210,7 @@ linters:
# Parentheses should not be padded with spaces. # Parentheses should not be padded with spaces.
SpaceBetweenParens: SpaceBetweenParens:
enabled: false enabled: false
# Enforces that string literals should be written with a consistent form # Enforces that string literals should be written with a consistent form
# of quotes (single or double). # of quotes (single or double).
StringQuotes: StringQuotes:
...@@ -241,7 +241,7 @@ linters: ...@@ -241,7 +241,7 @@ linters:
# be unnecessary. # be unnecessary.
UnnecessaryParentReference: UnnecessaryParentReference:
enabled: false enabled: false
# URLs should be valid and not contain protocols or domain names. # URLs should be valid and not contain protocols or domain names.
UrlFormat: UrlFormat:
enabled: true enabled: true
......
...@@ -341,7 +341,7 @@ GEM ...@@ -341,7 +341,7 @@ GEM
grape-entity (0.6.0) grape-entity (0.6.0)
activesupport activesupport
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grpc (1.2.5) grpc (1.3.4)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleauth (~> 0.5.1) googleauth (~> 0.5.1)
haml (4.0.7) haml (4.0.7)
......
This diff is collapsed.
...@@ -123,7 +123,7 @@ import ShortcutsBlob from './shortcuts_blob'; ...@@ -123,7 +123,7 @@ import ShortcutsBlob from './shortcuts_blob';
break; break;
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues: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'); new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
} }
Issuable.init(); Issuable.init();
......
...@@ -468,8 +468,8 @@ GitLabDropdown = (function() { ...@@ -468,8 +468,8 @@ GitLabDropdown = (function() {
// Process the data to make sure rendered data // Process the data to make sure rendered data
// matches the correct layout // matches the correct layout
if (this.fullData && hasMultiSelect && this.options.processData) { const inputValue = this.filterInput.val();
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)); this.options.processData.call(this.options, inputValue, this.filteredFullData(), this.parseData.bind(this));
} }
...@@ -740,6 +740,12 @@ GitLabDropdown = (function() { ...@@ -740,6 +740,12 @@ GitLabDropdown = (function() {
$input.attr('id', this.options.inputId); $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) { if (this.options.inputMeta) {
$input.attr('data-meta', selectedObject[this.options.inputMeta]); $input.attr('data-meta', selectedObject[this.options.inputMeta]);
} }
......
...@@ -42,3 +42,13 @@ export function formatRelevantDigits(number) { ...@@ -42,3 +42,13 @@ export function formatRelevantDigits(number) {
export function bytesToKiB(number) { export function bytesToKiB(number) {
return number / BYTES_IN_KIB; 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'; ...@@ -285,7 +285,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Similar to `toggler_behavior` in the discussion tab // Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash(); const hash = window.gl.utils.getLocationHash();
const anchor = hash && $container.find(`[id="${hash}"]`); const anchor = hash && $container.find(`[id="${hash}"]`);
if (anchor) { if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content'); const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old'; const lineType = notesContent.hasClass('new') ? 'new' : 'old';
notes.toggleDiffNote({ notes.toggleDiffNote({
......
This diff is collapsed.
<script> <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 stageColumnComponent from './stage_column_component.vue';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import '../../../flash'; import '../../../flash';
export default { export default {
props: {
isLoading: {
type: Boolean,
required: true,
},
pipeline: {
type: Object,
required: true,
},
},
components: { components: {
stageColumnComponent, stageColumnComponent,
loadingIcon, loadingIcon,
}, },
data() { computed: {
const DOMdata = document.getElementById('js-pipeline-graph-vue').dataset; graph() {
const store = new PipelineStore(); return this.pipeline.details && this.pipeline.details.stages;
},
return {
isLoading: false,
endpoint: DOMdata.endpoint,
store,
state: store.state,
};
},
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: { 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) { capitalizeStageName(name) {
return name.charAt(0).toUpperCase() + name.slice(1); return name.charAt(0).toUpperCase() + name.slice(1);
}, },
...@@ -101,7 +65,7 @@ ...@@ -101,7 +65,7 @@
v-if="!isLoading" v-if="!isLoading"
class="stage-column-list"> class="stage-column-list">
<stage-column-component <stage-column-component
v-for="(stage, index) in state.graph" v-for="(stage, index) in graph"
:title="capitalizeStageName(stage.name)" :title="capitalizeStageName(stage.name)"
:jobs="stage.groups" :jobs="stage.groups"
:key="stage.name" :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 { ...@@ -2,10 +2,10 @@ export default class PipelineStore {
constructor() { constructor() {
this.state = {}; this.state = {};
this.state.graph = []; this.state.pipeline = {};
} }
storeGraph(graph = []) { storePipeline(pipeline = {}) {
this.state.graph = graph; this.state.pipeline = pipeline;
} }
} }
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
window.SingleFileDiff = (function() { window.SingleFileDiff = (function() {
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; 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>'; LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
......
...@@ -35,6 +35,7 @@ function UsersSelect(currentUser, els) { ...@@ -35,6 +35,7 @@ function UsersSelect(currentUser, els) {
options.showCurrentUser = $dropdown.data('current-user'); options.showCurrentUser = $dropdown.data('current-user');
options.todoFilter = $dropdown.data('todo-filter'); options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter'); options.todoStateFilter = $dropdown.data('todo-state-filter');
options.perPage = $dropdown.data('per-page');
showNullUser = $dropdown.data('null-user'); showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default'); defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove'); showMenuAbove = $dropdown.data('showMenuAbove');
...@@ -214,7 +215,36 @@ function UsersSelect(currentUser, els) { ...@@ -214,7 +215,36 @@ function UsersSelect(currentUser, els) {
glDropdown.options.processData(term, users, callback); glDropdown.options.processData(term, users, callback);
}.bind(this)); }.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 anyUser;
let index; let index;
let j; let j;
...@@ -645,7 +675,7 @@ UsersSelect.prototype.users = function(query, options, callback) { ...@@ -645,7 +675,7 @@ UsersSelect.prototype.users = function(query, options, callback) {
url: url, url: url,
data: { data: {
search: query, search: query,
per_page: 20, per_page: options.perPage || 20,
active: true, active: true,
project_id: options.projectId || null, project_id: options.projectId || null,
group_id: options.groupId || null, group_id: options.groupId || null,
......
import statusCodes from '~/lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import MemoryGraph from '../../vue_shared/components/memory_graph'; import MemoryGraph from '../../vue_shared/components/memory_graph';
import MRWidgetService from '../services/mr_widget_service'; import MRWidgetService from '../services/mr_widget_service';
...@@ -9,8 +11,8 @@ export default { ...@@ -9,8 +11,8 @@ export default {
}, },
data() { data() {
return { return {
// memoryFrom: 0, memoryFrom: 0,
// memoryTo: 0, memoryTo: 0,
memoryMetrics: [], memoryMetrics: [],
deploymentTime: 0, deploymentTime: 0,
hasMetrics: false, hasMetrics: false,
...@@ -35,18 +37,38 @@ export default { ...@@ -35,18 +37,38 @@ export default {
shouldShowMetricsUnavailable() { shouldShowMetricsUnavailable() {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed; 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: { methods: {
getMegabytes(bytesString) {
const valueInBytes = Number(bytesString).toFixed(2);
return (bytesToMiB(valueInBytes)).toFixed(2);
},
computeGraphData(metrics, deploymentTime) { computeGraphData(metrics, deploymentTime) {
this.loadingMetrics = false; this.loadingMetrics = false;
const { memory_values } = metrics; const { memory_before, memory_after, memory_values } = metrics;
// if (memory_previous.length > 0) {
// this.memoryFrom = Number(memory_previous[0].value[1]).toFixed(2); // 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_current.length > 0) { if (memory_before.length > 0) {
// this.memoryTo = Number(memory_current[0].value[1]).toFixed(2); 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) { if (memory_values.length > 0) {
this.hasMetrics = true; this.hasMetrics = true;
...@@ -102,7 +124,7 @@ export default { ...@@ -102,7 +124,7 @@ export default {
<p <p
v-if="shouldShowMemoryGraph" v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info"> class="usage-info js-usage-info">
Deployment memory usage: Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
</p> </p>
<p <p
v-if="shouldShowLoadFailure" v-if="shouldShowLoadFailure"
......
...@@ -13,7 +13,7 @@ export default { ...@@ -13,7 +13,7 @@ export default {
}, },
data() { data() {
return { return {
removeSourceBranch: true, removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false, mergeWhenBuildSucceeds: false,
useCommitMessageWithDescription: false, useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false, setToMergeWhenPipelineSucceeds: false,
...@@ -69,6 +69,9 @@ export default { ...@@ -69,6 +69,9 @@ export default {
|| this.isMakingRequest || this.isMakingRequest
|| this.mr.preventMerge); || this.mr.preventMerge);
}, },
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled || !this.mr.canRemoveSourceBranch;
},
shouldShowSquashBeforeMerge() { shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge } = this.mr; const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1; return enableSquashBeforeMerge && commitsCount > 1;
...@@ -252,8 +255,9 @@ export default { ...@@ -252,8 +255,9 @@ export default {
<template v-if="isMergeAllowed()"> <template v-if="isMergeAllowed()">
<label class="spacing"> <label class="spacing">
<input <input
id="remove-source-branch-input"
v-model="removeSourceBranch" v-model="removeSourceBranch"
:disabled="isMergeButtonDisabled" :disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch type="checkbox"/> Remove source branch
</label> </label>
......
...@@ -50,7 +50,7 @@ export default class MergeRequestStore { ...@@ -50,7 +50,7 @@ export default class MergeRequestStore {
this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path; this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
this.removeWIPPath = data.remove_wip_path; this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists; 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.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false; this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path; 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 ...@@ -4,7 +4,7 @@ import PipelinesActionsComponent from '../../pipelines/components/pipelines_acti
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import ciBadge from './ci_badge_link.vue'; import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.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 PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
import CommitComponent from './commit'; 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 @@ ...@@ -34,6 +34,7 @@
@import "framework/selects.scss"; @import "framework/selects.scss";
@import "framework/sidebar.scss"; @import "framework/sidebar.scss";
@import "framework/tables.scss"; @import "framework/tables.scss";
@import "framework/notes.scss";
@import "framework/timeline.scss"; @import "framework/timeline.scss";
@import "framework/typography.scss"; @import "framework/typography.scss";
@import "framework/zen.scss"; @import "framework/zen.scss";
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
top: 0; top: 0;
margin-top: 3px; margin-top: 3px;
padding: $gl-padding; padding: $gl-padding;
z-index: 9; z-index: 300;
width: 300px; width: 300px;
font-size: 14px; font-size: 14px;
background-color: $white-light; background-color: $white-light;
......
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
.row-content-block { .row-content-block {
margin-top: 0; margin-top: 0;
margin-bottom: -$gl-padding;
background-color: $gray-light; background-color: $gray-light;
padding: $gl-padding; padding: $gl-padding;
margin-bottom: 0; margin-bottom: 0;
......
gl-emoji { gl-emoji {
display: inline-block;
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
......
...@@ -66,10 +66,10 @@ ...@@ -66,10 +66,10 @@
&.video { &.video {
background: $file-image-bg; background: $file-image-bg;
text-align: center; text-align: center;
padding: 30px;
img, img,
video { video {
padding: 20px;
max-width: 80%; max-width: 80%;
} }
} }
......
...@@ -36,6 +36,10 @@ ...@@ -36,6 +36,10 @@
border-radius: 0; border-radius: 0;
} }
} }
&:empty {
margin: 0;
}
} }
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
......
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
> li { > li {
padding: 10px 15px; padding: 10px 15px;
min-height: 20px; min-height: 20px;
border-bottom: 1px solid $list-border-light;
border-bottom: 1px solid $list-border; border-bottom: 1px solid $list-border;
&::after { &::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 @@ ...@@ -96,7 +96,6 @@
.select2-search-field input { .select2-search-field input {
padding: 5px $gl-padding / 2; padding: 5px $gl-padding / 2;
font-size: 13px;
height: auto; height: auto;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
.note-text { &::before {
p:last-child { @include notes-media('max', $screen-xs-max) {
margin-bottom: 0 !important; background: none;
} }
} }
...@@ -29,6 +29,16 @@ ...@@ -29,6 +29,16 @@
.timeline-entry-inner { .timeline-entry-inner {
position: relative; position: relative;
@include notes-media('max', $screen-xs-max) {
.timeline-icon {
display: none;
}
.timeline-content {
margin-left: 0;
}
}
} }
&:target, &:target,
...@@ -46,24 +56,6 @@ ...@@ -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 { .discussion .timeline-entry {
margin: 0; margin: 0;
border-right: none; border-right: none;
......
...@@ -21,6 +21,10 @@ ...@@ -21,6 +21,10 @@
margin-top: 0; margin-top: 0;
} }
> :last-child {
margin-bottom: 0;
}
// Single code lines should wrap // Single code lines should wrap
code { code {
font-family: $monospace_font; font-family: $monospace_font;
...@@ -157,7 +161,7 @@ ...@@ -157,7 +161,7 @@
ul, ul,
ol { ol {
padding: 0; padding: 0;
margin: 0 0 16px !important; margin: 0 0 16px;
} }
ul:dir(rtl), ul:dir(rtl),
......
...@@ -247,7 +247,6 @@ $dark-diff-match-bg: rgba(255, 255, 255, 0.3); ...@@ -247,7 +247,6 @@ $dark-diff-match-bg: rgba(255, 255, 255, 0.3);
$dark-diff-match-color: rgba(255, 255, 255, 0.1); $dark-diff-match-color: rgba(255, 255, 255, 0.1);
$file-mode-changed: #777; $file-mode-changed: #777;
$file-mode-changed: #777; $file-mode-changed: #777;
$diff-image-bg: #ddd;
$diff-image-info-color: grey; $diff-image-info-color: grey;
$diff-swipe-border: #999; $diff-swipe-border: #999;
$diff-view-modes-color: grey; $diff-view-modes-color: grey;
......
...@@ -72,7 +72,9 @@ ...@@ -72,7 +72,9 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
// scss-lint:disable DuplicateProperty
height: calc(100vh - 222px); height: calc(100vh - 222px);
// scss-lint:enable DuplicateProperty
min-height: 475px; min-height: 475px;
transition: width .2s; transition: width .2s;
......
...@@ -29,129 +29,140 @@ ...@@ -29,129 +29,140 @@
} }
} }
.build-page { @keyframes blinking-scroll-button {
pre.trace { 0% { opacity: 0.2; }
background: $builds-trace-bg; 25% { opacity: 0.5; }
color: $white-light; 50% { opacity: 0.7; }
font-family: $monospace_font; 100% { opacity: 1; }
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;
svg { .build-page {
position: relative; .sticky {
top: 1px; position: absolute;
margin-right: 5px; left: 0;
} right: 0;
} }
.truncated-info { .build-trace-container {
text-align: center; position: absolute;
border-bottom: 1px solid; top: 225px;
background-color: $black; left: 15px;
height: 45px; bottom: 10px;
padding: 15px; background: $black;
color: $gray-darkest;
font-family: $monospace_font;
font-size: 12px;
&.affix { &.sidebar-expanded {
top: 0; right: 305px;
} }
// with sidebar &.sidebar-collapsed {
&.affix.sidebar-expanded { right: 16px;
right: 312px;
left: 22px;
} }
// without sidebar code {
&.affix.sidebar-collapsed { background: $black;
right: 20px; color: $gray-darkest;
left: 20px;
} }
&.affix-top { .top-bar {
position: absolute;
top: 0; top: 0;
margin: 0 auto; height: 35px;
right: 5px; display: flex;
left: 5px; justify-content: flex-end;
} border-bottom: 1px outset $white-light;
.truncated-info-size { .truncated-info {
margin: 0 5px; margin: 0 auto;
} align-self: center;
.raw-link { .truncated-info-size {
color: inherit; margin: 0 5px;
margin-left: 5px; }
text-decoration: underline;
.raw-link {
color: inherit;
margin-left: 5px;
text-decoration: underline;
}
}
} }
}
}
.scroll-controls { .controllers {
height: 100%; display: flex;
align-self: center;
font-size: 15px;
.scroll-step { svg {
width: 31px; height: 15px;
margin: 0 0 0 auto; display: block;
} fill: $white-light;
}
.scroll-link, a,
.autoscroll-container { .btn-scroll {
right: 25px; margin: 0 8px;
z-index: 1; color: $white-light;
} }
.scroll-link { .btn-scroll.animate {
position: fixed; .first-triangle {
display: block; animation: blinking-scroll-button 1s ease infinite;
margin-bottom: 10px; animation-delay: .3s;
}
&.scroll-top .gitlab-icon-scroll-up-hover, .second-triangle {
&.scroll-top:hover .gitlab-icon-scroll-up, animation: blinking-scroll-button 1s ease infinite;
&.scroll-bottom .gitlab-icon-scroll-down-hover, animation-delay: .2s;
&.scroll-bottom:hover .gitlab-icon-scroll-down { }
display: none;
}
&.scroll-top:hover .gitlab-icon-scroll-up-hover, .third-triangle {
&.scroll-bottom:hover .gitlab-icon-scroll-down-hover { animation: blinking-scroll-button 1s ease infinite;
display: inline-block; }
}
&.scroll-top { &:disabled {
top: 10px; opacity: 1;
} }
}
&.scroll-bottom { .btn-scroll:disabled {
bottom: -2px; opacity: 0.35;
cursor: not-allowed;
}
} }
} }
.autoscroll-container { .bash {
position: absolute; 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, svg {
.autoscroll-container { position: relative;
right: ($gutter_width + ($gl-padding * 2)); 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 { .status-message {
...@@ -223,32 +234,6 @@ ...@@ -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 { .right-sidebar.build-sidebar {
padding: $gl-padding 0; padding: $gl-padding 0;
......
...@@ -36,7 +36,6 @@ ...@@ -36,7 +36,6 @@
pre.commit-message { pre.commit-message {
background: none; background: none;
padding: 0; padding: 0;
margin: 0;
border: none; border: none;
margin: 20px 0; margin: 20px 0;
border-radius: 0; border-radius: 0;
......
...@@ -94,7 +94,6 @@ ...@@ -94,7 +94,6 @@
.old_line, .old_line,
.new_line { .new_line {
margin: 0; margin: 0;
padding: 0;
border: none; border: none;
padding: 0 5px; padding: 0 5px;
border-right: 1px solid; border-right: 1px solid;
...@@ -151,14 +150,10 @@ ...@@ -151,14 +150,10 @@
} }
} }
} }
.text-file.diff-wrap-lines table .line_holder td span {
white-space: pre-wrap;
}
} }
.image { .image {
background: $diff-image-bg; background: $file-image-bg;
text-align: center; text-align: center;
padding: 30px; padding: 30px;
......
...@@ -64,6 +64,10 @@ ...@@ -64,6 +64,10 @@
} }
} }
.btn .text-center {
display: inline;
}
.commit-title { .commit-title {
margin: 0; margin: 0;
} }
......
...@@ -431,7 +431,7 @@ ...@@ -431,7 +431,7 @@
} }
.detail-page-description { .detail-page-description {
padding: 16px 0 0; padding: 16px 0;
small { small {
color: $gray-darkest; color: $gray-darkest;
...@@ -441,7 +441,7 @@ ...@@ -441,7 +441,7 @@
.edited-text { .edited-text {
color: $gray-darkest; color: $gray-darkest;
display: block; display: block;
margin: 0 0 16px; margin: 16px 0 0;
.author_link { .author_link {
color: $gray-darkest; color: $gray-darkest;
......
...@@ -204,7 +204,6 @@ ul.related-merge-requests > li { ...@@ -204,7 +204,6 @@ ul.related-merge-requests > li {
.dropdown-toggle { .dropdown-toggle {
.fa-caret-down { .fa-caret-down {
pointer-events: none; pointer-events: none;
margin-left: 0;
color: inherit; color: inherit;
margin-left: 0; margin-left: 0;
} }
......
...@@ -184,4 +184,4 @@ ...@@ -184,4 +184,4 @@
} }
} }
} }
} }
\ No newline at end of file
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
.note-edit-form { .note-edit-form {
.note-form-actions { .note-form-actions {
position: relative; position: relative;
margin: $gl-padding 0; margin: $gl-padding 0 0;
} }
.note-preview-holder { .note-preview-holder {
...@@ -124,10 +124,18 @@ ...@@ -124,10 +124,18 @@
} }
.discussion-form { .discussion-form {
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding $gl-padding;
background-color: $white-light; background-color: $white-light;
} }
.discussion-notes .disabled-comment {
padding: 6px 0;
}
.notes-form > li {
border: 0;
}
.note-edit-form { .note-edit-form {
display: none; display: none;
font-size: 14px; font-size: 14px;
......
...@@ -14,24 +14,11 @@ ul.notes { ...@@ -14,24 +14,11 @@ ul.notes {
margin: 0; margin: 0;
padding: 0; padding: 0;
.timeline-icon {
float: left;
svg {
width: 16px;
height: 16px;
fill: $gray-darkest;
position: absolute;
left: 0;
top: 16px;
}
}
.timeline-content { .timeline-content {
margin-left: 55px; margin-left: 55px;
&.timeline-content-form { &.timeline-content-form {
@media (max-width: $screen-sm-max) { @include notes-media('max', $screen-sm-max) {
margin-left: 0; margin-left: 0;
} }
} }
...@@ -56,21 +43,22 @@ ul.notes { ...@@ -56,21 +43,22 @@ ul.notes {
position: relative; position: relative;
} }
.note { > li {
padding: $gl-padding $gl-btn-padding 0; padding: $gl-padding $gl-btn-padding;
display: block; display: block;
position: relative; position: relative;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
&:last-child {
// Override `.timeline > li:last-child { border-bottom: none; }`
border-bottom: 1px solid $white-normal;
}
&.being-posted { &.being-posted {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
.dummy-avatar { .dummy-avatar {
display: inline-block;
height: 40px;
width: 40px;
border-radius: 50%;
background-color: $kdb-border; background-color: $kdb-border;
border: 1px solid darken($kdb-border, 25%); border: 1px solid darken($kdb-border, 25%);
} }
...@@ -126,13 +114,13 @@ ul.notes { ...@@ -126,13 +114,13 @@ ul.notes {
.note-awards { .note-awards {
.js-awards-block { .js-awards-block {
margin-bottom: 16px; margin-top: 16px;
} }
} }
.note-header { .note-header {
@media (max-width: $screen-xs-min) { @include notes-media('max', $screen-xs-min) {
.inline { .inline {
display: block; display: block;
} }
...@@ -161,10 +149,10 @@ ul.notes { ...@@ -161,10 +149,10 @@ ul.notes {
.system-note { .system-note {
font-size: 14px; font-size: 14px;
padding: 0; padding-left: 0;
clear: both; clear: both;
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-left: 65px; margin-left: 65px;
} }
...@@ -198,11 +186,22 @@ ul.notes { ...@@ -198,11 +186,22 @@ ul.notes {
} }
} }
.timeline-content { .timeline-icon {
padding: 14px 10px; float: left;
@media (min-width: $screen-sm-min) { svg {
margin-left: 20px; 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 { ...@@ -371,7 +370,7 @@ ul.notes {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@media (max-width: $screen-xs-max) { @include notes-media('max', $screen-xs-max) {
flex-flow: row wrap; flex-flow: row wrap;
} }
} }
...@@ -385,10 +384,16 @@ ul.notes { ...@@ -385,10 +384,16 @@ ul.notes {
padding-bottom: 0; padding-bottom: 0;
} }
.note-header-author-name {
@include notes-media('max', $screen-xs-max) {
display: none;
}
}
.note-headline-light { .note-headline-light {
display: inline; display: inline;
@media (max-width: $screen-xs-min) { @include notes-media('max', $screen-xs-min) {
display: block; display: block;
} }
} }
...@@ -430,7 +435,7 @@ ul.notes { ...@@ -430,7 +435,7 @@ ul.notes {
margin-left: 10px; margin-left: 10px;
color: $gray-darkest; color: $gray-darkest;
@media (max-width: $screen-xs-max) { @include notes-media('max', $screen-xs-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
} }
...@@ -441,7 +446,7 @@ ul.notes { ...@@ -441,7 +446,7 @@ ul.notes {
} }
.discussion-actions { .discussion-actions {
@media (max-width: $screen-md-max) { @include notes-media('max', $screen-md-max) {
float: none; float: none;
margin-left: 0; margin-left: 0;
...@@ -455,7 +460,7 @@ ul.notes { ...@@ -455,7 +460,7 @@ ul.notes {
display: inline; display: inline;
line-height: 20px; line-height: 20px;
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-left: 10px; margin-left: 10px;
line-height: 24px; line-height: 24px;
} }
...@@ -590,10 +595,15 @@ ul.notes { ...@@ -590,10 +595,15 @@ ul.notes {
.discussion-body, .discussion-body,
.diff-file { .diff-file {
.notes .note { .notes .note {
padding: 10px 15px; padding-left: $gl-padding;
padding-right: $gl-padding;
&.system-note { &.system-note {
padding: 0; padding-left: 0;
@media (min-width: $screen-sm-min) {
margin-left: 70px;
}
} }
} }
} }
...@@ -607,17 +617,11 @@ ul.notes { ...@@ -607,17 +617,11 @@ ul.notes {
} }
.disabled-comment { .disabled-comment {
margin-left: -$gl-padding-top;
margin-right: -$gl-padding-top;
background-color: $gray-light; background-color: $gray-light;
border-radius: $border-radius-base; border-radius: $border-radius-base;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
color: $note-disabled-comment-color; color: $note-disabled-comment-color;
line-height: 200px; padding: 90px 0;
.disabled-comment-text {
line-height: normal;
}
a { a {
color: $gl-link-color; color: $gl-link-color;
...@@ -625,7 +629,7 @@ ul.notes { ...@@ -625,7 +629,7 @@ ul.notes {
} }
.line-resolve-all-container { .line-resolve-all-container {
@media (min-width: $screen-sm-min) { @include notes-media('min', $screen-sm-min) {
margin-right: 0; margin-right: 0;
padding-left: $gl-padding; padding-left: $gl-padding;
} }
...@@ -667,7 +671,7 @@ ul.notes { ...@@ -667,7 +671,7 @@ ul.notes {
.line-resolve-all { .line-resolve-all {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
padding: 6px 10px; padding: 5px 10px 6px;
background-color: $gray-light; background-color: $gray-light;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
...@@ -680,6 +684,10 @@ ul.notes { ...@@ -680,6 +684,10 @@ ul.notes {
.line-resolve-btn { .line-resolve-btn {
margin-right: 5px; margin-right: 5px;
svg {
vertical-align: middle;
}
} }
} }
...@@ -716,6 +724,10 @@ ul.notes { ...@@ -716,6 +724,10 @@ ul.notes {
} }
} }
.line-resolve-text {
vertical-align: middle;
}
.discussion-next-btn { .discussion-next-btn {
svg { svg {
margin: 0; margin: 0;
...@@ -732,11 +744,6 @@ ul.notes { ...@@ -732,11 +744,6 @@ ul.notes {
// Merge request notes in diffs // Merge request notes in diffs
.diff-file { .diff-file {
// Diff is side by side
.notes_content.parallel .note-header .note-headline-light {
display: block;
position: relative;
}
// Diff is inline // Diff is inline
.notes_content .note-header .note-headline-light { .notes_content .note-header .note-headline-light {
display: inline-block; display: inline-block;
......
...@@ -88,6 +88,10 @@ ...@@ -88,6 +88,10 @@
} }
} }
.btn .text-center {
display: inline;
}
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
} }
......
...@@ -247,7 +247,6 @@ ...@@ -247,7 +247,6 @@
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
line-height: 13px; line-height: 13px;
padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px; letter-spacing: .4px;
padding: 6px 14px; padding: 6px 14px;
text-align: center; text-align: center;
...@@ -384,10 +383,6 @@ a.deploy-project-label { ...@@ -384,10 +383,6 @@ a.deploy-project-label {
} }
} }
.last-push-widget {
margin-top: -1px;
}
.fork-namespaces { .fork-namespaces {
.row { .row {
-webkit-flex-wrap: wrap; -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 class Admin::HooksController < Admin::ApplicationController
before_action :hook, only: :edit include HooksExecution
before_action :hook_logs, only: :edit
def index def index
@hooks = SystemHook.all @hooks = SystemHook.all
...@@ -36,15 +38,9 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -36,15 +38,9 @@ class Admin::HooksController < Admin::ApplicationController
end end
def test def test
data = { status, message = hook.execute(sample_hook_data, 'system_hooks')
event_name: "project_create",
name: "Ruby", set_hook_execution_notice(status, message)
path: "ruby",
project_id: 1,
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
hook.execute(data, 'system_hooks')
redirect_back_or_default redirect_back_or_default
end end
...@@ -55,6 +51,11 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -55,6 +51,11 @@ class Admin::HooksController < Admin::ApplicationController
@hook ||= SystemHook.find(params[:id]) @hook ||= SystemHook.find(params[:id])
end end
def hook_logs
@hook_logs ||=
Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
end
def hook_params def hook_params
params.require(:hook).permit( params.require(:hook).permit(
:enable_ssl_verification, :enable_ssl_verification,
...@@ -65,4 +66,15 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -65,4 +66,15 @@ class Admin::HooksController < Admin::ApplicationController
:url :url
) )
end 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 end
...@@ -283,12 +283,8 @@ class ApplicationController < ActionController::Base ...@@ -283,12 +283,8 @@ class ApplicationController < ActionController::Base
request.base_url request.base_url
end end
def set_locale def set_locale(&block)
Gitlab::I18n.set_locale(current_user) Gitlab::I18n.with_user_locale(current_user, &block)
yield
ensure
Gitlab::I18n.reset_locale
end end
def sessionless_sign_in(user) def sessionless_sign_in(user)
......
...@@ -9,7 +9,7 @@ class AutocompleteController < ApplicationController ...@@ -9,7 +9,7 @@ class AutocompleteController < ApplicationController
@users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present? @users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
@users = @users.active @users = @users.active
@users = @users.reorder(:name) @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 if params[:todo_filter].present? && current_user
@users = @users.todo_authors(current_user.id, params[:todo_state_filter]) @users = @users.todo_authors(current_user.id, params[:todo_state_filter])
......
...@@ -8,17 +8,6 @@ module DiffForPath ...@@ -8,17 +8,6 @@ module DiffForPath
return render_404 unless diff_file return render_404 unless diff_file
diff_commit = commit_for_diff(diff_file) render json: { html: view_to_html_string('projects/diffs/_content', diff_file: 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) }
end end
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 ...@@ -8,7 +8,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(non_public: true)).page(params[:page]) @projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format| respond_to do |format|
format.html { @last_push = current_user.recent_push } format.html
format.atom do format.atom do
load_events load_events
render layout: false render layout: false
...@@ -25,7 +25,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -25,7 +25,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = load_projects(params.merge(starred: true)). @projects = load_projects(params.merge(starred: true)).
includes(:forked_from_project, :tags).page(params[:page]) includes(:forked_from_project, :tags).page(params[:page])
@last_push = current_user.recent_push
@groups = [] @groups = []
respond_to do |format| respond_to do |format|
......
...@@ -9,8 +9,6 @@ class DashboardController < Dashboard::ApplicationController ...@@ -9,8 +9,6 @@ class DashboardController < Dashboard::ApplicationController
respond_to :html respond_to :html
def activity def activity
@last_push = current_user.recent_push
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -64,6 +64,8 @@ class GroupsController < Groups::ApplicationController ...@@ -64,6 +64,8 @@ class GroupsController < Groups::ApplicationController
end end
def subgroups def subgroups
return not_found unless Group.supports_nested_groups?
@nested_groups = GroupsFinder.new(current_user, parent: group).execute @nested_groups = GroupsFinder.new(current_user, parent: group).execute
@nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present? @nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
end end
...@@ -165,7 +167,6 @@ class GroupsController < Groups::ApplicationController ...@@ -165,7 +167,6 @@ class GroupsController < Groups::ApplicationController
def user_actions def user_actions
if current_user if current_user
@last_push = current_user.recent_push
@notification_setting = current_user.notification_settings_for(group) @notification_setting = current_user.notification_settings_for(group)
end end
end end
......
...@@ -51,13 +51,9 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -51,13 +51,9 @@ class Projects::CompareController < Projects::ApplicationController
if @compare if @compare
@commits = @compare.commits @commits = @compare.commits
@start_commit = @compare.start_commit
@commit = @compare.commit
@base_commit = @compare.base_commit
@diffs = @compare.diffs(diff_options) @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 @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
@diff_notes_disabled = true @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 class Projects::HooksController < Projects::ApplicationController
include HooksExecution
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :hook, only: :edit before_action :hook_logs, only: :edit
respond_to :html respond_to :html
...@@ -34,13 +36,7 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -34,13 +36,7 @@ class Projects::HooksController < Projects::ApplicationController
if !@project.empty_repo? if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user) status, message = TestHookService.new.execute(hook, current_user)
if status && status >= 200 && status < 400 set_hook_execution_notice(status, message)
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
else else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.' flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
end end
...@@ -60,6 +56,11 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -60,6 +56,11 @@ class Projects::HooksController < Projects::ApplicationController
@hook ||= @project.hooks.find(params[:id]) @hook ||= @project.hooks.find(params[:id])
end end
def hook_logs
@hook_logs ||=
Kaminari.paginate_array(hook.web_hook_logs.order(created_at: :desc)).page(params[:page])
end
def hook_params def hook_params
params.require(:hook).permit( params.require(:hook).permit(
:job_events, :job_events,
......
...@@ -14,7 +14,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -14,7 +14,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
] ]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] 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_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 :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 :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
before_action :check_if_can_be_merged, only: :show before_action :check_if_can_be_merged, only: :show
...@@ -130,8 +129,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -130,8 +129,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diff_notes_disabled = true @diff_notes_disabled = true
end end
define_commit_vars
render_diff_for_path(@diffs) render_diff_for_path(@diffs)
end end
...@@ -500,11 +497,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -500,11 +497,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end 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 def define_diff_vars
@merge_request_diff = @merge_request_diff =
if params[:diff_id] if params[:diff_id]
...@@ -569,7 +561,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -569,7 +561,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@source_project = merge_request.source_project @source_project = merge_request.source_project
@commits = @merge_request.compare_commits.reverse @commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit @commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@note_counts = Note.where(commit_id: @commits.map(&:id)). @note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count group(:commit_id).count
......
...@@ -74,6 +74,6 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -74,6 +74,6 @@ class Projects::RefsController < Projects::ApplicationController
private private
def validate_ref_id 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
end end
...@@ -15,16 +15,6 @@ module CommitsHelper ...@@ -15,16 +15,6 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer)) commit_person_link(commit, options.merge(source: :committer))
end 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) def commit_to_html(commit, ref, project)
render 'projects/commits/commit', render 'projects/commits/commit',
commit: commit, commit: commit,
......
...@@ -102,14 +102,14 @@ module DiffHelper ...@@ -102,14 +102,14 @@ module DiffHelper
].join(' ').html_safe ].join(' ').html_safe
end end
def commit_for_diff(diff_file) def diff_file_blob_raw_path(diff_file)
return diff_file.content_commit if diff_file.content_commit namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.content_sha, diff_file.file_path))
end
if diff_file.deleted_file def diff_file_old_blob_raw_path(diff_file)
@base_commit || @commit.parent || @commit sha = diff_file.old_content_sha
else return unless sha
@commit namespace_project_raw_path(@project.namespace, @project, tree_join(diff_file.old_content_sha, diff_file.old_path))
end
end end
def diff_file_html_data(project, diff_file_path, diff_commit_id) def diff_file_html_data(project, diff_file_path, diff_commit_id)
...@@ -120,8 +120,8 @@ module DiffHelper ...@@ -120,8 +120,8 @@ module DiffHelper
} }
end end
def editable_diff?(diff) def editable_diff?(diff_file)
!diff.deleted_file && @merge_request && @merge_request.source_project !diff_file.deleted_file? && @merge_request && @merge_request.source_project
end end
private private
......
...@@ -69,13 +69,12 @@ module LabelsHelper ...@@ -69,13 +69,12 @@ module LabelsHelper
end end
def render_colored_label(label, label_suffix = '', tooltip: true) 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 # Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter # by LabelReferenceFilter
span = %(<span class="label color-label #{"has-tooltip" if tooltip}" ) + 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">) + %(title="#{escape_once(label.description)}" data-container="body">) +
%(#{escape_once(label.name)}#{label_suffix}</span>) %(#{escape_once(label.name)}#{label_suffix}</span>)
......
...@@ -85,6 +85,12 @@ module ProjectsHelper ...@@ -85,6 +85,12 @@ module ProjectsHelper
@nav_tabs ||= get_project_nav_tabs(@project, current_user) @nav_tabs ||= get_project_nav_tabs(@project, current_user)
end 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) def project_nav_tab?(name)
project_nav_tabs.include? name project_nav_tabs.include? name
end end
...@@ -116,6 +122,7 @@ module ProjectsHelper ...@@ -116,6 +122,7 @@ module ProjectsHelper
def last_push_event def last_push_event
return unless current_user return unless current_user
return current_user.recent_push unless @project
project_ids = [@project.id] project_ids = [@project.id]
if fork = current_user.fork_of(@project) if fork = current_user.fork_of(@project)
...@@ -203,7 +210,17 @@ module ProjectsHelper ...@@ -203,7 +210,17 @@ module ProjectsHelper
nav_tabs << :container_registry nav_tabs << :container_registry
end 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, environments: :read_environment,
milestones: :read_milestone, milestones: :read_milestone,
pipelines: :read_pipeline, pipelines: :read_pipeline,
...@@ -215,14 +232,15 @@ module ProjectsHelper ...@@ -215,14 +232,15 @@ module ProjectsHelper
team: :read_project_member, team: :read_project_member,
wiki: :read_wiki wiki: :read_wiki
} }
end
tab_ability_map.each do |tab, ability| def search_tab_ability_map
if can?(current_user, ability, project) @search_tab_ability_map ||= tab_ability_map.merge(
nav_tabs << tab blobs: :download_code,
end commits: :download_code,
end merge_requests: :read_merge_request,
notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet]
nav_tabs.flatten )
end end
def project_lfs_status(project) def project_lfs_status(project)
......
...@@ -13,6 +13,7 @@ module SubmoduleHelper ...@@ -13,6 +13,7 @@ module SubmoduleHelper
if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/ if url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace, project = $1, $2 namespace, project = $1, $2
project.rstrip!
project.sub!(/\.git\z/, '') project.sub!(/\.git\z/, '')
if self_url?(url, namespace, project) if self_url?(url, namespace, project)
......
class BaseMailer < ActionMailer::Base class BaseMailer < ActionMailer::Base
around_action :render_with_default_locale
helper ApplicationHelper helper ApplicationHelper
helper MarkupHelper helper MarkupHelper
...@@ -14,6 +16,10 @@ class BaseMailer < ActionMailer::Base ...@@ -14,6 +16,10 @@ class BaseMailer < ActionMailer::Base
private private
def render_with_default_locale(&block)
Gitlab::I18n.with_default_locale(&block)
end
def default_sender_address def default_sender_address
address = Mail::Address.new(Gitlab.config.gitlab.email_from) address = Mail::Address.new(Gitlab.config.gitlab.email_from)
address.display_name = Gitlab.config.gitlab.email_display_name address.display_name = Gitlab.config.gitlab.email_display_name
......
...@@ -10,9 +10,9 @@ module Ci ...@@ -10,9 +10,9 @@ module Ci
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline' has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines has_many :pipelines
validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? } validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing_or_inactive? } validates :ref, presence: { unless: :importing? }
validates :description, presence: true validates :description, presence: true
before_save :set_next_run_at before_save :set_next_run_at
...@@ -32,10 +32,6 @@ module Ci ...@@ -32,10 +32,6 @@ module Ci
update_attribute(:active, false) update_attribute(:active, false)
end end
def importing_or_inactive?
importing? || inactive?
end
def runnable_by_owner? def runnable_by_owner?
Ability.allowed?(owner, :create_pipeline, project) Ability.allowed?(owner, :create_pipeline, project)
end end
......
...@@ -33,14 +33,4 @@ module NoteOnDiff ...@@ -33,14 +33,4 @@ module NoteOnDiff
def created_at_diff?(diff_refs) def created_at_diff?(diff_refs)
false false
end end
private
def noteable_diff_refs
if noteable.respond_to?(:diff_sha_refs)
noteable.diff_sha_refs
else
noteable.diff_refs
end
end
end end
...@@ -84,89 +84,6 @@ module Routable ...@@ -84,89 +84,6 @@ module Routable
joins(:route).where(wheres.join(' OR ')) joins(:route).where(wheres.join(' OR '))
end end
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 end
def full_name def full_name
......
...@@ -3,7 +3,11 @@ module SelectForProjectAuthorization ...@@ -3,7 +3,11 @@ module SelectForProjectAuthorization
module ClassMethods module ClassMethods
def select_for_project_authorization 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 end
end end
...@@ -63,7 +63,7 @@ class DiffNote < Note ...@@ -63,7 +63,7 @@ class DiffNote < Note
return false unless supported? return false unless supported?
return true if for_commit? return true if for_commit?
diff_refs ||= noteable_diff_refs diff_refs ||= noteable.diff_refs
self.position.diff_refs == diff_refs self.position.diff_refs == diff_refs
end end
...@@ -99,7 +99,7 @@ class DiffNote < Note ...@@ -99,7 +99,7 @@ class DiffNote < Note
self.project, self.project,
nil, nil,
old_diff_refs: self.position.diff_refs, old_diff_refs: self.position.diff_refs,
new_diff_refs: noteable_diff_refs, new_diff_refs: noteable.diff_refs,
paths: self.position.paths paths: self.position.paths
).execute(self) ).execute(self)
end end
......
...@@ -38,6 +38,10 @@ class Group < Namespace ...@@ -38,6 +38,10 @@ class Group < Namespace
after_save :update_two_factor_requirement after_save :update_two_factor_requirement
class << self class << self
def supports_nested_groups?
Gitlab::Database.postgresql?
end
# Searches for groups matching the given query. # Searches for groups matching the given query.
# #
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
...@@ -78,7 +82,7 @@ class Group < Namespace ...@@ -78,7 +82,7 @@ class Group < Namespace
if current_scope.joins_values.include?(:shared_projects) if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id') joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
.where('project_namespace.share_with_group_lock = ?', false) .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 else
super super
end end
......
...@@ -2,6 +2,6 @@ class ServiceHook < WebHook ...@@ -2,6 +2,6 @@ class ServiceHook < WebHook
belongs_to :service belongs_to :service
def execute(data) def execute(data)
super(data, 'service_hook') WebHookService.new(self, data, 'service_hook').execute
end end
end end
...@@ -3,8 +3,4 @@ class SystemHook < WebHook ...@@ -3,8 +3,4 @@ class SystemHook < WebHook
default_value_for :push_events, false default_value_for :push_events, false
default_value_for :repository_update_events, true default_value_for :repository_update_events, true
def async_execute(data, hook_name)
Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name)
end
end end
class WebHook < ActiveRecord::Base class WebHook < ActiveRecord::Base
include Sortable include Sortable
include HTTParty
default_value_for :push_events, true default_value_for :push_events, true
default_value_for :issues_events, false default_value_for :issues_events, false
...@@ -13,52 +12,18 @@ class WebHook < ActiveRecord::Base ...@@ -13,52 +12,18 @@ class WebHook < ActiveRecord::Base
default_value_for :repository_update_events, false default_value_for :repository_update_events, false
default_value_for :enable_ssl_verification, true default_value_for :enable_ssl_verification, true
has_many :web_hook_logs, dependent: :destroy
scope :push_hooks, -> { where(push_events: true) } scope :push_hooks, -> { where(push_events: true) }
scope :tag_push_hooks, -> { where(tag_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 validates :url, presence: true, url: true
def execute(data, hook_name) def execute(data, hook_name)
parsed_url = URI.parse(url) WebHookService.new(self, data, hook_name).execute
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]
end end
def async_execute(data, hook_name) def async_execute(data, hook_name)
Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name) WebHookService.new(self, data, hook_name).async_execute
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
end end
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 ...@@ -133,6 +133,10 @@ class Label < ActiveRecord::Base
template template
end end
def color
super || DEFAULT_COLOR
end
def text_color def text_color
LabelsHelper.text_color_for_bg(self.color) LabelsHelper.text_color_for_bg(self.color)
end end
......
...@@ -61,7 +61,7 @@ class LegacyDiffNote < Note ...@@ -61,7 +61,7 @@ class LegacyDiffNote < Note
return true if for_commit? return true if for_commit?
return true unless diff_line return true unless diff_line
return false unless noteable 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 noteable_diff = find_noteable_diff
......
...@@ -245,19 +245,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -245,19 +245,6 @@ class MergeRequest < ActiveRecord::Base
end end
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 def diff_start_commit
if persisted? if persisted?
merge_request_diff.start_commit merge_request_diff.start_commit
...@@ -322,21 +309,14 @@ class MergeRequest < ActiveRecord::Base ...@@ -322,21 +309,14 @@ class MergeRequest < ActiveRecord::Base
end end
def diff_refs def diff_refs
return unless diff_start_commit || diff_base_commit if persisted?
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 merge_request_diff.diff_refs
else else
diff_refs Gitlab::Diff::DiffRefs.new(
base_sha: diff_base_sha,
start_sha: diff_start_sha,
head_sha: diff_head_sha
)
end end
end end
...@@ -870,7 +850,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -870,7 +850,7 @@ class MergeRequest < ActiveRecord::Base
end end
def has_complete_diff_refs? def has_complete_diff_refs?
diff_sha_refs && diff_sha_refs.complete? diff_refs && diff_refs.complete?
end end
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:, current_user: nil) def update_diff_notes_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
......
...@@ -150,6 +150,29 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -150,6 +150,29 @@ class MergeRequestDiff < ActiveRecord::Base
) )
end 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? def diff_refs_by_sha?
base_commit_sha? && head_commit_sha? && start_commit_sha? base_commit_sha? && head_commit_sha? && start_commit_sha?
end end
......
...@@ -107,7 +107,7 @@ class Milestone < ActiveRecord::Base ...@@ -107,7 +107,7 @@ class Milestone < ActiveRecord::Base
end end
def participants def participants
User.joins(assigned_issues: :milestone).where("milestones.id = ?", id) User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
end end
def self.sort(method) def self.sort(method)
......
...@@ -176,26 +176,20 @@ class Namespace < ActiveRecord::Base ...@@ -176,26 +176,20 @@ class Namespace < ActiveRecord::Base
projects.with_shared_runners.any? projects.with_shared_runners.any?
end end
# Scopes the model on ancestors of the record # Returns all the ancestors of the current namespaces.
def ancestors def ancestors
if parent_id return self.class.none unless parent_id
path = route ? route.path : full_path
paths = []
until path.blank? Gitlab::GroupHierarchy.
path = path.rpartition('/').first new(self.class.where(id: parent_id)).
paths << path base_and_ancestors
end
self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
else
self.class.none
end
end end
# Scopes the model on direct and indirect children of the record # Returns all the descendants of the current namespace.
def descendants 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 end
def user_ids_for_project_authorizations def user_ids_for_project_authorizations
......
...@@ -205,8 +205,8 @@ class Project < ActiveRecord::Base ...@@ -205,8 +205,8 @@ class Project < ActiveRecord::Base
presence: true, presence: true,
dynamic_path: true, dynamic_path: true,
length: { maximum: 255 }, length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_format_regex, format: { with: Gitlab::PathRegex.project_path_format_regex,
message: Gitlab::Regex.project_path_regex_message }, message: Gitlab::PathRegex.project_path_format_message },
uniqueness: { scope: :namespace_id } uniqueness: { scope: :namespace_id }
validates :namespace, presence: true validates :namespace, presence: true
...@@ -380,11 +380,9 @@ class Project < ActiveRecord::Base ...@@ -380,11 +380,9 @@ class Project < ActiveRecord::Base
end end
def reference_pattern def reference_pattern
name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
%r{ %r{
((?<namespace>#{name_pattern})\/)? ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
(?<project>#{name_pattern}) (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
}x }x
end end
......
...@@ -6,6 +6,12 @@ class ProjectAuthorization < ActiveRecord::Base ...@@ -6,6 +6,12 @@ class ProjectAuthorization < ActiveRecord::Base
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
validates :user, uniqueness: { scope: [:project, :access_level] }, 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) def self.insert_authorizations(rows, per_batch = 1000)
rows.each_slice(per_batch) do |slice| rows.each_slice(per_batch) do |slice|
tuples = slice.map do |tuple| tuples = slice.map do |tuple|
......
...@@ -2,9 +2,10 @@ class JiraService < IssueTrackerService ...@@ -2,9 +2,10 @@ class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
validates :url, url: true, presence: true, if: :activated? validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
validates :project_key, presence: true, if: :activated? 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 :jira_issue_transition_id, :title, :description
before_update :reset_password before_update :reset_password
...@@ -25,20 +26,18 @@ class JiraService < IssueTrackerService ...@@ -25,20 +26,18 @@ class JiraService < IssueTrackerService
super do super do
self.properties = { self.properties = {
title: issues_tracker['title'], title: issues_tracker['title'],
url: issues_tracker['url'] url: issues_tracker['url'],
api_url: issues_tracker['api_url']
} }
end end
end end
def reset_password def reset_password
# don't reset the password if a new one is provided self.password = nil if reset_password?
if url_changed? && !password_touched?
self.password = nil
end
end end
def options def options
url = URI.parse(self.url) url = URI.parse(client_url)
{ {
username: self.username, username: self.username,
...@@ -87,7 +86,8 @@ class JiraService < IssueTrackerService ...@@ -87,7 +86,8 @@ class JiraService < IssueTrackerService
def fields 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: 'project_key', placeholder: 'Project Key' },
{ type: 'text', name: 'username', placeholder: '' }, { type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' }, { type: 'password', name: 'password', placeholder: '' },
...@@ -186,7 +186,7 @@ class JiraService < IssueTrackerService ...@@ -186,7 +186,7 @@ class JiraService < IssueTrackerService
end end
def test_settings def test_settings
return unless url.present? return unless client_url.present?
# Test settings by getting the project # Test settings by getting the project
jira_request { jira_project.present? } jira_request { jira_project.present? }
end end
...@@ -236,13 +236,13 @@ class JiraService < IssueTrackerService ...@@ -236,13 +236,13 @@ class JiraService < IssueTrackerService
end end
def send_message(issue, message, remote_link_props) def send_message(issue, message, remote_link_props)
return unless url.present? return unless client_url.present?
jira_request do jira_request do
if issue.comments.build.save!(body: message) if issue.comments.build.save!(body: message)
remote_link = issue.remotelink.build remote_link = issue.remotelink.build
remote_link.save!(remote_link_props) 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 end
Rails.logger.info(result_message) Rails.logger.info(result_message)
...@@ -295,7 +295,20 @@ class JiraService < IssueTrackerService ...@@ -295,7 +295,20 @@ class JiraService < IssueTrackerService
yield yield
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e 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 nil
end 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 end
...@@ -77,6 +77,14 @@ class KubernetesService < DeploymentService ...@@ -77,6 +77,14 @@ class KubernetesService < DeploymentService
] ]
end end
def actual_namespace
if namespace.present?
namespace
else
default_namespace
end
end
# Check we can connect to the Kubernetes API # Check we can connect to the Kubernetes API
def test(*args) def test(*args)
kubeclient = build_kubeclient! kubeclient = build_kubeclient!
...@@ -91,7 +99,7 @@ class KubernetesService < DeploymentService ...@@ -91,7 +99,7 @@ class KubernetesService < DeploymentService
variables = [ variables = [
{ key: 'KUBE_URL', value: api_url, public: true }, { key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false }, { 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? if ca_pem.present?
...@@ -110,7 +118,7 @@ class KubernetesService < DeploymentService ...@@ -110,7 +118,7 @@ class KubernetesService < DeploymentService
with_reactive_cache do |data| with_reactive_cache do |data|
pods = data.fetch(:pods, nil) pods = data.fetch(:pods, nil)
filter_pods(pods, app: environment.slug). 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) } each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end end
end end
...@@ -124,7 +132,7 @@ class KubernetesService < DeploymentService ...@@ -124,7 +132,7 @@ class KubernetesService < DeploymentService
# Store as hashes, rather than as third-party types # Store as hashes, rather than as third-party types
pods = begin pods = begin
kubeclient.get_pods(namespace: namespace).as_json kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err rescue KubeException => err
raise err unless err.error_code == 404 raise err unless err.error_code == 404
[] []
...@@ -142,20 +150,12 @@ class KubernetesService < DeploymentService ...@@ -142,20 +150,12 @@ class KubernetesService < DeploymentService
default_namespace || TEMPLATE_PLACEHOLDER default_namespace || TEMPLATE_PLACEHOLDER
end end
def namespace_variable
if namespace.present?
namespace
else
default_namespace
end
end
def default_namespace def default_namespace
"#{project.path}-#{project.id}" if project.present? "#{project.path}-#{project.id}" if project.present?
end end
def build_kubeclient!(api_path: 'api', api_version: 'v1') 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( ::Kubeclient::Client.new(
join_api_url(api_path), join_api_url(api_path),
......
...@@ -10,9 +10,12 @@ class User < ActiveRecord::Base ...@@ -10,9 +10,12 @@ class User < ActiveRecord::Base
include Sortable include Sortable
include CaseSensitivity include CaseSensitivity
include TokenAuthenticatable include TokenAuthenticatable
include IgnorableColumn
DEFAULT_NOTIFICATION_LEVEL = :participating DEFAULT_NOTIFICATION_LEVEL = :participating
ignore_column :authorized_projects_populated
add_authentication_token_field :authentication_token add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token add_authentication_token_field :incoming_email_token
add_authentication_token_field :rss_token add_authentication_token_field :rss_token
...@@ -218,7 +221,6 @@ class User < ActiveRecord::Base ...@@ -218,7 +221,6 @@ class User < ActiveRecord::Base
scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) } scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal } 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 :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 :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')) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
...@@ -368,7 +370,7 @@ class User < ActiveRecord::Base ...@@ -368,7 +370,7 @@ class User < ActiveRecord::Base
def reference_pattern def reference_pattern
%r{ %r{
#{Regexp.escape(reference_prefix)} #{Regexp.escape(reference_prefix)}
(?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}) (?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
}x }x
end end
...@@ -510,23 +512,16 @@ class User < ActiveRecord::Base ...@@ -510,23 +512,16 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})") Group.where("namespaces.id IN (#{union.to_sql})")
end end
def nested_groups # Returns a relation of groups the user has access to, including their parent
Group.member_descendants(id) # and child groups (recursively).
end
def all_expanded_groups def all_expanded_groups
Group.member_hierarchy(id) Gitlab::GroupHierarchy.new(groups).all_groups
end end
def expanded_groups_requiring_two_factor_authentication def expanded_groups_requiring_two_factor_authentication
all_expanded_groups.where(require_two_factor_authentication: true) all_expanded_groups.where(require_two_factor_authentication: true)
end end
def nested_groups_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id)
end
def refresh_authorized_projects def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute Users::RefreshAuthorizedProjectsService.new(self).execute
end end
...@@ -535,18 +530,15 @@ class User < ActiveRecord::Base ...@@ -535,18 +530,15 @@ class User < ActiveRecord::Base
project_authorizations.where(project_id: project_ids).delete_all project_authorizations.where(project_id: project_ids).delete_all
end 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) 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 = 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 projects
end end
...@@ -919,13 +911,13 @@ class User < ActiveRecord::Base ...@@ -919,13 +911,13 @@ class User < ActiveRecord::Base
end end
def assigned_open_merge_requests_count(force: false) 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 MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end end
end end
def assigned_open_issues_count(force: false) 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 IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end end
end end
......
...@@ -39,6 +39,7 @@ class MergeRequestEntity < IssuableEntity ...@@ -39,6 +39,7 @@ class MergeRequestEntity < IssuableEntity
expose :commits_count expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged expose :can_be_merged?, as: :can_be_merged
expose :remove_source_branch?, as: :remove_source_branch
expose :project_archived do |merge_request| expose :project_archived do |merge_request|
merge_request.project.archived? merge_request.project.archived?
......
...@@ -28,6 +28,7 @@ module Issues ...@@ -28,6 +28,7 @@ module Issues
notification_service.close_issue(issue, current_user) if notifications notification_service.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user) todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close') execute_hooks(issue, 'close')
invalidate_cache_counts(issue.assignees, issue)
end end
issue issue
......
...@@ -8,6 +8,7 @@ module Issues ...@@ -8,6 +8,7 @@ module Issues
create_note(issue) create_note(issue)
notification_service.reopen_issue(issue, current_user) notification_service.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen') execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue.assignees, issue)
end end
issue issue
......
...@@ -13,6 +13,7 @@ module MergeRequests ...@@ -13,6 +13,7 @@ module MergeRequests
notification_service.close_mr(merge_request, current_user) notification_service.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user) todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close') execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request.assignees, merge_request)
end end
merge_request merge_request
......
...@@ -13,6 +13,7 @@ module MergeRequests ...@@ -13,6 +13,7 @@ module MergeRequests
create_note(merge_request) create_note(merge_request)
notification_service.merge_mr(merge_request, current_user) notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge') execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request.assignees, merge_request)
end end
private private
......
...@@ -10,6 +10,7 @@ module MergeRequests ...@@ -10,6 +10,7 @@ module MergeRequests
execute_hooks(merge_request, 'reopen') execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user) merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
invalidate_cache_counts(merge_request.assignees, merge_request)
end end
merge_request merge_request
......
...@@ -12,7 +12,7 @@ class SearchService ...@@ -12,7 +12,7 @@ class SearchService
@project = @project =
if params[:project_id].present? if params[:project_id].present?
the_project = Project.find_by(id: params[:project_id]) 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 else
nil nil
end end
......
...@@ -73,12 +73,11 @@ module Users ...@@ -73,12 +73,11 @@ module Users
# remove - The IDs of the authorization rows to remove. # remove - The IDs of the authorization rows to remove.
# add - Rows to insert in the form `[user id, project id, access level]` # add - Rows to insert in the form `[user id, project id, access level]`
def update_authorizations(remove = [], add = []) 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.transaction do
user.remove_project_authorizations(remove) unless remove.empty? user.remove_project_authorizations(remove) unless remove.empty?
ProjectAuthorization.insert_authorizations(add) unless add.empty? ProjectAuthorization.insert_authorizations(add) unless add.empty?
user.set_authorized_projects_column
end end
# Since we batch insert authorization rows, Rails' associations may get # Since we batch insert authorization rows, Rails' associations may get
...@@ -101,38 +100,13 @@ module Users ...@@ -101,38 +100,13 @@ module Users
end end
def fresh_authorizations def fresh_authorizations
ProjectAuthorization. klass = if Group.supports_nested_groups?
unscoped. Gitlab::ProjectAuthorizations::WithNestedGroups
select('project_id, MAX(access_level) AS access_level'). else
from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}"). Gitlab::ProjectAuthorizations::WithoutNestedGroups
group(:project_id) end
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 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