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-12 18:09:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-02-12 18:09:37 +0300
commit2c89e169769ead722394a79ed67fcd08e96863dd (patch)
tree0dadb576846c484475b895f75fab41f71cdb952e /app
parentbd497e352ebd279536ae11855871162e82a3f88c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue38
-rw-r--r--app/assets/javascripts/blob/components/blob_header_default_actions.vue14
-rw-r--r--app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue15
-rw-r--r--app/assets/javascripts/blob/event_hub.js3
-rw-r--r--app/assets/javascripts/ide/ide_router.js6
-rw-r--r--app/assets/javascripts/ide/ide_router_extension.js21
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js6
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js179
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js11
-rw-r--r--app/controllers/projects/alerting/notifications_controller.rb47
-rw-r--r--app/helpers/blob_helper.rb2
-rw-r--r--app/models/container_repository.rb6
-rw-r--r--app/models/project.rb5
-rw-r--r--app/models/project_services/alerts_service.rb78
-rw-r--r--app/models/project_services/alerts_service_data.rb14
-rw-r--r--app/services/projects/alerting/notify_service.rb49
-rw-r--r--app/services/projects/container_repository/delete_tags_service.rb24
-rw-r--r--app/services/projects/lsif_data_service.rb46
-rw-r--r--app/views/projects/graphs/charts.html.haml30
19 files changed, 450 insertions, 144 deletions
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 61a66513838..b7d9600ec40 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -2,8 +2,7 @@
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import DefaultActions from './blob_header_default_actions.vue';
import BlobFilepath from './blob_header_filepath.vue';
-import eventHub from '../event_hub';
-import { RICH_BLOB_VIEWER, SIMPLE_BLOB_VIEWER } from './constants';
+import { SIMPLE_BLOB_VIEWER } from './constants';
export default {
components: {
@@ -26,10 +25,15 @@ export default {
required: false,
default: false,
},
+ activeViewerType: {
+ type: String,
+ required: false,
+ default: SIMPLE_BLOB_VIEWER,
+ },
},
data() {
return {
- activeViewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
+ viewer: this.hideViewerSwitcher ? null : this.activeViewerType,
};
},
computed: {
@@ -40,19 +44,16 @@ export default {
return !this.hideDefaultActions;
},
},
- created() {
- if (this.showViewerSwitcher) {
- eventHub.$on('switch-viewer', this.setActiveViewer);
- }
- },
- beforeDestroy() {
- if (this.showViewerSwitcher) {
- eventHub.$off('switch-viewer', this.setActiveViewer);
- }
+ watch: {
+ viewer(newVal, oldVal) {
+ if (!this.hideViewerSwitcher && newVal !== oldVal) {
+ this.$emit('viewer-changed', newVal);
+ }
+ },
},
methods: {
- setActiveViewer(viewer) {
- this.activeViewer = viewer;
+ proxyCopyRequest() {
+ this.$emit('copy');
},
},
};
@@ -66,11 +67,16 @@ export default {
</blob-filepath>
<div class="file-actions d-none d-sm-block">
- <viewer-switcher v-if="showViewerSwitcher" :blob="blob" :active-viewer="activeViewer" />
+ <viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
<slot name="actions"></slot>
- <default-actions v-if="showDefaultActions" :blob="blob" :active-viewer="activeViewer" />
+ <default-actions
+ v-if="showDefaultActions"
+ :raw-path="blob.rawPath"
+ :active-viewer="viewer"
+ @copy="proxyCopyRequest"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
index e526fae0dba..f5157fba819 100644
--- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue
+++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
@@ -7,7 +7,6 @@ import {
RICH_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER,
} from './constants';
-import eventHub from '../event_hub';
export default {
components: {
@@ -19,8 +18,8 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
- blob: {
- type: Object,
+ rawPath: {
+ type: String,
required: true,
},
activeViewer: {
@@ -30,11 +29,8 @@ export default {
},
},
computed: {
- rawUrl() {
- return this.blob.rawPath;
- },
downloadUrl() {
- return `${this.blob.rawPath}?inline=false`;
+ return `${this.rawPath}?inline=false`;
},
copyDisabled() {
return this.activeViewer === RICH_BLOB_VIEWER;
@@ -42,7 +38,7 @@ export default {
},
methods: {
requestCopyContents() {
- eventHub.$emit('copy');
+ this.$emit('copy');
},
},
BTN_COPY_CONTENTS_TITLE,
@@ -65,7 +61,7 @@ export default {
v-gl-tooltip.hover
:aria-label="$options.BTN_RAW_TITLE"
:title="$options.BTN_RAW_TITLE"
- :href="rawUrl"
+ :href="rawPath"
target="_blank"
>
<gl-icon name="doc-code" :size="14" />
diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
index 13ea87c99b1..689fa7638f0 100644
--- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
+++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
@@ -6,7 +6,6 @@ import {
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
} from './constants';
-import eventHub from '../event_hub';
export default {
components: {
@@ -18,11 +17,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
- blob: {
- type: Object,
- required: true,
- },
- activeViewer: {
+ value: {
type: String,
default: SIMPLE_BLOB_VIEWER,
required: false,
@@ -30,16 +25,16 @@ export default {
},
computed: {
isSimpleViewer() {
- return this.activeViewer === SIMPLE_BLOB_VIEWER;
+ return this.value === SIMPLE_BLOB_VIEWER;
},
isRichViewer() {
- return this.activeViewer === RICH_BLOB_VIEWER;
+ return this.value === RICH_BLOB_VIEWER;
},
},
methods: {
switchToViewer(viewer) {
- if (viewer !== this.activeViewer) {
- eventHub.$emit('switch-viewer', viewer);
+ if (viewer !== this.value) {
+ this.$emit('input', viewer);
}
},
},
diff --git a/app/assets/javascripts/blob/event_hub.js b/app/assets/javascripts/blob/event_hub.js
deleted file mode 100644
index 0948c2e5352..00000000000
--- a/app/assets/javascripts/blob/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Vue from 'vue';
-
-export default new Vue();
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 8c84b98a108..0fab3ee0f3b 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
-import VueRouter from 'vue-router';
+import IdeRouter from '~/ide/ide_router_extension';
import { joinPaths } from '~/lib/utils/url_utility';
import flash from '~/flash';
import store from './stores';
import { __ } from '~/locale';
-Vue.use(VueRouter);
+Vue.use(IdeRouter);
/**
* Routes below /-/ide/:
@@ -33,7 +33,7 @@ const EmptyRouterComponent = {
},
};
-const router = new VueRouter({
+const router = new IdeRouter({
mode: 'history',
base: joinPaths(gon.relative_url_root || '', '/-/ide/'),
routes: [
diff --git a/app/assets/javascripts/ide/ide_router_extension.js b/app/assets/javascripts/ide/ide_router_extension.js
new file mode 100644
index 00000000000..a146aca7283
--- /dev/null
+++ b/app/assets/javascripts/ide/ide_router_extension.js
@@ -0,0 +1,21 @@
+import VueRouter from 'vue-router';
+import { escapeFileUrl } from '~/lib/utils/url_utility';
+
+// To allow special characters (like "#," for example) in the branch names, we
+// should encode all the locations before those get processed by History API.
+// Otherwise, paths get messed up so that the router receives incorrect
+// branchid. The only way to do it consistently and in a more or less
+// future-proof manner is, unfortunately, to monkey-patch VueRouter or, as
+// suggested here, achieve the same more reliably by subclassing VueRouter and
+// update the methods, used in WebIDE.
+//
+// More context: https://gitlab.com/gitlab-org/gitlab/issues/35473
+
+export default class IDERouter extends VueRouter {
+ push(location, onComplete, onAbort) {
+ super.push(escapeFileUrl(location), onComplete, onAbort);
+ }
+ resolve(to, current, append) {
+ return super.resolve(escapeFileUrl(to), current, append);
+ }
+}
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 267b49e9d98..1ff4f7bab97 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -194,12 +194,14 @@ export function redirectTo(url) {
return window.location.assign(url);
}
+export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
+
export function webIDEUrl(route = undefined) {
let returnUrl = `${gon.relative_url_root || ''}/-/ide/`;
if (route) {
returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`;
}
- return returnUrl;
+ return escapeFileUrl(returnUrl);
}
/**
@@ -313,8 +315,6 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f
return urlObj.toString();
};
-export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');
-
export function urlIsDifferent(url, compare = String(window.location)) {
return url !== compare;
}
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 1b94fb06107..803f4e37705 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -1,47 +1,15 @@
-import $ from 'jquery';
-import Chart from 'chart.js';
-import { barChartOptions, pieChartOptions } from '~/lib/utils/chart_utils';
+import Vue from 'vue';
+import { __ } from '~/locale';
+import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import SeriesDataMixin from './series_data_mixin';
document.addEventListener('DOMContentLoaded', () => {
- const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML);
+ const languagesContainer = document.getElementById('js-languages-chart');
+ const monthContainer = document.getElementById('js-month-chart');
+ const weekdayContainer = document.getElementById('js-weekday-chart');
+ const hourContainer = document.getElementById('js-hour-chart');
- const barChart = (selector, data) => {
- // get selector by context
- const ctx = selector.get(0).getContext('2d');
- // pointing parent container to make chart.js inherit its width
- const container = $(selector).parent();
- selector.attr('width', $(container).width());
-
- // Scale fonts if window width lower than 768px (iPad portrait)
- const shouldAdjustFontSize = window.innerWidth < 768;
- return new Chart(ctx, {
- type: 'bar',
- data,
- options: barChartOptions(shouldAdjustFontSize),
- });
- };
-
- const pieChart = (context, data) => {
- const options = pieChartOptions();
-
- return new Chart(context, {
- type: 'pie',
- data,
- options,
- });
- };
-
- const chartData = data => ({
- labels: Object.keys(data),
- datasets: [
- {
- backgroundColor: 'rgba(220,220,220,0.5)',
- borderColor: 'rgba(220,220,220,1)',
- borderWidth: 1,
- data: Object.values(data),
- },
- ],
- });
+ const LANGUAGE_CHART_HEIGHT = 300;
const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => {
if (firstDayOfWeek === 0) {
@@ -58,28 +26,115 @@ document.addEventListener('DOMContentLoaded', () => {
}, {});
};
- const hourData = chartData(projectChartData.hour);
- barChart($('#hour-chart'), hourData);
-
- const weekDays = reorderWeekDays(projectChartData.weekDays, gon.first_day_of_week);
- const dayData = chartData(weekDays);
- barChart($('#weekday-chart'), dayData);
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: languagesContainer,
+ components: {
+ GlColumnChart,
+ },
+ data() {
+ return {
+ chartData: JSON.parse(languagesContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ return { full: this.chartData.map(d => [d.label, d.value]) };
+ },
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Used programming language'),
+ yAxisTitle: __('Percentage'),
+ xAxisType: 'category',
+ },
+ attrs: {
+ height: LANGUAGE_CHART_HEIGHT,
+ },
+ });
+ },
+ });
- const monthData = chartData(projectChartData.month);
- barChart($('#month-chart'), monthData);
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: monthContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(monthContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Day of month'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
- const data = {
- datasets: [
- {
- data: projectChartData.languages.map(x => x.value),
- backgroundColor: projectChartData.languages.map(x => x.color),
- hoverBackgroundColor: projectChartData.languages.map(x => x.highlight),
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: weekdayContainer,
+ components: {
+ GlColumnChart,
+ },
+ data() {
+ return {
+ chartData: JSON.parse(weekdayContainer.dataset.chartData),
+ };
+ },
+ computed: {
+ seriesData() {
+ const weekDays = reorderWeekDays(this.chartData, gon.first_day_of_week);
+ const data = Object.keys(weekDays).reduce((acc, key) => {
+ acc.push([key, weekDays[key]]);
+ return acc;
+ }, []);
+ return { full: data };
},
- ],
- labels: projectChartData.languages.map(x => x.label),
- };
- const ctx = $('#languages-chart')
- .get(0)
- .getContext('2d');
- pieChart(ctx, data);
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Weekday'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: hourContainer,
+ components: {
+ GlColumnChart,
+ },
+ mixins: [SeriesDataMixin],
+ data() {
+ return {
+ chartData: JSON.parse(hourContainer.dataset.chartData),
+ };
+ },
+ render(h) {
+ return h(GlColumnChart, {
+ props: {
+ data: this.seriesData,
+ xAxisTitle: __('Hour (UTC)'),
+ yAxisTitle: __('No. of commits'),
+ xAxisType: 'category',
+ },
+ });
+ },
+ });
});
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js b/app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js
new file mode 100644
index 00000000000..941427a1ac3
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/graphs/charts/series_data_mixin.js
@@ -0,0 +1,11 @@
+export default {
+ computed: {
+ seriesData() {
+ const data = Object.keys(this.chartData).reduce((acc, key) => {
+ acc.push([key, this.chartData[key]]);
+ return acc;
+ }, []);
+ return { full: data };
+ },
+ },
+};
diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb
new file mode 100644
index 00000000000..1fe31863469
--- /dev/null
+++ b/app/controllers/projects/alerting/notifications_controller.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Projects
+ module Alerting
+ class NotificationsController < Projects::ApplicationController
+ respond_to :json
+
+ skip_before_action :verify_authenticity_token
+ skip_before_action :project
+
+ prepend_before_action :repository, :project_without_auth
+
+ def create
+ token = extract_alert_manager_token(request)
+ result = notify_service.execute(token)
+
+ head(response_status(result))
+ end
+
+ private
+
+ def project_without_auth
+ @project ||= Project
+ .find_by_full_path("#{params[:namespace_id]}/#{params[:project_id]}")
+ end
+
+ def extract_alert_manager_token(request)
+ Doorkeeper::OAuth::Token.from_bearer_authorization(request)
+ end
+
+ def notify_service
+ Projects::Alerting::NotifyService
+ .new(project, current_user, notification_payload)
+ end
+
+ def response_status(result)
+ return :ok if result.success?
+
+ result.http_status
+ end
+
+ def notification_payload
+ params.permit![:notification]
+ end
+ end
+ end
+end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 7c0f4da355d..77a320f8925 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -27,7 +27,7 @@ module BlobHelper
"#{current_user.namespace.full_path}/#{project.path}"
end
- segments = [ide_path, 'project', project_path, 'edit', ref]
+ segments = [ide_path, 'project', project_path, 'edit', encode_ide_path(ref)]
segments.concat(['-', encode_ide_path(path)]) if path.present?
File.join(segments)
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 152aa7b3218..fcbfda8fbc2 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -77,7 +77,11 @@ class ContainerRepository < ApplicationRecord
end
def delete_tag_by_digest(digest)
- client.delete_repository_tag(self.path, digest)
+ client.delete_repository_tag_by_digest(self.path, digest)
+ end
+
+ def delete_tag_by_name(name)
+ client.delete_repository_tag_by_name(self.path, name)
end
def self.build_from_path(path)
diff --git a/app/models/project.rb b/app/models/project.rb
index 44701ef792a..b2ac9c99ab6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -138,6 +138,7 @@ class Project < ApplicationRecord
has_many :boards
# Project services
+ has_one :alerts_service
has_one :campfire_service
has_one :discord_service
has_one :drone_ci_service
@@ -2330,6 +2331,10 @@ class Project < ApplicationRecord
protected_branches.limit(limit)
end
+ def alerts_service_activated?
+ false
+ end
+
private
def closest_namespace_setting(name)
diff --git a/app/models/project_services/alerts_service.rb b/app/models/project_services/alerts_service.rb
new file mode 100644
index 00000000000..2f7902d9617
--- /dev/null
+++ b/app/models/project_services/alerts_service.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+class AlertsService < Service
+ has_one :data, class_name: 'AlertsServiceData', autosave: true,
+ inverse_of: :service, foreign_key: :service_id
+
+ attribute :token, :string
+ delegate :token, :token=, :token_changed?, :token_was, to: :data
+
+ validates :token, presence: true, if: :activated?
+
+ before_validation :prevent_token_assignment
+ before_validation :ensure_token, if: :activated?
+
+ def url
+ url_helpers.project_alerts_notify_url(project, format: :json)
+ end
+
+ def json_fields
+ super + %w(token)
+ end
+
+ def editable?
+ false
+ end
+
+ def show_active_box?
+ false
+ end
+
+ def can_test?
+ false
+ end
+
+ def title
+ _('Alerts endpoint')
+ end
+
+ def description
+ _('Receive alerts on GitLab from any source')
+ end
+
+ def detailed_description
+ description
+ end
+
+ def self.to_param
+ 'alerts'
+ end
+
+ def self.supported_events
+ %w()
+ end
+
+ def data
+ super || build_data
+ end
+
+ private
+
+ def prevent_token_assignment
+ self.token = token_was if token.present? && token_changed?
+ end
+
+ def ensure_token
+ self.token = generate_token if token.blank?
+ end
+
+ def generate_token
+ SecureRandom.hex
+ end
+
+ def url_helpers
+ Gitlab::Routing.url_helpers
+ end
+end
diff --git a/app/models/project_services/alerts_service_data.rb b/app/models/project_services/alerts_service_data.rb
new file mode 100644
index 00000000000..5a52ed83455
--- /dev/null
+++ b/app/models/project_services/alerts_service_data.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+class AlertsServiceData < ApplicationRecord
+ belongs_to :service, class_name: 'AlertsService'
+
+ validates :service, presence: true
+
+ attr_encrypted :token,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm'
+end
diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb
new file mode 100644
index 00000000000..4ca3b154e4b
--- /dev/null
+++ b/app/services/projects/alerting/notify_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Projects
+ module Alerting
+ class NotifyService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute(token)
+ return forbidden unless alerts_service_activated?
+ return unauthorized unless valid_token?(token)
+
+ process_incident_issues
+
+ ServiceResponse.success
+ rescue Gitlab::Alerting::NotificationPayloadParser::BadPayloadError
+ bad_request
+ end
+
+ private
+
+ delegate :alerts_service, :alerts_service_activated?, to: :project
+
+ def process_incident_issues
+ IncidentManagement::ProcessAlertWorker
+ .perform_async(project.id, parsed_payload)
+ end
+
+ def parsed_payload
+ Gitlab::Alerting::NotificationPayloadParser.call(params.to_h)
+ end
+
+ def valid_token?(token)
+ token == alerts_service.token
+ end
+
+ def bad_request
+ ServiceResponse.error(message: 'Bad Request', http_status: 400)
+ end
+
+ def unauthorized
+ ServiceResponse.error(message: 'Unauthorized', http_status: 401)
+ end
+
+ def forbidden
+ ServiceResponse.error(message: 'Forbidden', http_status: 403)
+ end
+ end
+ end
+end
diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb
index 88ff3c2c9df..d19f275e928 100644
--- a/app/services/projects/container_repository/delete_tags_service.rb
+++ b/app/services/projects/container_repository/delete_tags_service.rb
@@ -14,12 +14,25 @@ module Projects
private
+ # Delete tags by name with a single DELETE request. This is only supported
+ # by the GitLab Container Registry fork. See
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23325 for details.
+ def fast_delete(container_repository, tag_names)
+ deleted_tags = tag_names.select do |name|
+ container_repository.delete_tag_by_name(name)
+ end
+
+ deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags')
+ end
+
# Replace a tag on the registry with a dummy tag.
# This is a hack as the registry doesn't support deleting individual
# tags. This code effectively pushes a dummy image and assigns the tag to it.
# This way when the tag is deleted only the dummy image is affected.
+ # This is used to preverse compatibility with third-party registries that
+ # don't support fast delete.
# See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
- def smart_delete(container_repository, tag_names)
+ def slow_delete(container_repository, tag_names)
# generates the blobs for the dummy image
dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path)
return error('could not generate manifest') if dummy_manifest.nil?
@@ -36,6 +49,15 @@ module Projects
end
end
+ def smart_delete(container_repository, tag_names)
+ fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true)
+ if fast_delete_enabled && container_repository.client.supports_tag_delete?
+ fast_delete(container_repository, tag_names)
+ else
+ slow_delete(container_repository, tag_names)
+ end
+ end
+
# update the manifests of the tags with the new dummy image
def replace_tag_manifests(container_repository, dummy_manifest, tag_names)
deleted_tags = {}
diff --git a/app/services/projects/lsif_data_service.rb b/app/services/projects/lsif_data_service.rb
index 00103f364bf..1282a0736e7 100644
--- a/app/services/projects/lsif_data_service.rb
+++ b/app/services/projects/lsif_data_service.rb
@@ -2,7 +2,8 @@
module Projects
class LsifDataService
- attr_reader :file, :project, :path, :commit_id
+ attr_reader :file, :project, :path, :commit_id,
+ :docs, :doc_ranges, :ranges, :def_refs
CACHE_EXPIRE_IN = 1.hour
@@ -14,19 +15,18 @@ module Projects
end
def execute
- docs, doc_ranges, ranges =
- fetch_data.values_at('docs', 'doc_ranges', 'ranges')
-
- doc_id = doc_id_from(docs)
+ fetch_data!
doc_ranges[doc_id]&.map do |range_id|
- line_data, column_data = ranges[range_id]['loc']
+ location, ref_id = ranges[range_id].values_at('loc', 'ref_id')
+ line_data, column_data = location
{
start_line: line_data.first,
end_line: line_data.last,
start_char: column_data.first,
- end_char: column_data.last
+ end_char: column_data.last,
+ definition_url: definition_url_for(def_refs[ref_id])
}
end
end
@@ -47,8 +47,17 @@ module Projects
end
end
- def doc_id_from(docs)
- docs.reduce(nil) do |doc_id, (id, doc_path)|
+ def fetch_data!
+ data = fetch_data
+
+ @docs = data['docs']
+ @doc_ranges = data['doc_ranges']
+ @ranges = data['ranges']
+ @def_refs = data['def_refs']
+ end
+
+ def doc_id
+ @doc_id ||= docs.reduce(nil) do |doc_id, (id, doc_path)|
next doc_id unless doc_path =~ /#{path}$/
if doc_id.nil? || docs[doc_id].size > doc_path.size
@@ -58,5 +67,24 @@ module Projects
doc_id
end
end
+
+ def dir_absolute_path
+ @dir_absolute_path ||= docs[doc_id]&.delete_suffix(path)
+ end
+
+ def definition_url_for(ref_id)
+ return unless range = ranges[ref_id]
+
+ def_doc_id, location = range.values_at('doc_id', 'loc')
+ localized_doc_url = docs[def_doc_id].delete_prefix(dir_absolute_path)
+
+ # location is stored as [[start_line, end_line], [start_char, end_char]]
+ start_line = location.first.first
+
+ line_anchor = "L#{start_line + 1}"
+ definition_ref_path = [commit_id, localized_doc_url].join('/')
+
+ Gitlab::Routing.url_helpers.project_blob_path(project, definition_ref_path, anchor: line_anchor)
+ end
end
end
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 93a43b5d1ea..b38449b3ab9 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -7,20 +7,7 @@
%p
= _("Measured in bytes of code. Excludes generated and vendored code.")
- .row
- .col-md-4
- %ul.bordered-list
- - @languages.each do |language|
- %li
- %span{ style: "color: #{language[:color]}" }
- = icon('circle')
- &nbsp;
- = language[:label]
- .float-right
- = language[:value]
- \%
- .col-md-8
- %canvas#languages-chart{ height: 400 }
+ #js-languages-chart{ data: { chart_data: @languages.to_json.html_safe } }
.repo-charts
.sub-header-block.border-top
@@ -60,27 +47,18 @@
%p.slead
= _("Commits per day of month")
%div
- %canvas#month-chart
+ #js-month-chart{ data: { chart_data: @commits_per_month.to_json.html_safe } }
.row
.col-md-6
.col-md-6
%p.slead
= _("Commits per weekday")
%div
- %canvas#weekday-chart
+ #js-weekday-chart{ data: { chart_data: @commits_per_week_days.to_json.html_safe } }
.row
.col-md-6
.col-md-6
%p.slead
= _("Commits per day hour (UTC)")
%div
- %canvas#hour-chart
-
--# haml-lint:disable InlineJavaScript
-%script#projectChartData{ type: "application/json" }
- - projectChartData = {};
- - projectChartData['hour'] = @commits_per_time
- - projectChartData['weekDays'] = @commits_per_week_days
- - projectChartData['month'] = @commits_per_month
- - projectChartData['languages'] = @languages
- = projectChartData.to_json.html_safe
+ #js-hour-chart{ data: { chart_data: @commits_per_time.to_json.html_safe } }