Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-02-13 18:08:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-13 18:08:52 +0300
commit0ab47b994caa80c5587f33dc818626b66cfdafe2 (patch)
tree5ef3976d2f84e3368903a67ba2dbd87a74b9a43c /app
parent1308dc5eb484ab0f8064989fc551ebdb4b1a7976 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js1
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue150
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue98
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue3
-rw-r--r--app/assets/javascripts/error_tracking/details.js22
-rw-r--r--app/assets/javascripts/error_tracking/list.js30
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue54
-rw-r--r--app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue2
-rw-r--r--app/assets/javascripts/snippet/collapsible_input.js45
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js3
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/models/commit.rb10
-rw-r--r--app/models/concerns/has_repository.rb8
-rw-r--r--app/models/personal_snippet.rb4
-rw-r--r--app/models/project.rb5
-rw-r--r--app/models/project_snippet.rb4
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/snippet.rb46
-rw-r--r--app/models/snippet_repository.rb13
-rw-r--r--app/models/storage/hashed.rb18
-rw-r--r--app/services/boards/list_service.rb21
-rw-r--r--app/services/container_expiration_policy_service.rb6
-rw-r--r--app/services/metrics/dashboard/self_monitoring_dashboard_service.rb37
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb8
-rw-r--r--app/services/projects/container_repository/delete_tags_service.rb2
-rw-r--r--app/services/users/block_service.rb27
-rw-r--r--app/views/dashboard/_activities.html.haml3
-rw-r--r--app/views/projects/snippets/edit.html.haml1
-rw-r--r--app/views/projects/snippets/new.html.haml1
-rw-r--r--app/views/shared/snippets/_form.html.haml52
-rw-r--r--app/views/snippets/edit.html.haml1
-rw-r--r--app/views/snippets/new.html.haml1
-rw-r--r--app/workers/cleanup_container_repository_worker.rb7
34 files changed, 435 insertions, 262 deletions
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 26456fb28db..939c396e1b9 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -257,6 +257,7 @@ export default class ClusterStore {
name: environment.name,
project: environment.project,
environmentPath: environment.environment_path,
+ logsPath: environment.logs_path,
lastDeployment: environment.last_deployment,
rolloutStatus: {
status: environment.rollout_status ? environment.rollout_status.status : null,
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
deleted file mode 100644
index 9eaceb8893c..00000000000
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ /dev/null
@@ -1,150 +0,0 @@
-<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
-import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
-import Icon from '~/vue_shared/components/icon.vue';
-import DiffGutterAvatars from './diff_gutter_avatars.vue';
-import { LINE_POSITION_RIGHT } from '../constants';
-
-export default {
- components: {
- DiffGutterAvatars,
- Icon,
- },
- props: {
- line: {
- type: Object,
- required: true,
- },
- fileHash: {
- type: String,
- required: true,
- },
- contextLinesPath: {
- type: String,
- required: true,
- },
- lineNumber: {
- type: Number,
- required: false,
- default: 0,
- },
- linePosition: {
- type: String,
- required: false,
- default: '',
- },
- showCommentButton: {
- type: Boolean,
- required: false,
- default: false,
- },
- isBottom: {
- type: Boolean,
- required: false,
- default: false,
- },
- isMatchLine: {
- type: Boolean,
- required: false,
- default: false,
- },
- isMetaLine: {
- type: Boolean,
- required: false,
- default: false,
- },
- isContextLine: {
- type: Boolean,
- required: false,
- default: false,
- },
- isHover: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- ...mapState({
- diffViewType: state => state.diffs.diffViewType,
- diffFiles: state => state.diffs.diffFiles,
- }),
- ...mapGetters(['isLoggedIn']),
- lineCode() {
- return (
- this.line.line_code ||
- (this.line.left && this.line.left.line_code) ||
- (this.line.right && this.line.right.line_code)
- );
- },
- lineHref() {
- return `#${this.line.line_code || ''}`;
- },
- shouldShowCommentButton() {
- return (
- this.isHover &&
- !this.isMatchLine &&
- !this.isContextLine &&
- !this.isMetaLine &&
- !this.hasDiscussions
- );
- },
- hasDiscussions() {
- return this.line.discussions && this.line.discussions.length > 0;
- },
- shouldShowAvatarsOnGutter() {
- if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) {
- return false;
- }
- return this.showCommentButton && this.hasDiscussions;
- },
- shouldRenderCommentButton() {
- const isDiffHead = parseBoolean(getParameterByName('diff_head'));
- return !isDiffHead && this.isLoggedIn && this.showCommentButton;
- },
- },
- methods: {
- ...mapActions('diffs', [
- 'loadMoreLines',
- 'showCommentForm',
- 'setHighlightedRow',
- 'toggleLineDiscussions',
- 'toggleLineDiscussionWrappers',
- ]),
- handleCommentButton() {
- this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
- },
- },
-};
-</script>
-
-<template>
- <div>
- <button
- v-if="shouldRenderCommentButton"
- v-show="shouldShowCommentButton"
- type="button"
- class="add-diff-note js-add-diff-note-button qa-diff-comment"
- title="Add a comment to this line"
- @click="handleCommentButton"
- >
- <icon :size="12" name="comment" />
- </button>
- <a
- v-if="lineNumber"
- ref="lineNumberRef"
- :data-linenumber="lineNumber"
- :href="lineHref"
- @click="setHighlightedRow(lineCode)"
- >
- </a>
- <diff-gutter-avatars
- v-if="shouldShowAvatarsOnGutter"
- :discussions="line.discussions"
- :discussions-expanded="line.discussionsExpanded"
- @toggleLineDiscussions="
- toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
- "
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index 0f3e9208d21..9544fbe9fc5 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -1,21 +1,24 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import DiffLineGutterContent from './diff_line_gutter_content.vue';
+import { GlIcon } from '@gitlab/ui';
+import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import DiffGutterAvatars from './diff_gutter_avatars.vue';
import {
MATCH_LINE_TYPE,
CONTEXT_LINE_TYPE,
+ LINE_POSITION_RIGHT,
EMPTY_CELL_TYPE,
OLD_LINE_TYPE,
OLD_NO_NEW_LINE_TYPE,
NEW_NO_NEW_LINE_TYPE,
LINE_HOVER_CLASS_NAME,
LINE_UNFOLD_CLASS_NAME,
- INLINE_DIFF_VIEW_TYPE,
} from '../constants';
export default {
components: {
- DiffLineGutterContent,
+ DiffGutterAvatars,
+ GlIcon,
},
props: {
line: {
@@ -33,12 +36,6 @@ export default {
isHighlighted: {
type: Boolean,
required: true,
- default: false,
- },
- diffViewType: {
- type: String,
- required: false,
- default: INLINE_DIFF_VIEW_TYPE,
},
showCommentButton: {
type: Boolean,
@@ -73,6 +70,38 @@ export default {
},
computed: {
...mapGetters(['isLoggedIn']),
+ lineCode() {
+ return (
+ this.line.line_code ||
+ (this.line.left && this.line.left.line_code) ||
+ (this.line.right && this.line.right.line_code)
+ );
+ },
+ lineHref() {
+ return `#${this.line.line_code || ''}`;
+ },
+ shouldShowCommentButton() {
+ return (
+ this.isHover &&
+ !this.isMatchLine &&
+ !this.isContextLine &&
+ !this.isMetaLine &&
+ !this.hasDiscussions
+ );
+ },
+ hasDiscussions() {
+ return this.line.discussions && this.line.discussions.length > 0;
+ },
+ shouldShowAvatarsOnGutter() {
+ if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) {
+ return false;
+ }
+ return this.showCommentButton && this.hasDiscussions;
+ },
+ shouldRenderCommentButton() {
+ const isDiffHead = parseBoolean(getParameterByName('diff_head'));
+ return !isDiffHead && this.isLoggedIn && this.showCommentButton;
+ },
isMatchLine() {
return this.line.type === MATCH_LINE_TYPE;
},
@@ -107,24 +136,45 @@ export default {
return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line;
},
},
- methods: mapActions('diffs', ['setHighlightedRow']),
+ methods: {
+ ...mapActions('diffs', ['showCommentForm', 'setHighlightedRow', 'toggleLineDiscussions']),
+ handleCommentButton() {
+ this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
+ },
+ },
};
</script>
<template>
- <td :class="classNameMap">
- <diff-line-gutter-content
- :line="line"
- :file-hash="fileHash"
- :context-lines-path="contextLinesPath"
- :line-position="linePosition"
- :line-number="lineNumber"
- :show-comment-button="showCommentButton"
- :is-hover="isHover"
- :is-bottom="isBottom"
- :is-match-line="isMatchLine"
- :is-context-line="isContentLine"
- :is-meta-line="isMetaLine"
- />
+ <td ref="td" :class="classNameMap">
+ <div>
+ <button
+ v-if="shouldRenderCommentButton"
+ v-show="shouldShowCommentButton"
+ ref="addDiffNoteButton"
+ type="button"
+ class="add-diff-note js-add-diff-note-button qa-diff-comment"
+ title="Add a comment to this line"
+ @click="handleCommentButton"
+ >
+ <gl-icon :size="12" name="comment" />
+ </button>
+ <a
+ v-if="lineNumber"
+ ref="lineNumberRef"
+ :data-linenumber="lineNumber"
+ :href="lineHref"
+ @click="setHighlightedRow(lineCode)"
+ >
+ </a>
+ <diff-gutter-avatars
+ v-if="shouldShowAvatarsOnGutter"
+ :discussions="line.discussions"
+ :discussions-expanded="line.discussionsExpanded"
+ @toggleLineDiscussions="
+ toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
+ "
+ />
+ </div>
</td>
</template>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 8abc927c500..3f316643784 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -162,8 +162,7 @@ export default {
:is-loading="model.isLoadingDeployBoard"
:is-empty="model.isEmptyDeployBoard"
:has-legacy-app-label="model.hasLegacyAppLabel"
- :project-path="model.project_path"
- :environment-name="model.name"
+ :logs-path="model.logs_path"
/>
</div>
</div>
diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js
index 1a92681374b..55ab362f805 100644
--- a/app/assets/javascripts/error_tracking/details.js
+++ b/app/assets/javascripts/error_tracking/details.js
@@ -8,28 +8,30 @@ import csrf from '~/lib/utils/csrf';
Vue.use(VueApollo);
export default () => {
+ const selector = '#js-error_details';
+
+ const domEl = document.querySelector(selector);
+ const {
+ issueId,
+ projectPath,
+ issueUpdatePath,
+ issueStackTracePath,
+ projectIssuesPath,
+ } = domEl.dataset;
+
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
// eslint-disable-next-line no-new
new Vue({
- el: '#js-error_details',
+ el: selector,
apolloProvider,
components: {
ErrorDetails,
},
store,
render(createElement) {
- const domEl = document.querySelector(this.$options.el);
- const {
- issueId,
- projectPath,
- issueUpdatePath,
- issueStackTracePath,
- projectIssuesPath,
- } = domEl.dataset;
-
return createElement('error-details', {
props: {
issueId,
diff --git a/app/assets/javascripts/error_tracking/list.js b/app/assets/javascripts/error_tracking/list.js
index 8f3700249da..cb656a9ef13 100644
--- a/app/assets/javascripts/error_tracking/list.js
+++ b/app/assets/javascripts/error_tracking/list.js
@@ -4,27 +4,29 @@ import store from './store';
import ErrorTrackingList from './components/error_tracking_list.vue';
export default () => {
+ const selector = '#js-error_tracking';
+
+ const domEl = document.querySelector(selector);
+ const {
+ indexPath,
+ enableErrorTrackingLink,
+ illustrationPath,
+ projectPath,
+ listPath,
+ } = domEl.dataset;
+ let { errorTrackingEnabled, userCanEnableErrorTracking } = domEl.dataset;
+
+ errorTrackingEnabled = parseBoolean(errorTrackingEnabled);
+ userCanEnableErrorTracking = parseBoolean(userCanEnableErrorTracking);
+
// eslint-disable-next-line no-new
new Vue({
- el: '#js-error_tracking',
+ el: selector,
components: {
ErrorTrackingList,
},
store,
render(createElement) {
- const domEl = document.querySelector(this.$options.el);
- const {
- indexPath,
- enableErrorTrackingLink,
- illustrationPath,
- projectPath,
- listPath,
- } = domEl.dataset;
- let { errorTrackingEnabled, userCanEnableErrorTracking } = domEl.dataset;
-
- errorTrackingEnabled = parseBoolean(errorTrackingEnabled);
- userCanEnableErrorTracking = parseBoolean(userCanEnableErrorTracking);
-
return createElement('error-tracking-list', {
props: {
indexPath,
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 8b2c5e44bb5..eaf0780d9e1 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -18,6 +18,17 @@ import {
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
+/**
+ * A "virtual" coordinates system for the deployment icons.
+ * Deployment icons are displayed along the [min, max]
+ * range at height `pos`.
+ */
+const deploymentYAxisCoords = {
+ min: 0,
+ pos: 3, // 3% height of chart's grid
+ max: 100,
+};
+
const THROTTLED_DATAZOOM_WAIT = 1000; // miliseconds
const timestampToISODate = timestamp => new Date(timestamp).toISOString();
@@ -145,10 +156,33 @@ export default {
}, []);
},
chartOptionSeries() {
- return (this.option.series || []).concat(this.scatterSeries ? [this.scatterSeries] : []);
+ return (this.option.series || []).concat(
+ this.deploymentSeries ? [this.deploymentSeries] : [],
+ );
},
chartOptions() {
const option = omit(this.option, 'series');
+
+ const dataYAxis = {
+ name: this.yAxisLabel,
+ nameGap: 50, // same as gitlab-ui's default
+ nameLocation: 'center', // same as gitlab-ui's default
+ boundaryGap: [0.1, 0.1],
+ scale: true,
+ axisLabel: {
+ formatter: num => roundOffFloat(num, 3).toString(),
+ },
+ };
+ const deploymentsYAxis = {
+ show: false,
+ min: deploymentYAxisCoords.min,
+ max: deploymentYAxisCoords.max,
+ axisLabel: {
+ // formatter fn required to trigger tooltip re-positioning
+ formatter: () => {},
+ },
+ };
+
return {
series: this.chartOptionSeries,
xAxis: {
@@ -161,12 +195,7 @@ export default {
snap: true,
},
},
- yAxis: {
- name: this.yAxisLabel,
- axisLabel: {
- formatter: num => roundOffFloat(num, 3).toString(),
- },
- },
+ yAxis: [dataYAxis, deploymentsYAxis],
dataZoom: [this.dataZoomConfig],
...option,
};
@@ -228,10 +257,16 @@ export default {
return acc;
}, []);
},
- scatterSeries() {
+ deploymentSeries() {
return {
type: graphTypes.deploymentData,
- data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]),
+
+ yAxisIndex: 1, // deploymentsYAxis index
+ data: this.recentDeployments.map(deployment => [
+ deployment.createdAt,
+ deploymentYAxisCoords.pos,
+ ]),
+
symbol: this.svgs.rocket,
symbolSize: symbolSizes.default,
itemStyle: {
@@ -265,6 +300,7 @@ export default {
formatTooltipText(params) {
this.tooltip.title = dateFormat(params.value, dateFormats.default);
this.tooltip.content = [];
+
params.seriesData.forEach(dataPoint => {
if (dataPoint.value) {
const [xVal, yVal] = dataPoint.value;
diff --git a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
index 84d1c5ccc6a..a15b854cb9b 100644
--- a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
+++ b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
@@ -82,7 +82,7 @@ export default {
regexHelpText() {
return sprintf(
s__(
- 'ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
+ 'ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
),
{
codeStart: '<code>',
diff --git a/app/assets/javascripts/snippet/collapsible_input.js b/app/assets/javascripts/snippet/collapsible_input.js
new file mode 100644
index 00000000000..e7225162f86
--- /dev/null
+++ b/app/assets/javascripts/snippet/collapsible_input.js
@@ -0,0 +1,45 @@
+const hide = el => el.classList.add('d-none');
+const show = el => el.classList.remove('d-none');
+
+const setupCollapsibleInput = el => {
+ const collapsedEl = el.querySelector('.js-collapsed');
+ const expandedEl = el.querySelector('.js-expanded');
+ const collapsedInputEl = collapsedEl.querySelector('textarea,input,select');
+ const expandedInputEl = expandedEl.querySelector('textarea,input,select');
+ const formEl = el.closest('form');
+
+ const collapse = () => {
+ hide(expandedEl);
+ show(collapsedEl);
+ };
+
+ const expand = () => {
+ hide(collapsedEl);
+ show(expandedEl);
+ };
+
+ // NOTE:
+ // We add focus listener to all form inputs so that we can collapse
+ // when something is focused that's not the expanded input.
+ formEl.addEventListener('focusin', e => {
+ if (e.target === collapsedInputEl) {
+ expand();
+ expandedInputEl.focus();
+ } else if (!el.contains(e.target) && !expandedInputEl.value) {
+ collapse();
+ }
+ });
+};
+
+/**
+ * Usage in HAML
+ *
+ * .js-collapsible-input
+ * .js-collapsed{ class: ('d-none' if is_expanded) }
+ * = input
+ * .js-expanded{ class: ('d-none' if !is_expanded) }
+ * = big_input
+ */
+export default () => {
+ Array.from(document.querySelectorAll('.js-collapsible-input')).forEach(setupCollapsibleInput);
+};
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index dcee17453b8..652531a1289 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,6 +1,7 @@
/* global ace */
import $ from 'jquery';
+import setupCollapsibleInputs from './collapsible_input';
export default () => {
const editor = ace.edit('editor');
@@ -8,4 +9,6 @@ export default () => {
$('.snippet-form-holder form').on('submit', () => {
$('.snippet-file-content').val(editor.getValue());
});
+
+ setupCollapsibleInputs();
};
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 9fbfc59f630..8414095d454 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -75,7 +75,9 @@ class Admin::UsersController < Admin::ApplicationController
end
def block
- if update_user { |user| user.block }
+ result = Users::BlockService.new(current_user).execute(user)
+
+ if result[:status] = :success
redirect_back_or_admin_user(notice: _("Successfully blocked"))
else
redirect_back_or_admin_user(alert: _("Error occurred. User was not blocked"))
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d3b0304f2c7..1ce76fd57b1 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -717,6 +717,6 @@ module ProjectsHelper
def settings_container_registry_expiration_policy_available?(project)
Feature.enabled?(:registry_retention_policies_settings, project) &&
Gitlab.config.registry.enabled &&
- can?(current_user, :read_container_image, project)
+ can?(current_user, :destroy_container_image, project)
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 46222bbc4cd..d8a3bbfeeb2 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -244,6 +244,8 @@ class Commit
# Discover issues should be closed when this commit is pushed to a project's
# default branch.
def closes_issues(current_user = self.committer)
+ return unless repository.repo_type.project?
+
Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end
@@ -297,7 +299,11 @@ class Commit
end
def merge_requests
- @merge_requests ||= project&.merge_requests&.by_commit_sha(sha)
+ strong_memoize(:merge_requests) do
+ next MergeRequest.none unless repository.repo_type.project? && project
+
+ project.merge_requests.by_commit_sha(sha)
+ end
end
def method_missing(method, *args, &block)
@@ -507,7 +513,7 @@ class Commit
end
def commit_reference(from, referable_commit_id, full: false)
- base = project&.to_reference_base(from, full: full)
+ base = container.to_reference_base(from, full: full)
if base.present?
"#{base}#{self.class.reference_prefix}#{referable_commit_id}"
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index 66c2f57bedd..d04a6408a21 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -1,9 +1,17 @@
# frozen_string_literal: true
+# This concern is created to handle repository actions.
+# It should be include inside any object capable
+# of directly having a repository, like project or snippet.
+#
+# It also includes `Referable`, therefore the method
+# `to_reference` should be overriden in case the object
+# needs any special behavior.
module HasRepository
extend ActiveSupport::Concern
include Gitlab::ShellAdapter
include AfterCommitQueue
+ include Referable
include Gitlab::Utils::StrongMemoize
delegate :base_dir, :disk_path, to: :storage
diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb
index 1b5be8698b1..5940265b17a 100644
--- a/app/models/personal_snippet.rb
+++ b/app/models/personal_snippet.rb
@@ -2,4 +2,8 @@
class PersonalSnippet < Snippet
include WithUploads
+
+ def web_url(only_path: nil)
+ Gitlab::Routing.url_helpers.snippet_url(self, only_path: only_path)
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0ed2510dbf4..bc652a19986 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -9,7 +9,6 @@ class Project < ApplicationRecord
include AccessRequestable
include Avatarable
include CacheMarkdownField
- include Referable
include Sortable
include AfterCommitQueue
include CaseSensitivity
@@ -2336,6 +2335,10 @@ class Project < ApplicationRecord
false
end
+ def self_monitoring?
+ Gitlab::CurrentSettings.self_monitoring_project_id == id
+ end
+
private
def closest_namespace_setting(name)
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index ffb08e10f1f..6045ec71c6e 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -5,4 +5,8 @@ class ProjectSnippet < Snippet
validates :project, presence: true
validates :secret, inclusion: { in: [false] }
+
+ def web_url(only_path: nil)
+ Gitlab::Routing.url_helpers.project_snippet_url(project, self, only_path: only_path)
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 37a20404ae7..c439d0700f1 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1131,7 +1131,11 @@ class Repository
end
def project
- container
+ if repo_type.snippet?
+ container.project
+ else
+ container
+ end
end
private
@@ -1145,7 +1149,7 @@ class Repository
Gitlab::Git::Commit.find(raw_repository, oid_or_ref)
end
- ::Commit.new(commit, project) if commit
+ ::Commit.new(commit, container) if commit
end
def cache
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 77ec683f584..e2b72dfde7a 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -6,7 +6,6 @@ class Snippet < ApplicationRecord
include CacheMarkdownField
include Noteable
include Participable
- include Referable
include Sortable
include Awardable
include Mentionable
@@ -15,10 +14,11 @@ class Snippet < ApplicationRecord
include Gitlab::SQL::Pattern
include FromUnion
include IgnorableColumns
-
+ include HasRepository
extend ::Gitlab::Utils::Override
ignore_column :storage_version, remove_with: '12.9', remove_after: '2020-03-22'
+ ignore_column :repository_storage, remove_with: '12.10', remove_after: '2020-04-22'
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -42,6 +42,7 @@ class Snippet < ApplicationRecord
has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: "SnippetUserMention"
+ has_one :snippet_repository, inverse_of: :snippet
delegate :name, :email, to: :author, prefix: true, allow_nil: true
@@ -254,6 +255,47 @@ class Snippet < ApplicationRecord
super
end
+ def repository
+ @repository ||= Repository.new(full_path, self, disk_path: disk_path, repo_type: Gitlab::GlRepository::SNIPPET)
+ end
+
+ def storage
+ @storage ||= Storage::Hashed.new(self, prefix: Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX)
+ end
+
+ # This is the full_path used to identify the
+ # the snippet repository. It will be used mostly
+ # for logging purposes.
+ def full_path
+ return unless persisted?
+
+ @full_path ||= begin
+ components = []
+ components << project.full_path if project_id?
+ components << '@snippets'
+ components << self.id
+ components.join('/')
+ end
+ end
+
+ def repository_storage
+ snippet_repository&.shard_name ||
+ Gitlab::CurrentSettings.pick_repository_storage
+ end
+
+ def create_repository
+ return if repository_exists?
+
+ repository.create_if_not_exists
+
+ track_snippet_repository if repository_exists?
+ end
+
+ def track_snippet_repository
+ repository = snippet_repository || build_snippet_repository
+ repository.update!(shard_name: repository_storage, disk_path: disk_path)
+ end
+
class << self
# Searches for snippets with a matching title or file name.
#
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
new file mode 100644
index 00000000000..ba2a061a5f4
--- /dev/null
+++ b/app/models/snippet_repository.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class SnippetRepository < ApplicationRecord
+ include Shardable
+
+ belongs_to :snippet, inverse_of: :snippet_repository
+
+ class << self
+ def find_snippet(disk_path)
+ find_by(disk_path: disk_path)&.snippet
+ end
+ end
+end
diff --git a/app/models/storage/hashed.rb b/app/models/storage/hashed.rb
index 898e75194db..3dea50ab98b 100644
--- a/app/models/storage/hashed.rb
+++ b/app/models/storage/hashed.rb
@@ -2,14 +2,15 @@
module Storage
class Hashed
- attr_accessor :project
- delegate :gitlab_shell, :repository_storage, to: :project
+ attr_accessor :container
+ delegate :gitlab_shell, :repository_storage, to: :container
REPOSITORY_PATH_PREFIX = '@hashed'
+ SNIPPET_REPOSITORY_PATH_PREFIX = '@snippets'
POOL_PATH_PREFIX = '@pools'
- def initialize(project, prefix: REPOSITORY_PATH_PREFIX)
- @project = project
+ def initialize(container, prefix: REPOSITORY_PATH_PREFIX)
+ @container = container
@prefix = prefix
end
@@ -20,9 +21,10 @@ module Storage
"#{@prefix}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash
end
- # Disk path is used to build repository and project's wiki path on disk
+ # Disk path is used to build repository path on disk
#
- # @return [String] combination of base_dir and the repository own name without `.git` or `.wiki.git` extensions
+ # @return [String] combination of base_dir and the repository own name
+ # without `.git`, `.wiki.git`, or any other extension
def disk_path
"#{base_dir}/#{disk_hash}" if disk_hash
end
@@ -33,10 +35,10 @@ module Storage
private
- # Generates the hash for the project path and name on disk
+ # Generates the hash for the repository path and name on disk
# If you need to refer to the repository on disk, use the `#disk_path`
def disk_hash
- @disk_hash ||= Digest::SHA2.hexdigest(project.id.to_s) if project.id
+ @disk_hash ||= Digest::SHA2.hexdigest(container.id.to_s) if container.id
end
end
end
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
index c6dfd62804f..729bca6580e 100644
--- a/app/services/boards/list_service.rb
+++ b/app/services/boards/list_service.rb
@@ -5,13 +5,7 @@ module Boards
def execute(create_default_board: true)
create_board! if create_default_board && parent.boards.empty?
- if parent.multiple_issue_boards_available?
- boards
- else
- # When multiple issue boards are not available
- # a user is only allowed to view the default shown board
- first_board
- end
+ find_boards
end
private
@@ -27,5 +21,18 @@ module Boards
def create_board!
Boards::CreateService.new(parent, current_user).execute
end
+
+ def find_boards
+ found =
+ if parent.multiple_issue_boards_available?
+ boards
+ else
+ # When multiple issue boards are not available
+ # a user is only allowed to view the default shown board
+ first_board
+ end
+
+ params[:board_id].present? ? [found.find(params[:board_id])] : found
+ end
end
end
diff --git a/app/services/container_expiration_policy_service.rb b/app/services/container_expiration_policy_service.rb
index 5d141d4d64d..82274fd8668 100644
--- a/app/services/container_expiration_policy_service.rb
+++ b/app/services/container_expiration_policy_service.rb
@@ -6,9 +6,11 @@ class ContainerExpirationPolicyService < BaseService
container_expiration_policy.container_repositories.find_each do |container_repository|
CleanupContainerRepositoryWorker.perform_async(
- current_user.id,
+ nil,
container_repository.id,
- container_expiration_policy.attributes.except("created_at", "updated_at")
+ container_expiration_policy.attributes
+ .except('created_at', 'updated_at')
+ .merge(container_expiration_policy: true)
)
end
end
diff --git a/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb
new file mode 100644
index 00000000000..d705c3f3ce5
--- /dev/null
+++ b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# Fetches the self monitoring metrics dashboard and formats the output.
+# Use Gitlab::Metrics::Dashboard::Finder to retrieve dashboards.
+module Metrics
+ module Dashboard
+ class SelfMonitoringDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
+ DASHBOARD_PATH = 'config/prometheus/self_monitoring_default.yml'
+ DASHBOARD_NAME = 'Default'
+
+ SEQUENCE = [
+ STAGES::ProjectMetricsInserter,
+ STAGES::EndpointInserter,
+ STAGES::Sorter
+ ].freeze
+
+ class << self
+ def valid_params?(params)
+ matching_dashboard?(params[:dashboard_path]) || self_monitoring_project?(params)
+ end
+
+ def all_dashboard_paths(_project)
+ [{
+ path: DASHBOARD_PATH,
+ display_name: DASHBOARD_NAME,
+ default: true,
+ system_dashboard: false
+ }]
+ end
+
+ def self_monitoring_project?(params)
+ params[:dashboard_path].nil? && params[:environment]&.project&.self_monitoring?
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index b995df12e56..046745d725e 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -5,7 +5,7 @@ module Projects
class CleanupTagsService < BaseService
def execute(container_repository)
return error('feature disabled') unless can_use?
- return error('access denied') unless can_admin?
+ return error('access denied') unless can_destroy?
tags = container_repository.tags
tags_by_digest = group_by_digest(tags)
@@ -82,8 +82,10 @@ module Projects
end
end
- def can_admin?
- can?(current_user, :admin_container_image, project)
+ def can_destroy?
+ return true if params['container_expiration_policy']
+
+ can?(current_user, :destroy_container_image, project)
end
def can_use?
diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb
index d19f275e928..21081bd077f 100644
--- a/app/services/projects/container_repository/delete_tags_service.rb
+++ b/app/services/projects/container_repository/delete_tags_service.rb
@@ -42,7 +42,7 @@ module Projects
# Deletes the dummy image
# All created tag digests are the same since they all have the same dummy image.
# a single delete is sufficient to remove all tags with it
- if deleted_tags.any? && container_repository.delete_tag_by_digest(deleted_tags.values.first)
+ if deleted_tags.any? && container_repository.delete_tag_by_digest(deleted_tags.each_value.first)
success(deleted: deleted_tags.keys)
else
error('could not delete tags')
diff --git a/app/services/users/block_service.rb b/app/services/users/block_service.rb
new file mode 100644
index 00000000000..9c393832d8f
--- /dev/null
+++ b/app/services/users/block_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Users
+ class BlockService < BaseService
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(user)
+ if user.block
+ after_block_hook(user)
+ success
+ else
+ messages = user.errors.full_messages
+ error(messages.uniq.join('. '))
+ end
+ end
+
+ private
+
+ def after_block_hook(user)
+ # overriden by EE module
+ end
+ end
+end
+
+Users::BlockService.prepend_if_ee('EE::Users::BlockService')
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index 4359a2c3c2b..2db3e35250f 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -5,4 +5,5 @@
%i.fa.fa-rss
.content_list
-= spinner
+.loading
+ .spinner.spinner-md
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 6dbd67df886..9f5af1cfe1e 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,6 +1,7 @@
- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title @snippet.to_reference
- page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
+- @content_class = "limit-container-width" unless fluid_layout
%h3.page-title
= _("Edit Snippet")
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index d64e3a49a81..d55a1160d48 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,6 +1,7 @@
- add_to_breadcrumbs _("Snippets"), project_snippets_path(@project)
- breadcrumb_title _("New")
- page_title _("New Snippet")
+- @content_class = "limit-container-width" unless fluid_layout
%h3.page-title
= _("New Snippet")
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 73401029da4..f867fb2b6f7 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -6,27 +6,37 @@
html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
= form_errors(@snippet)
- .form-group.row
- .col-sm-2.col-form-label
- = f.label :title
- .col-sm-10
- = f.text_field :title, class: 'form-control qa-snippet-title', required: true, autofocus: true
-
- = render 'shared/form_elements/description', model: @snippet, project: @project, form: f
-
- = render 'shared/old_visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet, with_label: false
-
- .file-editor
- .form-group.row
- .col-sm-2.col-form-label
- = f.label :file_name, "File"
- .col-sm-10
- .file-holder.snippet
- .js-file-title.file-title-flex-parent
- = f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name qa-snippet-file-name'
- .file-content.code
- %pre#editor= @snippet.content
- = f.hidden_field :content, class: 'snippet-file-content'
+ .form-group
+ = f.label :title, class: 'label-bold'
+ = f.text_field :title, class: 'form-control qa-snippet-title', required: true, autofocus: true
+
+ .form-group.js-description-input
+ - description_placeholder = s_('Snippets|Optionally add a description about what your snippet does or how to use it...')
+ - is_expanded = @snippet.description && !@snippet.description.empty?
+ = f.label :description, s_("Snippets|Description (optional)"), class: 'label-bold'
+ .js-collapsible-input
+ .js-collapsed{ class: ('d-none' if is_expanded) }
+ = text_field_tag nil, nil, class: 'form-control', placeholder: description_placeholder
+ .js-expanded{ class: ('d-none' if !is_expanded) }
+ = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
+ = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder
+ = render 'shared/notes/hints'
+
+ .form-group.file-editor
+ = f.label :file_name, s_('Snippets|File')
+ .file-holder.snippet
+ .js-file-title.file-title-flex-parent
+ = f.text_field :file_name, placeholder: s_("Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"), class: 'form-control snippet-file-name qa-snippet-file-name'
+ .file-content.code
+ %pre#editor= @snippet.content
+ = f.hidden_field :content, class: 'snippet-file-content'
+
+ .form-group
+ .font-weight-bold
+ = _('Visibility level')
+ = link_to icon('question-circle'), help_page_path("public_access/public_access"), target: '_blank'
+ = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet, with_label: false
+
- if params[:files]
- params[:files].each_with_index do |file, index|
= hidden_field_tag "files[]", file, id: "files_#{index}"
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index f5ffb037152..66f5e8148e1 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -1,4 +1,5 @@
- page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
+- @content_class = "limit-container-width" unless fluid_layout
%h3.page-title
= _("Edit Snippet")
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index 9d462865471..acc0ce0fff3 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -1,6 +1,7 @@
- @hide_top_links = true
- @hide_breadcrumbs = true
- page_title _("New Snippet")
+- @content_class = "limit-container-width" unless fluid_layout
.page-title-holder.d-flex.align-items-center
%h1.page-title= _('New Snippet')
diff --git a/app/workers/cleanup_container_repository_worker.rb b/app/workers/cleanup_container_repository_worker.rb
index 83fb3e58d29..83397a1dda2 100644
--- a/app/workers/cleanup_container_repository_worker.rb
+++ b/app/workers/cleanup_container_repository_worker.rb
@@ -11,6 +11,7 @@ class CleanupContainerRepositoryWorker
def perform(current_user_id, container_repository_id, params)
@current_user = User.find_by_id(current_user_id)
@container_repository = ContainerRepository.find_by_id(container_repository_id)
+ @params = params
return unless valid?
@@ -22,9 +23,15 @@ class CleanupContainerRepositoryWorker
private
def valid?
+ return true if run_by_container_expiration_policy?
+
current_user && container_repository && project
end
+ def run_by_container_expiration_policy?
+ @params['container_expiration_policy'] && container_repository && project
+ end
+
def project
container_repository&.project
end