Commit 25bd5413 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into build-chunks-on-object-storage

parents b025e89e 969b7c56
*.erb
lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
app/controllers/projects/approver_groups_controller.rb
app/controllers/projects/approvers_controller.rb
app/controllers/projects/protected_branches/merge_access_levels_controller.rb
app/controllers/projects/protected_branches/push_access_levels_controller.rb
app/controllers/projects/protected_tags/create_access_levels_controller.rb
app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb
app/models/project_services/packagist_service.rb
lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb
......@@ -19,6 +26,8 @@ ee/db/**/*
ee/app/serializers/ee/merge_request_widget_entity.rb
ee/lib/api/epics.rb
ee/lib/api/geo_nodes.rb
ee/lib/ee/api/group_boards.rb
ee/lib/ee/api/boards.rb
ee/lib/ee/gitlab/ldap/sync/admin_users.rb
ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb
ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb
......
......@@ -269,10 +269,10 @@ package-and-qa:
<<: *single-script-job-variables
SCRIPT_NAME: trigger-build-docs
environment:
name: review-docs/$CI_COMMIT_REF_NAME
name: review-docs/$CI_COMMIT_REF_SLUG
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
# Trigger a manual docs build in gitlab-docs only on non docs-only branches.
......@@ -298,9 +298,8 @@ review-docs-deploy:
- gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy
only:
- /(^docs[\/-].*|.*-docs$)/
- branches@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ee
- /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ce
- /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ee
<<: *except-qa
# Cleanup remote environment of gitlab-docs
......@@ -308,7 +307,7 @@ review-docs-cleanup:
<<: *review-docs
stage: post-cleanup
environment:
name: review-docs/$CI_COMMIT_REF_NAME
name: review-docs/$CI_COMMIT_REF_SLUG
action: stop
when: manual
script:
......@@ -326,11 +325,9 @@ cloud-native-image:
variables:
GIT_DEPTH: "1"
cache: {}
before_script:
- gem install gitlab --no-rdoc --no-ri
- chmod 755 ./scripts/trigger-build-cloud-native
script:
- ./scripts/trigger-build-cloud-native
- gem install gitlab --no-ri --no-rdoc
- ./trigger-build cng
only:
- tags@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ee
......
......@@ -10,10 +10,6 @@
Capybara/CurrentPathExpectation:
Enabled: false
# Offense count: 956
Capybara/FeatureMethods:
Enabled: false
# Offense count: 23
FactoryBot/DynamicAttributeDefinedStatically:
Exclude:
......
......@@ -2,6 +2,26 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 11.0.3 (2018-07-05)
### Fixed (14 changes, 1 of them is from the community)
- Revert merge request widget button max height. !20175 (George Tsiolis)
- Implement upload copy when moving an issue with upload on object storage. !20191
- Fix broken '!' support to autocomplete MRs in GFM fields. !20204
- Restore showing Elasticsearch and Geo status on dashboard. !20276
- Fix merge request page rendering error when its target/source branch is missing. !20280
- Fix sidebar collapse breapoints for job and wiki pages.
- fix size of code blocks in headings.
- Fix loading screen for search autocomplete dropdown.
- Fix ambiguous due_date column for Issue scopes.
- Always serve favicon from main GitLab domain so that CI badge can be drawn over it.
- Fix tooltip flickering bug.
- Fix refreshing cache keys for open issues count.
- Replace deprecated bs.affix in merge request tabs with sticky polyfill.
- Prevent pipeline job tooltip from scrolling off dropdown container.
## 11.0.2 (2018-06-26)
### Fixed (8 changes, 1 of them is from the community)
......
......@@ -47,7 +47,7 @@ gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-shibboleth', '~> 1.3.0'
gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.3'
......@@ -132,7 +132,7 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing
gem 'html-pipeline', '~> 2.7.1'
gem 'html-pipeline', '~> 2.8'
gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.4'
gem 'redcarpet', '~> 3.4'
......
......@@ -108,7 +108,7 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (1.2.1)
carrierwave (1.2.3)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
......@@ -394,7 +394,7 @@ GEM
hipchat (1.5.2)
httparty
mimemagic
html-pipeline (2.7.1)
html-pipeline (2.8.3)
activesupport (>= 2)
nokogiri (>= 1.4)
html2text (0.2.0)
......@@ -568,7 +568,7 @@ GEM
omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7)
omniauth-shibboleth (1.2.1)
omniauth-shibboleth (1.3.0)
omniauth (>= 1.0.0)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
......@@ -1061,7 +1061,7 @@ DEPENDENCIES
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
html-pipeline (~> 2.7.1)
html-pipeline (~> 2.8)
html2text
httparty (~> 0.13.3)
icalendar
......@@ -1101,7 +1101,7 @@ DEPENDENCIES
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0)
omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
......
......@@ -397,7 +397,7 @@ GEM
hipchat (1.5.2)
httparty
mimemagic
html-pipeline (2.7.1)
html-pipeline (2.8.3)
activesupport (>= 2)
nokogiri (>= 1.4)
html2text (0.2.0)
......@@ -572,7 +572,7 @@ GEM
omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7)
omniauth-shibboleth (1.2.1)
omniauth-shibboleth (1.3.0)
omniauth (>= 1.0.0)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
......@@ -1071,7 +1071,7 @@ DEPENDENCIES
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
html-pipeline (~> 2.7.1)
html-pipeline (~> 2.8)
html2text
httparty (~> 0.13.3)
icalendar
......@@ -1111,7 +1111,7 @@ DEPENDENCIES
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0)
omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
......
......@@ -120,6 +120,10 @@ All documentation can be found on [docs.gitlab.com/ce/](https://docs.gitlab.com/
Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on our website for the many options to get help.
## Why?
[Read here](https://about.gitlab.com/why/)
## Is it any good?
[Yes](https://news.ycombinator.com/item?id=3067434)
......
......@@ -28,16 +28,22 @@ export default {
},
},
methods: {
buildUpdateRequest(list) {
return {
add_label_ids: [list.label.id],
};
},
addIssues() {
const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.id);
const req = this.buildUpdateRequest(list);
// Post the data to the backend
gl.boardService.bulkUpdate(issueIds, {
add_label_ids: [list.label.id],
}).catch(() => {
gl.boardService
.bulkUpdate(issueIds, req)
.catch(() => {
Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach((issue) => {
......
......@@ -121,8 +121,7 @@
<div
v-if="issuesCount > 0 && issues.length === 0"
class="empty-state add-issues-empty-state-filter text-center">
<div
class="svg-content">
<div class="svg-content">
<img :src="emptyStateSvg" />
</div>
<div class="text-content">
......
......@@ -5,7 +5,7 @@
const Store = gl.issueBoards.BoardsStore;
export default {
export default Vue.extend({
props: {
issue: {
type: Object,
......@@ -25,19 +25,16 @@
removeIssue() {
const { issue } = this;
const lists = issue.getLists();
const listLabelIds = lists.map(list => list.label.id);
let labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
const req = this.buildPatchRequest(issue, lists);
const data = {
issue: {
label_ids: labelIds,
},
issue: this.seedPatchRequest(issue, req),
};
if (data.issue.label_ids.length === 0) {
data.issue.label_ids = [''];
}
// Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.'));
......@@ -54,8 +51,30 @@
Store.detail.issue = {};
},
},
/**
* Build the default patch request.
*/
buildPatchRequest(issue, lists) {
const listLabelIds = lists.map(list => list.label.id);
const labelIds = issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
return {
label_ids: labelIds,
};
},
/**
* Seed the given patch request.
*
* (This is overridden in EE)
*/
seedPatchRequest(issue, req) {
return req;
},
},
});
</script>
<template>
<div
......
......@@ -4,6 +4,7 @@
/* global ListAssignee */
import Vue from 'vue';
import '~/vue_shared/models/label';
import IssueProject from './project';
class ListIssue {
......
......@@ -7,6 +7,24 @@ import queryData from '../utils/query_data';
const PER_PAGE = 20;
const TYPES = {
backlog: {
isPreset: true,
isExpandable: true,
isBlank: false,
},
closed: {
isPreset: true,
isExpandable: true,
isBlank: false,
},
blank: {
isPreset: true,
isExpandable: false,
isBlank: true,
},
};
class List {
constructor(obj, defaultAvatar) {
this.id = obj.id;
......@@ -14,8 +32,10 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1;
this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1;
const typeInfo = this.getTypeInfo(this.type);
this.preset = !!typeInfo.isPreset;
this.isExpandable = !!typeInfo.isExpandable;
this.isExpanded = true;
this.page = 1;
this.loading = true;
......@@ -31,7 +51,7 @@ class List {
this.title = this.assignee.name;
}
if (this.type !== 'blank' && this.id) {
if (!typeInfo.isBlank && this.id) {
this.getIssues().catch(() => {
// TODO: handle request error
});
......@@ -107,7 +127,7 @@ class List {
return gl.boardService
.getIssuesForList(this.id, data)
.then(res => res.data)
.then((data) => {
.then(data => {
this.loading = false;
this.issuesSize = data.size;
......@@ -126,18 +146,7 @@ class List {
return gl.boardService
.newIssue(this.id, issue)
.then(res => res.data)
.then((data) => {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
}
});
.then(data => this.onNewIssueResponse(issue, data));
}
createIssues(data) {
......@@ -217,6 +226,25 @@ class List {
return !matchesRemove;
});
}
getTypeInfo (type) {
return TYPES[type] || {};
}
onNewIssueResponse (issue, data) {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
}
}
}
window.List = List;
export default List;
......@@ -2,6 +2,7 @@
import _ from 'underscore';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale';
......@@ -12,6 +13,7 @@ export default {
ClipboardButton,
EditButton,
Icon,
FileIcon,
},
directives: {
Tooltip,
......@@ -139,18 +141,20 @@ export default {
:name="collapseIcon"
:size="16"
aria-hidden="true"
class="diff-toggle-caret"
class="diff-toggle-caret append-right-5"
@click.stop="handleToggle"
/>
<a
ref="titleWrapper"
:href="titleLink"
class="append-right-4"
>
<i
:class="`fa-${icon}`"
class="fa fa-fw"
<file-icon
:file-name="filePath"
:size="18"
aria-hidden="true"
></i>
css-classes="js-file-icon append-right-5"
/>
<span v-if="diffFile.renamedFile">
<strong
v-tooltip
......
......@@ -24,19 +24,21 @@ export default {
...mapGetters(['commit']),
parallelDiffLines() {
return this.diffLines.map(line => {
const parallelLine = Object.assign({}, line);
if (line.left) {
Object.assign(line, { left: trimFirstCharOfLineContent(line.left) });
parallelLine.left = trimFirstCharOfLineContent(line.left);
} else {
Object.assign(line, { left: { type: EMPTY_CELL_TYPE } });
parallelLine.left = { type: EMPTY_CELL_TYPE };
}
if (line.right) {
Object.assign(line, { right: trimFirstCharOfLineContent(line.right) });
parallelLine.right = trimFirstCharOfLineContent(line.right);
} else {
Object.assign(line, { right: { type: EMPTY_CELL_TYPE } });
parallelLine.right = { type: EMPTY_CELL_TYPE };
}
return line;
return parallelLine;
});
},
diffLinesLength() {
......
......@@ -155,18 +155,21 @@ export function addContextLines(options) {
}
}
export function trimFirstCharOfLineContent(line) {
if (!line.richText) {
return line;
}
/**
* Trims the first char of the `richText` property when it's either a space or a diff symbol.
* @param {Object} line
* @returns {Object}
*/
export function trimFirstCharOfLineContent(line = {}) {
const parsedLine = Object.assign({}, line);
const firstChar = line.richText.charAt(0);
if (line.richText) {
const firstChar = parsedLine.richText.charAt(0);
if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
Object.assign(line, {
richText: line.richText.substring(1),
});
parsedLine.richText = line.richText.substring(1);
}
}
return line;
return parsedLine;
}
import $ from 'jquery';
import { sprintf, __ } from '~/locale';
import flash from '~/flash';
import { stripHtml } from '~/lib/utils/text_utility';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import router from '../../../ide_router';
......@@ -198,11 +197,18 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
if (err.response.status === 400) {
$('#ide-create-branch-modal').modal('show');
} else {
let errMsg = __('Error committing changes. Please try again.');
if (err.response.data && err.response.data.message) {
errMsg += ` (${stripHtml(err.response.data.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
dispatch(
'setErrorMessage',
{
text: __('An error accured whilst committing your changes.'),
action: () =>
dispatch('commitChanges').then(() =>
dispatch('setErrorMessage', null, { root: true }),
),
actionText: __('Please try again'),
},
{ root: true },
);
window.dispatchEvent(new Event('resize'));
}
......
import { __ } from '../../../../locale';
import Api from '../../../../api';
import flash from '../../../../flash';
import router from '../../../ide_router';
import { scopes } from './constants';
import * as types from './mutation_types';
......@@ -8,8 +7,20 @@ import * as rootTypes from '../../mutation_types';
export const requestMergeRequests = ({ commit }, type) =>
commit(types.REQUEST_MERGE_REQUESTS, type);
export const receiveMergeRequestsError = ({ commit }, type) => {
flash(__('Error loading merge requests.'));
export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => {
dispatch(
'setErrorMessage',
{
text: __('Error loading merge requests.'),
action: payload =>
dispatch('fetchMergeRequests', payload).then(() =>
dispatch('setErrorMessage', null, { root: true }),
),
actionText: __('Please try again'),
actionPayload: { type, search },
},
{ root: true },
);
commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type);
};
export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) =>
......@@ -22,7 +33,7 @@ export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, searc
Api.mergeRequests({ scope, state, search })
.then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
.catch(() => dispatch('receiveMergeRequestsError', type));
.catch(() => dispatch('receiveMergeRequestsError', { type, search }));
};
export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type);
......
import Visibility from 'visibilityjs';
import axios from 'axios';
import httpStatus from '../../../../lib/utils/http_status';
import { __ } from '../../../../locale';
import flash from '../../../../flash';
import Poll from '../../../../lib/utils/poll';
import service from '../../../services';
import { rightSidebarViews } from '../../../constants';
......@@ -18,10 +18,27 @@ export const stopPipelinePolling = () => {
export const restartPipelinePolling = () => {
if (eTagPoll) eTagPoll.restart();
};
export const forcePipelineRequest = () => {
if (eTagPoll) eTagPoll.makeRequest();
};
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
export const receiveLatestPipelineError = ({ commit, dispatch }) => {
flash(__('There was an error loading latest pipeline'));
export const receiveLatestPipelineError = ({ commit, dispatch }, err) => {
if (err.status !== httpStatus.NOT_FOUND) {
dispatch(
'setErrorMessage',
{
text: __('An error occured whilst fetching the latest pipline.'),
action: () =>
dispatch('forcePipelineRequest').then(() =>
dispatch('setErrorMessage', null, { root: true }),
),
actionText: __('Please try again'),
actionPayload: null,
},
{ root: true },
);
}
commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
dispatch('stopPipelinePolling');
};
......@@ -46,7 +63,7 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
method: 'lastCommitPipelines',
data: { getters: rootGetters },
successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data),
errorCallback: () => dispatch('receiveLatestPipelineError'),
errorCallback: err => dispatch('receiveLatestPipelineError', err),
});
if (!Visibility.hidden()) {
......@@ -63,9 +80,21 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
};
export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
export const receiveJobsError = ({ commit }, id) => {
flash(__('There was an error loading jobs'));
commit(types.RECEIVE_JOBS_ERROR, id);
export const receiveJobsError = ({ commit, dispatch }, stage) => {
dispatch(
'setErrorMessage',
{
text: __('An error occured whilst loading the pipelines jobs.'),
action: payload =>
dispatch('fetchJobs', payload).then(() =>
dispatch('setErrorMessage', null, { root: true }),
),
actionText: __('Please try again'),
actionPayload: stage,
},
{ root: true },
);
commit(types.RECEIVE_JOBS_ERROR, stage.id);
};
export const receiveJobsSuccess = ({ commit }, { id, data }) =>
commit(types.RECEIVE_JOBS_SUCCESS, { id, data });
......@@ -76,7 +105,7 @@ export const fetchJobs = ({ dispatch }, stage) => {
axios
.get(stage.dropdownPath)
.then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
.catch(() => dispatch('receiveJobsError', stage.id));
.catch(() => dispatch('receiveJobsError', stage));
};
export const toggleStageCollapsed = ({ commit }, stageId) =>
......@@ -90,8 +119,18 @@ export const setDetailJob = ({ commit, dispatch }, job) => {
};
export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
export const receiveJobTraceError = ({ commit }) => {
flash(__('Error fetching job trace'));
export const receiveJobTraceError = ({ commit, dispatch }) => {
dispatch(
'setErrorMessage',
{
text: __('An error occured whilst fetching the job trace.'),
action: () =>
dispatch('fetchJobTrace').then(() => dispatch('setErrorMessage', null, { root: true })),
actionText: __('Please try again'),
actionPayload: null,
},
{ root: true },
);
commit(types.RECEIVE_JOB_TRACE_ERROR);
};
export const receiveJobTraceSuccess = ({ commit }, data) =>
......
......@@ -5,6 +5,7 @@
import $ from 'jquery';
import _ from 'underscore';
import { __ } from '~/locale';
import '~/gl_dropdown';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store';
......@@ -251,3 +252,5 @@ export default class MilestoneSelect {
});
}
}
window.MilestoneSelect = MilestoneSelect;
<script>
import _ from 'underscore';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
......@@ -13,6 +15,7 @@ export default {
Graph,
GraphGroup,
EmptyState,
Icon,
},
props: {
hasMetrics: {
......@@ -80,6 +83,14 @@ export default {
type: String,
required: true,
},
environmentsEndpoint: {
type: String,
required: true,
},
currentEnvironmentName: {
type: String,
required: true,
},
},
data() {
return {
......@@ -96,6 +107,7 @@ export default {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
......@@ -122,7 +134,11 @@ export default {
this.service
.getDeploymentData()
.then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')),
.catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))),
this.service
.getEnvironmentsData()
.then((data) => this.store.storeEnvironmentsData(data))
.catch(() => Flash(s__('Metrics|There was an error getting environments information.'))),
])
.then(() => {
if (this.store.groups.length < 1) {
......@@ -155,8 +171,41 @@ export default {
<template>
<div
v-if="!showEmptyState"
class="prometheus-graphs"
class="prometheus-graphs prepend-top-10"
>
<div class="environments d-flex align-items-center">
{{ s__('Metrics|Environment') }}
<div class="dropdown prepend-left-10">
<button
class="dropdown-menu-toggle"
data-toggle="dropdown"
type="button"
>
<span>
{{ currentEnvironmentName }}
</span>
<icon
name="chevron-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li
v-for="environment in store.environmentsData"
:key="environment.latest.id"
>
<a
:href="environment.latest.metrics_path"
:class="{ 'is-active': environment.latest.name == currentEnvironmentName }"
class="dropdown-item"
>
{{ environment.latest.name }}
</a>
</li>
</ul>
</div>
</div>
</div>
<graph-group
v-for="(groupData, index) in store.groups"
:key="index"
......
import axios from '../../lib/utils/axios_utils';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
const MAX_REQUESTS = 3;
......@@ -23,9 +24,10 @@ function backOffRequest(makeRequestCallback) {
}
export default class MonitoringService {
constructor({ metricsEndpoint, deploymentEndpoint }) {
constructor({ metricsEndpoint, deploymentEndpoint, environmentsEndpoint }) {
this.metricsEndpoint = metricsEndpoint;
this.deploymentEndpoint = deploymentEndpoint;
this.environmentsEndpoint = environmentsEndpoint;
}
getGraphsData() {
......@@ -33,7 +35,7 @@ export default class MonitoringService {
.then(resp => resp.data)
.then((response) => {
if (!response || !response.data) {
throw new Error('Unexpected metrics data response from prometheus endpoint');
throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
}
return response.data;
});
......@@ -47,9 +49,20 @@ export default class MonitoringService {
.then(resp => resp.data)
.then((response) => {
if (!response || !response.deployments) {
throw new Error('Unexpected deployment data response from prometheus endpoint');
throw new Error(s__('Metrics|Unexpected deployment data response from prometheus endpoint'));
}
return response.deployments;
});
}
getEnvironmentsData() {
return axios.get(this.environmentsEndpoint)
.then(resp => resp.data)
.then((response) => {
if (!response || !response.environments) {
throw new Error(s__('Metrics|There was an error fetching the environments data, please try again'));
}
return response.environments;
});
}
}
......@@ -24,6 +24,7 @@ export default class MonitoringStore {
constructor() {
this.groups = [];
this.deploymentData = [];
this.environmentsData = [];
}
storeMetrics(groups = []) {
......@@ -37,6 +38,10 @@ export default class MonitoringStore {
this.deploymentData = deploymentData;
}
storeEnvironmentsData(environmentsData = []) {
this.environmentsData = environmentsData;
}
getMetricsCount() {
return this.groups.reduce((count, group) => count + group.metrics.length, 0);
}
......
<script>
import { mapState, mapActions } from 'vuex';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
import { mapState, mapActions } from 'vuex';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
export default {
export default {
components: {
DiffFileHeader,
SkeletonLoadingContainer,
......@@ -27,7 +27,8 @@ export default {
noteableData: state => state.notes.noteableData,
}),
hasTruncatedDiffLines() {
return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
return this.discussion.truncatedDiffLines &&
this.discussion.truncatedDiffLines.length !== 0;
},
isDiscussionsExpanded() {
return true; // TODO: @fatihacet - Fix this.
......@@ -55,9 +56,13 @@ export default {
return window.gon.user_color_scheme;
},
normalizedDiffLines() {
const lines = this.discussion.truncatedDiffLines || [];
if (this.discussion.truncatedDiffLines) {
return this.discussion.truncatedDiffLines.map(line =>
trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)),
);
}
return lines.map(line => trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)));
return [];
},
},
mounted() {
......@@ -83,7 +88,7 @@ export default {
});
},
},
};
};
</script>
<template>
......
......@@ -85,9 +85,9 @@ export const allDiscussions = (state, getters) => {
export const resolvedDiscussionsById = state => {
const map = {};
state.discussions.forEach(n => {
state.discussions.filter(d => d.resolvable).forEach(n => {
if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system);
const resolved = n.notes.filter(note => note.resolvable).every(note => note.resolved);
if (resolved) {
map[n.id] = n;
......
......@@ -39,6 +39,7 @@ export default class Todos {
}
initFilters() {
this.initFilterDropdown($('.js-group-search'), 'group_id', ['text']);
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-action-search'), 'action_id');
......@@ -53,7 +54,16 @@ export default class Todos {
filterable: searchFields ? true : false,
search: { fields: searchFields },
data: $dropdown.data('data'),
clicked: () => $dropdown.closest('form.filter-form').submit(),
clicked: () => {
const $formEl = $dropdown.closest('form.filter-form');
const mutexDropdowns = {
group_id: 'project_id',
project_id: 'group_id',
};
$formEl.find(`input[name="${mutexDropdowns[fieldName]}"]`).remove();
$formEl.submit();
},
});
}
......
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
gcpSignupOffer();
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
gcpSignupOffer();
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => {
const { page } = document.body.dataset;
const newClusterViews = [
'projects:clusters:new',
'projects:clusters:create_gcp',
'projects:clusters:create_user',
];
if (newClusterViews.indexOf(page) > -1) {
gcpSignupOffer();
initGkeDropdowns();
}
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
});
import initTerminal from '~/terminal/';
document.addEventListener('DOMContentLoaded', initTerminal);
......@@ -20,6 +20,7 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project');
......
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
const MARK_TEXT = __('Mark todo as done');
const TODO_TEXT = __('Add todo');
export default {
directives: {
tooltip,
},
components: {
Icon,
LoadingIcon,
},
props: {
issuableId: {
type: Number,
required: true,
},
issuableType: {
type: String,
required: true,
},
isTodo: {
type: Boolean,
required: false,
default: true,
},
isActionActive: {
type: Boolean,
required: false,
default: false,
},
collapsed: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
buttonClasses() {
return this.collapsed ?
'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' :
'btn btn-default btn-todo issuable-header-btn float-right';
},
buttonLabel() {
return this.isTodo ? MARK_TEXT : TODO_TEXT;
},
collapsedButtonIconClasses() {
return this.isTodo ? 'todo-undone' : '';
},
collapsedButtonIcon() {
return this.isTodo ? 'todo-done' : 'todo-add';
},
},
methods: {
handleButtonClick() {
this.$emit('toggleTodo');
},
},
};
</script>
<template>
<button
v-tooltip
:class="buttonClasses"
:title="buttonLabel"
:aria-label="buttonLabel"
:data-issuable-id="issuableId"
:data-issuable-type="issuableType"
type="button"
data-container="body"
data-placement="left"
data-boundary="viewport"
@click="handleButtonClick"
>
<icon
v-show="collapsed"
:css-classes="collapsedButtonIconClasses"
:name="collapsedButtonIcon"
/>
<span
v-show="!collapsed"
class="issuable-todo-inner"
>
{{ buttonLabel }}
</span>
<loading-icon
v-show="isActionActive"
:inline="true"
/>
</button>
</template>
......@@ -29,8 +29,8 @@
methods: {
isValid(form) {
return !form ||
form.find('.js-vue-markdown-field').length ||
$(this.$el).closest('form') === form[0];
form.find('.js-vue-markdown-field').length &&
$(this.$el).closest('form')[0] === form[0];
},
previewMarkdownTab(event, form) {
......
......@@ -12,6 +12,11 @@ export default {
type: Boolean,
required: true,
},
cssClasses: {
type: String,
required: false,
default: '',
},
},
computed: {
tooltipLabel() {
......@@ -30,10 +35,12 @@ export default {
<button
v-tooltip
:title="tooltipLabel"
:class="cssClasses"
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
data-container="body"
data-placement="left"
data-boundary="viewport"
@click="toggle"
>
<i
......
......@@ -15,7 +15,7 @@
}
svg {
vertical-align: text-bottom;
vertical-align: middle;
}
}
......
......@@ -222,6 +222,23 @@
}
}
.prometheus-graphs {
.environments {
.dropdown-menu-toggle {
svg {
position: absolute;
right: 5%;
top: 25%;
}
}
.dropdown-menu-toggle,
.dropdown-menu {
width: 240px;
}
}
}
.environments-actions {
.external-url,
.monitoring-url,
......
......@@ -449,6 +449,7 @@
.todo-undone {
color: $gl-link-color;
fill: $gl-link-color;
}
.author {
......
......@@ -191,6 +191,22 @@
}
}
.initialize-with-readme-setting {
.form-check {
margin-bottom: 10px;
.option-title {
font-weight: $gl-font-weight-normal;
display: inline-block;
color: $gl-text-color;
}
.option-description {
color: $project-option-descr-color;
}
}
}
.prometheus-metrics-monitoring {
.card {
.card-toggle {
......
......@@ -174,6 +174,18 @@
}
}
@include media-breakpoint-down(lg) {
.todos-filters {
.filter-categories {
width: 75%;
.filter-item {
margin-bottom: 10px;
}
}
}
}
@include media-breakpoint-down(xs) {
.todo {
.avatar {
......@@ -199,6 +211,10 @@
}
.todos-filters {
.filter-categories {
width: auto;
}
.dropdown-menu-toggle {
width: 100%;
}
......
module TodosActions
extend ActiveSupport::Concern
def create
todo = TodoService.new.mark_todo(issuable, current_user)
render json: {
count: TodosFinder.new(current_user, state: :pending).execute.count,
delete_path: dashboard_todo_path(todo)
}
end
end
......@@ -45,6 +45,16 @@ module UploadsActions
send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
def authorize
set_workhorse_internal_api_content_type
authorized = uploader_class.workhorse_authorize(
has_length: false,
maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i)
render json: authorized
end
private
# Explicitly set the format.
......
......@@ -70,7 +70,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def todo_params
params.permit(:action_id, :author_id, :project_id, :type, :sort, :state)
params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
end
def redirect_out_of_range(todos)
......
class Groups::UploadsController < Groups::ApplicationController
include UploadsActions
include WorkhorseRequest
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
before_action :authorize_upload_file!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize]
private
......
class Projects::Clusters::GcpController < Projects::ApplicationController
before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:new, :create]
before_action :authorize_google_api, except: :login
helper_method :token_in_session
def login
begin
state = generate_session_key_redirect(gcp_new_namespace_project_clusters_path.to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
state: state).authorize_url
rescue GoogleApi::Auth::ConfigMissingError
# no-op
end
end
def new
@cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end
def create
@cluster = ::Clusters::CreateService
.new(project, current_user, create_params)
.execute(token_in_session)
if @cluster.persisted?
redirect_to project_cluster_path(project, @cluster)
else
render :new
end
end
private
def create_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
]).merge(
provider_type: :gcp,
platform_type: :kubernetes
)
end
def authorize_google_api
unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
redirect_to action: 'login'
end
end
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
@expires_at_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
def generate_session_key_redirect(uri)
GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
session[key] = uri
end
end
end
class Projects::Clusters::UserController < Projects::ApplicationController
before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:new, :create]
def new
@cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
end
end
def create
@cluster = ::Clusters::CreateService
.new(project, current_user, create_params)
.execute
if @cluster.persisted?
redirect_to project_cluster_path(project, @cluster)
else
render :new
end
end
private
def create_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
platform_kubernetes_attributes: [
:namespace,
:api_url,
:token,
:ca_cert
]).merge(
provider_type: :user,
platform_type: :kubernetes
)
end
end
class Projects::ClustersController < Projects::ApplicationController
before_action :cluster, except: [:index, :new]
before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
before_action :authorize_read_cluster!
before_action :generate_gcp_authorize_url, only: [:new]
before_action :validate_gcp_token, only: [:new]
before_action :gcp_cluster, only: [:new]
before_action :user_cluster, only: [:new]
before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:status]
helper_method :token_in_session
STATUS_POLLING_INTERVAL = 10_000
......@@ -64,6 +69,38 @@ class Projects::ClustersController < Projects::ApplicationController
end
end
def create_gcp
@gcp_cluster = ::Clusters::CreateService
.new(project, current_user, create_gcp_cluster_params)
.execute(token_in_session)
if @gcp_cluster.persisted?
redirect_to project_cluster_path(project, @gcp_cluster)
else
generate_gcp_authorize_url
validate_gcp_token
user_cluster
render :new, locals: { active_tab: 'gcp' }
end
end
def create_user
@user_cluster = ::Clusters::CreateService
.new(project, current_user, create_user_cluster_params)
.execute(token_in_session)
if @user_cluster.persisted?
redirect_to project_cluster_path(project, @user_cluster)
else
generate_gcp_authorize_url
validate_gcp_token
gcp_cluster
render :new, locals: { active_tab: 'user' }
end
end
private
def cluster
......@@ -95,6 +132,80 @@ class Projects::ClustersController < Projects::ApplicationController
end
end
def create_gcp_cluster_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
]).merge(
provider_type: :gcp,
platform_type: :kubernetes
)
end
def create_user_cluster_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
platform_kubernetes_attributes: [
:namespace,
:api_url,
:token,
:ca_cert
]).merge(
provider_type: :user,
platform_type: :kubernetes
)
end
def generate_gcp_authorize_url
state = generate_session_key_redirect(new_project_cluster_path(@project).to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
state: state).authorize_url
rescue GoogleApi::Auth::ConfigMissingError
# no-op
end
def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end
def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
end
end
def validate_gcp_token
@valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
end
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
@expires_at_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
def generate_session_key_redirect(uri)
GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
session[key] = uri
end
end
def authorize_update_cluster!
access_denied! unless can?(current_user, :update_cluster, cluster)
end
......
......@@ -120,6 +120,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
def metrics_redirect
environment = project.default_environment
if environment
redirect_to environment_metrics_path(environment)
else
render :empty
end
end
def metrics
# Currently, this acts as a hint to load the metrics details into the cache
# if they aren't there already
......
......@@ -2,11 +2,12 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!,
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_read_build!
before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_workhorse_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
layout 'project'
......@@ -44,12 +45,10 @@ class Projects::JobsController < Projects::ApplicationController
end
def show
@builds = @project.pipelines
.find_by_sha(@build.sha)
.builds
@pipeline = @build.pipeline
@builds = @pipeline.builds
.order('id DESC')
.present(current_user: current_user)
@pipeline = @build.pipeline
respond_to do |format|
format.html
......@@ -136,6 +135,15 @@ class Projects::JobsController < Projects::ApplicationController
end
end
def terminal
end
# GET .../terminal.ws : implemented in gitlab-workhorse
def terminal_websocket_authorize
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(@build.terminal_specification)
end
private
def authorize_update_build!
......@@ -146,6 +154,14 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :erase_build, build)
end
def authorize_use_build_terminal!
return access_denied! unless can?(current_user, :create_build_terminal, build)
end
def verify_api_request!
Gitlab::Workhorse.verify_api_request!(request.headers)
end
def raw_send_params
{ type: 'text/plain; charset=utf-8', disposition: 'inline' }
end
......
......@@ -13,7 +13,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
@pipelines = PipelinesFinder
.new(project, scope: @scope)
.new(project, current_user, scope: @scope)
.execute
.page(params[:page])
.per(30)
......@@ -178,7 +178,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def limited_pipelines_count(project, scope = nil)
finder = PipelinesFinder.new(project, scope: scope)
finder = PipelinesFinder.new(project, current_user, scope: scope)
view_context.limited_counter_with_delimiter(finder.execute)
end
......
......@@ -74,7 +74,7 @@ module Projects
.ordered
.page(params[:page]).per(20)
@shared_runners = ::Ci::Runner.shared.active
@shared_runners = ::Ci::Runner.instance_type.active
@shared_runners_count = @shared_runners.count(:all)
......
class Projects::TodosController < Projects::ApplicationController
before_action :authenticate_user!, only: [:create]
def create
todo = TodoService.new.mark_todo(issuable, current_user)
include Gitlab::Utils::StrongMemoize
include TodosActions
render json: {
count: TodosFinder.new(current_user, state: :pending).execute.count,
delete_path: dashboard_todo_path(todo)
}
end
before_action :authenticate_user!, only: [:create]
private
def issuable
@issuable ||= begin
strong_memoize(:issuable) do
case params[:issuable_type]
when "issue"
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
......
class Projects::UploadsController < Projects::ApplicationController
include UploadsActions
include WorkhorseRequest
# These will kick you out if you don't have access.
skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
before_action :authorize_upload_file!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize]
private
......
......@@ -347,6 +347,7 @@ class ProjectsController < Projects::ApplicationController
:visibility_level,
:template_name,
:merge_method,
:initialize_with_readme,
project_feature_attributes: %i[
builds_access_level
......
class PipelinesFinder
attr_reader :project, :pipelines, :params
attr_reader :project, :pipelines, :params, :current_user
ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze
def initialize(project, params = {})
def initialize(project, current_user, params = {})
@project = project
@current_user = current_user
@pipelines = project.pipelines
@params = params
end
def execute
unless Ability.allowed?(current_user, :read_pipeline, project)
return Ci::Pipeline.none
end
items = pipelines
items = by_scope(items)
items = by_status(items)
......
......@@ -15,6 +15,7 @@
class TodosFinder
prepend FinderWithCrossProjectAccess
include FinderMethods
include Gitlab::Utils::StrongMemoize
requires_cross_project_access unless: -> { project? }
......@@ -34,9 +35,11 @@ class TodosFinder
items = by_author(items)
items = by_state(items)
items = by_type(items)
items = by_group(items)
# Filtering by project HAS TO be the last because we use
# the project IDs yielded by the todos query thus far
items = by_project(items)
items = visible_to_user(items)
sort(items)
end
......@@ -82,6 +85,10 @@ class TodosFinder
params[:project_id].present?
end
def group?
params[:group_id].present?
end
def project
return @project if defined?(@project)
......@@ -100,18 +107,14 @@ class TodosFinder
@project
end
def project_ids(items)
ids = items.except(:order).select(:project_id)
if Gitlab::Database.mysql?
# To make UPDATE work on MySQL, wrap it in a SELECT with an alias
ids = Todo.except(:order).select('*').from("(#{ids.to_sql}) AS t")
def group
strong_memoize(:group) do
Group.find(params[:group_id])
end
ids
end
def type?
type.present? && %w(Issue MergeRequest).include?(type)
type.present? && %w(Issue MergeRequest Epic).include?(type)
end
def type
......@@ -148,12 +151,37 @@ class TodosFinder
def by_project(items)
if project?
items.where(project: project)
else
projects = Project.public_or_visible_to_user(current_user)
items = items.where(project: project)
end
items
end
def by_group(items)
if group?
groups = group.self_and_descendants
items = items.where(
'project_id IN (?) OR group_id IN (?)',
Project.where(group: groups).select(:id),
groups.select(:id)
)
end
items.joins(:project).merge(projects)
items
end
def visible_to_user(items)
projects = Project.public_or_visible_to_user(current_user)
groups = Group.public_or_visible_to_user(current_user)
items
.joins('LEFT JOIN namespaces ON namespaces.id = todos.group_id')
.joins('LEFT JOIN projects ON projects.id = todos.project_id')
.where(
'project_id IN (?) OR group_id IN (?)',
projects.select(:id),
groups.select(:id)
)
end
def by_state(items)
......
......@@ -2,7 +2,10 @@ class GitlabSchema < GraphQL::Schema
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present
use Gitlab::Graphql::Connections
query(Types::QueryType)
default_max_page_size 100
# mutation(Types::MutationType)
end
module ResolvesPipelines
extend ActiveSupport::Concern
included do
type [Types::Ci::PipelineType], null: false
argument :status,
Types::Ci::PipelineStatusEnum,
required: false,
description: "Filter pipelines by their status"
argument :ref,
GraphQL::STRING_TYPE,
required: false,
description: "Filter pipelines by the ref they are run for"
argument :sha,
GraphQL::STRING_TYPE,
required: false,
description: "Filter pipelines by the sha of the commit they are run for"
end
def resolve_pipelines(project, params = {})
PipelinesFinder.new(project, context[:current_user], params).execute
end
end
module Resolvers
class MergeRequestPipelinesResolver < BaseResolver
include ::ResolvesPipelines
alias_method :merge_request, :object
def resolve(**args)
resolve_pipelines(project, args)
.merge(merge_request.all_pipelines)
end
def project
merge_request.source_project
end
end
end
module Resolvers
class ProjectPipelinesResolver < BaseResolver
include ResolvesPipelines
alias_method :project, :object
def resolve(**args)
resolve_pipelines(project, args)
end
end
end
module Types
module Ci
class PipelineStatusEnum < BaseEnum
::Ci::Pipeline.all_state_names.each do |state_symbol|
value state_symbol.to_s.upcase, value: state_symbol.to_s
end
end
end
end
module Types
module Ci
class PipelineType < BaseObject
expose_permissions Types::PermissionTypes::Ci::Pipeline
graphql_name 'Pipeline'
field :id, GraphQL::ID_TYPE, null: false
field :iid, GraphQL::ID_TYPE, null: false
field :sha, GraphQL::STRING_TYPE, null: false
field :before_sha, GraphQL::STRING_TYPE, null: true
field :status, PipelineStatusEnum, null: false
field :duration,
GraphQL::INT_TYPE,
null: true,
description: "Duration of the pipeline in seconds"
field :coverage,
GraphQL::FLOAT_TYPE,
null: true,
description: "Coverage percentage"
field :created_at, Types::TimeType, null: false
field :updated_at, Types::TimeType, null: false
field :started_at, Types::TimeType, null: true
field :finished_at, Types::TimeType, null: true
field :committed_at, Types::TimeType, null: true
# TODO: Add triggering user as a type
end
end
end
......@@ -45,5 +45,11 @@ module Types
field :upvotes, GraphQL::INT_TYPE, null: false
field :downvotes, GraphQL::INT_TYPE, null: false
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline do
authorize :read_pipeline
end
field :pipelines, Types::Ci::PipelineType.connection_type,
resolver: Resolvers::MergeRequestPipelinesResolver
end
end
module Types
module PermissionTypes
module Ci
class Pipeline < BasePermissionType
graphql_name 'PipelinePermissions'
abilities :update_pipeline, :admin_pipeline, :destroy_pipeline
end
end
end
end
......@@ -70,5 +70,10 @@ module Types
resolver: Resolvers::MergeRequestResolver do
authorize :read_merge_request
end
field :pipelines,
Types::Ci::PipelineType.connection_type,
null: false,
resolver: Resolvers::ProjectPipelinesResolver
end
end
......@@ -122,7 +122,7 @@ module CiStatusHelper
def no_runners_for_project?(project)
project.runners.blank? &&
Ci::Runner.shared.blank?
Ci::Runner.instance_type.blank?
end
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body')
......
......@@ -131,6 +131,19 @@ module IssuablesHelper
end
end
def group_dropdown_label(group_id, default_label)
return default_label if group_id.nil?
return "Any group" if group_id == "0"
group = ::Group.find_by(id: group_id)
if group
group.full_name
else
default_label
end
end
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
title =
case milestone_title
......
......@@ -43,7 +43,7 @@ module TodosHelper
project_commit_path(todo.project,
todo.target, anchor: anchor)
else
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
path = [todo.parent, todo.target]
path.unshift(:pipelines) if todo.build_failed?
......@@ -167,4 +167,12 @@ module TodosHelper
def show_todo_state?(todo)
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
end
def todo_group_options
groups = current_user.authorized_groups.map do |group|
{ id: group.id, text: group.full_name }
end
groups.unshift({ id: '', text: 'Any Group' }).to_json
end
end
......@@ -26,4 +26,8 @@ class Board < ActiveRecord::Base
def closed_list
lists.merge(List.closed).take
end
def scoped?
false
end
end
......@@ -27,7 +27,13 @@ module Ci
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
accepts_nested_attributes_for :runner_session
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
##
......@@ -174,6 +180,10 @@ module Ci
after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state
end
after_transition running: any do |build|
Ci::BuildRunnerSession.where(build: build).delete_all
end
end
def ensure_metadata
......@@ -584,6 +594,10 @@ module Ci
super(options).merge(when: read_attribute(:when))
end
def has_terminal?
running? && runner_session_url.present?
end
private
def update_artifacts_size
......
module Ci
# The purpose of this class is to store Build related runner session.
# Data will be removed after transitioning from running to any state.
class BuildRunnerSession < ActiveRecord::Base
extend Gitlab::Ci::Model
self.table_name = 'ci_builds_runner_session'
belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session
validates :build, presence: true
validates :url, url: { protocols: %w(https) }
def terminal_specification
return {} unless url.present?
{
subprotocols: ['terminal.gitlab.com'].freeze,
url: "#{url}/exec".sub("https://", "wss://"),
headers: { Authorization: authorization.presence }.compact,
ca_pem: certificate.presence
}
end
end
end
......@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern
include IgnorableColumn
include RedisCacheable
include ChronicDurationAttribute
......@@ -11,6 +12,8 @@ module Ci
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
ignore_column :is_shared
has_many :builds
has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects
......@@ -21,13 +24,16 @@ module Ci
before_validation :set_default_values
scope :specific, -> { where(is_shared: false) }
scope :shared, -> { where(is_shared: true) }
scope :active, -> { where(active: true) }
scope :paused, -> { where(active: false) }
scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
scope :ordered, -> { order(id: :desc) }
# BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb`
scope :deprecated_shared, -> { instance_type }
# this should get replaced with `project_type.or(group_type)` once using Rails5
scope :deprecated_specific, -> { where(runner_type: [runner_types[:project_type], runner_types[:group_type]]) }
scope :belonging_to_project, -> (project_id) {
joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
}
......@@ -39,9 +45,9 @@ module Ci
joins(:groups).where(namespaces: { id: hierarchy_groups })
}
scope :owned_or_shared, -> (project_id) do
scope :owned_or_instance_wide, -> (project_id) do
union = Gitlab::SQL::Union.new(
[belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared],
[belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), instance_type],
remove_duplicates: false
)
from("(#{union.to_sql}) ci_runners")
......@@ -63,7 +69,6 @@ module Ci
validate :no_groups, unless: :group_type?
validate :any_project, if: :project_type?
validate :exactly_one_group, if: :group_type?
validate :validate_is_shared
acts_as_taggable
......@@ -113,8 +118,7 @@ module Ci
end
def assign_to(project, current_user = nil)
if shared?
self.is_shared = false if shared?
if instance_type?
self.runner_type = :project_type
elsif group_type?
raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
......@@ -137,10 +141,6 @@ module Ci
description
end
def shared?
is_shared
end
def online?
contacted_at && contacted_at > self.class.contact_time_deadline
end
......@@ -159,10 +159,6 @@ module Ci
runner_projects.count == 1
end
def specific?
!shared?
end
def assigned_to_group?
runner_namespaces.any?
end
......@@ -260,7 +256,7 @@ module Ci
end
def assignable_for?(project_id)
self.class.owned_or_shared(project_id).where(id: self.id).any?
self.class.owned_or_instance_wide(project_id).where(id: self.id).any?
end
def no_projects
......@@ -287,12 +283,6 @@ module Ci
end
end
def validate_is_shared
unless is_shared? == instance_type?
errors.add(:is_shared, 'is not equal to instance_type?')
end
end
def accepting_tags?(build)
(run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
end
......
......@@ -243,6 +243,12 @@ module Issuable
opened?
end
def overdue?
return false unless respond_to?(:due_date)
due_date.try(:past?) || false
end
def user_notes_count
if notes.loaded?
# Use the in-memory association to select and count to avoid hitting the db
......
......@@ -39,6 +39,8 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
has_many :todos
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
......@@ -82,6 +84,12 @@ class Group < Namespace
where(id: user.authorized_groups.select(:id).reorder(nil))
end
def public_or_visible_to_user(user)
where('id IN (?) OR namespaces.visibility_level IN (?)',
user.authorized_groups.select(:id),
Gitlab::VisibilityLevel.levels_for_user(user))
end
def select_for_project_authorization
if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
......
......@@ -275,10 +275,6 @@ class Issue < ActiveRecord::Base
user ? readable_by?(user) : publicly_visible?
end
def overdue?
due_date.try(:past?) || false
end
def check_for_spam?
project.public? && (title_changed? || description_changed?)
end
......
......@@ -229,6 +229,10 @@ class Note < ActiveRecord::Base
!for_personal_snippet?
end
def for_issuable?
for_issue? || for_merge_request?
end
def skip_project_check?
!for_project_noteable?
end
......
......@@ -1422,7 +1422,7 @@ class Project < ActiveRecord::Base
end
def shared_runners
@shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
@shared_runners ||= shared_runners_available? ? Ci::Runner.instance_type : Ci::Runner.none
end
def group_runners
......@@ -1774,6 +1774,15 @@ class Project < ActiveRecord::Base
end
end
def default_environment
production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC"
environments
.with_state(:available)
.reorder(production_first)
.first
end
def secret_variables_for(ref:, environment: nil)
# EE would use the environment
if protected_for?(ref)
......
......@@ -240,7 +240,7 @@ class KubernetesService < DeploymentService
end
def deprecation_validation
return if active_changed?(from: true, to: false)
return if active_changed?(from: true, to: false) || (new_record? && !active?)
if deprecated?
errors[:base] << deprecation_message
......
......@@ -281,9 +281,9 @@ class Service < ActiveRecord::Base
def self.build_from_template(project_id, template)
service = template.dup
service.active = false unless service.valid?
service.template = false
service.project_id = project_id
service.active = false if service.active? && !service.valid?
service
end
......
......@@ -22,15 +22,18 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
belongs_to :group
belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :action, :project, :target_type, :user, presence: true
validates :action, :target_type, :user, presence: true
validates :author, presence: true
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
validates :project, presence: true, unless: :group_id
validates :group, presence: true, unless: :project_id
scope :pending, -> { with_state(:pending) }
scope :done, -> { with_state(:done) }
......@@ -44,7 +47,7 @@ class Todo < ActiveRecord::Base
state :done
end
after_save :keep_around_commit
after_save :keep_around_commit, if: :commit_id
class << self
# Priority sorting isn't displayed in the dropdown, because we don't show
......@@ -79,6 +82,10 @@ class Todo < ActiveRecord::Base
end
end
def parent
project
end
def unmergeable?
action == UNMERGEABLE
end
......
......@@ -1032,7 +1032,7 @@ class User < ActiveRecord::Base
union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])
Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Ci::Runner.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
end
......
......@@ -18,6 +18,10 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
end
condition(:terminal, scope: :subject) do
@subject.has_terminal?
end
rule { protected_ref }.policy do
prevent :update_build
prevent :erase_build
......@@ -29,5 +33,7 @@ module Ci
enable :update_build
enable :update_commit_status
end
rule { can?(:update_build) & terminal }.enable :create_build_terminal
end
end
......@@ -25,6 +25,8 @@ class DiffFileEntity < Grape::Entity
expose :can_modify_blob do |diff_file|
merge_request = options[:merge_request]
next unless diff_file.blob
if merge_request&.source_project && current_user
can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
else
......@@ -108,6 +110,7 @@ class DiffFileEntity < Grape::Entity
project = merge_request.target_project
next unless project
next unless diff_file.content_sha
project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
end
......@@ -125,6 +128,8 @@ class DiffFileEntity < Grape::Entity
end
expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
next unless diff_file.content_sha
project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path))
end
......
......@@ -3,7 +3,7 @@ class DiscussionEntity < Grape::Entity
include NotesHelper
expose :id, :reply_id
expose :position, if: -> (d, _) { d.diff_discussion? }
expose :position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? }
expose :line_code, if: -> (d, _) { d.diff_discussion? }
expose :expanded?, as: :expanded
expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
......
......@@ -4,7 +4,7 @@ class RunnerEntity < Grape::Entity
expose :id, :description
expose :edit_path,
if: -> (*) { can?(request.current_user, :admin_build, project) && runner.specific? } do |runner|
if: -> (*) { can?(request.current_user, :admin_build, project) && runner.project_type? } do |runner|
edit_project_runner_path(project, runner)
end
......
......@@ -13,9 +13,9 @@ module Ci
@runner = runner
end
def execute
def execute(params = {})
builds =
if runner.shared?
if runner.instance_type?
builds_for_shared_runner
elsif runner.group_type?
builds_for_group_runner
......@@ -41,6 +41,8 @@ module Ci
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
begin
build.runner_id = runner.id
build.runner_session_attributes = params[:session] if params[:session].present?
build.run!
register_success(build)
......@@ -99,7 +101,7 @@ module Ci
end
def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared)
Ci::Build.running.where(runner: Ci::Runner.instance_type)
.group(:project_id).select(:project_id, 'count(*) AS running_builds')
end
......@@ -115,7 +117,7 @@ module Ci
end
def register_success(job)
labels = { shared_runner: runner.shared?,
labels = { shared_runner: runner.instance_type?,
jobs_running_for_project: jobs_running_for_project(job) }
job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
......@@ -123,10 +125,10 @@ module Ci
end
def jobs_running_for_project(job)
return '+Inf' unless runner.shared?
return '+Inf' unless runner.instance_type?
# excluding currently started job
running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.instance_type)
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end
......
......@@ -32,8 +32,9 @@ module Issues
def filter_assignee(issuable)
return if params[:assignee_ids].blank?
# The number of assignees is limited by one for GitLab CE
params[:assignee_ids] = params[:assignee_ids][0, 1]
unless issuable.allows_multiple_assignees?
params[:assignee_ids] = params[:assignee_ids].take(1)
end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
......
......@@ -20,6 +20,7 @@ module Labels
@available_labels ||= LabelsFinder.new(
current_user,
"#{parent_type}_id".to_sym => parent.id,
include_ancestor_groups: include_ancestor_groups?,
only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization)
end
......@@ -30,7 +31,8 @@ module Labels
new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
create_params = params.except(:include_ancestor_groups)
new_label = Labels::CreateService.new(create_params).execute(parent_type.to_sym => parent)
end
new_label
......@@ -47,5 +49,9 @@ module Labels
def parent_is_group?
parent_type == "group"
end
def include_ancestor_groups?
params[:include_ancestor_groups] == true
end
end
end
......@@ -2,6 +2,8 @@ module Projects
class CreateService < BaseService
def initialize(user, params)
@current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki)
@initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
end
def execute
......@@ -11,7 +13,6 @@ module Projects
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
@skip_wiki = params.delete(:skip_wiki)
@project = Project.new(params)
......@@ -102,6 +103,8 @@ module Projects
setup_authorizations
current_user.invalidate_personal_projects_count
create_readme if @initialize_with_readme
end
# Refresh the current user's authorizations inline (so they can access the
......@@ -116,6 +119,17 @@ module Projects
end
end
def create_readme
commit_attrs = {
branch_name: 'master',
commit_message: 'Initial commit',
file_path: 'README.md',
file_content: "# #{@project.name}\n\n#{@project.description}"
}
Files::CreateService.new(@project, current_user, commit_attrs).execute
end
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
......
......@@ -260,15 +260,15 @@ class TodoService
end
end
def create_mention_todos(project, target, author, note = nil, skip_users = [])
def create_mention_todos(parent, target, author, note = nil, skip_users = [])
# Create Todos for directly addressed users
directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note)
create_todos(directly_addressed_users, attributes)
# Create Todos for mentioned users
mentioned_users = filter_mentioned_users(project, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users)
attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
end
......@@ -299,36 +299,36 @@ class TodoService
def attributes_for_todo(project, target, author, action, note = nil)
attributes_for_target(target).merge!(
project_id: project.id,
project_id: project&.id,
author_id: author.id,
action: action,
note: note
)
end
def filter_todo_users(users, project, target)
reject_users_without_access(users, project, target).uniq
def filter_todo_users(users, parent, target)
reject_users_without_access(users, parent, target).uniq
end
def filter_mentioned_users(project, target, author, skip_users = [])
def filter_mentioned_users(parent, target, author, skip_users = [])
mentioned_users = target.mentioned_users(author) - skip_users
filter_todo_users(mentioned_users, project, target)
filter_todo_users(mentioned_users, parent, target)
end
def filter_directly_addressed_users(project, target, author, skip_users = [])
def filter_directly_addressed_users(parent, target, author, skip_users = [])
directly_addressed_users = target.directly_addressed_users(author) - skip_users
filter_todo_users(directly_addressed_users, project, target)
filter_todo_users(directly_addressed_users, parent, target)
end
def reject_users_without_access(users, project, target)
if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?)
def reject_users_without_access(users, parent, target)
if target.is_a?(Note) && target.for_issuable?
target = target.noteable
end
if target.is_a?(Issuable)
select_users(users, :"read_#{target.to_ability_name}", target)
else
select_users(users, :read_project, project)
select_users(users, :read_project, parent)
end
end
......
# frozen_string_literal: true
class AttachmentUploader < GitlabUploader
include RecordsUploads::Concern
include ObjectStorage::Concern
......
# frozen_string_literal: true
class AvatarUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern
......
# frozen_string_literal: true
class FaviconUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[png ico].freeze
......
# frozen_string_literal: true
class FileMover
attr_reader :secret, :file_name, :model, :update_field
......
# frozen_string_literal: true
# This class breaks the actual CarrierWave concept.
# Every uploader should use a base_dir that is model agnostic so we can build
# back URLs from base_dir-relative paths saved in the `Upload` model.
......@@ -117,7 +119,7 @@ class FileUploader < GitlabUploader
end
def markdown_link
markdown = "[#{markdown_name}](#{secure_url})"
markdown = +"[#{markdown_name}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous?
markdown
end
......
# frozen_string_literal: true
class GitlabUploader < CarrierWave::Uploader::Base
class_attribute :options
......
# frozen_string_literal: true
class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
......
# frozen_string_literal: true
class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
......
# frozen_string_literal: true
class LfsObjectUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
......
# frozen_string_literal: true
class NamespaceFileUploader < FileUploader
# Re-Override
def self.root
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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