Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-02-09 21:11:21 +00:00
parent ed0bfef6b7
commit d336f9902a
61 changed files with 1605 additions and 1277 deletions

View File

@ -11,11 +11,14 @@ import { __ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import { xAxisLabelFormatter, dateFormatter } from '../utils';
import { MASTER_CHART_HEIGHT } from '../constants';
import ContributorAreaChart from './contributor_area_chart.vue';
import IndividualChart from './individual_chart.vue';
const GRAPHS_PATH_REGEX = /^(.*?)\/-\/graphs/g;
export default {
MASTER_CHART_HEIGHT,
i18n: {
history: __('History'),
refSelectorTranslations: {
@ -27,6 +30,7 @@ export default {
GlButton,
GlLoadingIcon,
ContributorAreaChart,
IndividualChart,
RefSelector,
},
props: {
@ -52,9 +56,8 @@ export default {
return {
masterChart: null,
individualCharts: [],
individualChartZoom: {},
svgs: {},
masterChartHeight: 264,
individualChartHeight: 216,
selectedBranch: this.branch,
};
},
@ -195,23 +198,13 @@ export default {
});
})
.catch(() => {});
this.masterChart.on('datazoom', debounce(this.setIndividualChartsZoom, 200));
},
onIndividualChartCreated(chart) {
this.individualCharts.push(chart);
},
setIndividualChartsZoom(options) {
this.charts.forEach((chart) =>
chart.setOption(
{
dataZoom: {
start: options.start,
end: options.end,
show: false,
},
},
{ lazyUpdate: true },
),
this.masterChart.on(
'datazoom',
debounce(() => {
const [{ startValue, endValue }] = this.masterChart.getOption().dataZoom;
this.individualChartZoom = { startValue, endValue };
}, 200),
);
},
visitBranch(selected) {
@ -230,7 +223,7 @@ export default {
</div>
<template v-else-if="showChart">
<div class="gl-border-b gl-border-gray-100 gl-mb-6 gl-bg-gray-10 gl-p-5">
<div class="gl-border-b gl-border-gray-100 gl-mb-6 gl-bg-gray-10 gl-py-5">
<div class="gl-display-flex">
<div class="gl-mr-3">
<ref-selector
@ -246,35 +239,25 @@ export default {
</gl-button>
</div>
</div>
<div data-testid="contributors-charts">
<h4 class="gl-mb-2 gl-mt-5">{{ __('Commits to') }} {{ branch }}</h4>
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
<contributor-area-chart
class="gl-mb-5"
:data="masterChartData"
:option="masterChartOptions"
:height="masterChartHeight"
@created="onMasterChartCreated"
/>
<div class="row">
<div
v-for="(contributor, index) in individualChartsData"
:key="index"
class="col-lg-6 col-12 gl-my-5"
>
<h4 class="gl-mb-2 gl-mt-0">{{ contributor.name }}</h4>
<p class="gl-mb-3">
{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})
</p>
<contributor-area-chart
:data="contributor.dates"
:option="individualChartOptions"
:height="individualChartHeight"
@created="onIndividualChartCreated"
/>
</div>
</div>
<h4 class="gl-mb-2 gl-mt-5">{{ __('Commits to') }} {{ branch }}</h4>
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
<contributor-area-chart
class="gl-mb-5"
:data="masterChartData"
:option="masterChartOptions"
:height="$options.MASTER_CHART_HEIGHT"
@created="onMasterChartCreated"
/>
<div class="row">
<individual-chart
v-for="(contributor, index) in individualChartsData"
:key="index"
:contributor="contributor"
:chart-options="individualChartOptions"
:zoom="individualChartZoom"
/>
</div>
</template>
</div>

View File

@ -0,0 +1,86 @@
<script>
import { isNumber } from 'lodash';
import { isInTimePeriod } from '~/lib/utils/datetime/date_calculation_utility';
import { INDIVIDUAL_CHART_HEIGHT } from '../constants';
import ContributorAreaChart from './contributor_area_chart.vue';
export default {
INDIVIDUAL_CHART_HEIGHT,
components: {
ContributorAreaChart,
},
props: {
contributor: {
type: Object,
required: true,
},
chartOptions: {
type: Object,
required: true,
},
zoom: {
type: Object,
required: true,
},
},
data() {
return {
chart: null,
};
},
computed: {
hasZoom() {
const { startValue, endValue } = this.zoom;
return isNumber(startValue) && isNumber(endValue);
},
commitCount() {
if (!this.hasZoom) return this.contributor.commits;
const start = new Date(this.zoom.startValue);
const end = new Date(this.zoom.endValue);
return this.contributor.dates[0].data
.filter(([date, count]) => count > 0 && isInTimePeriod(new Date(date), start, end))
.map(([, count]) => count)
.reduce((acc, count) => acc + count, 0);
},
},
watch: {
chart() {
this.syncChartZoom();
},
zoom() {
this.syncChartZoom();
},
},
methods: {
onChartCreated(chart) {
this.chart = chart;
},
syncChartZoom() {
if (!this.hasZoom || !this.chart) return;
const { startValue, endValue } = this.zoom;
this.chart.setOption(
{ dataZoom: { startValue, endValue, show: false } },
{ lazyUpdate: true },
);
},
},
};
</script>
<template>
<div class="col-lg-6 col-12 gl-my-5">
<h4 class="gl-mb-2 gl-mt-0" data-testid="chart-header">{{ contributor.name }}</h4>
<p class="gl-mb-3" data-testid="commit-count">
{{ n__('%d commit', '%d commits', commitCount) }} ({{ contributor.email }})
</p>
<contributor-area-chart
:data="contributor.dates"
:option="chartOptions"
:height="$options.INDIVIDUAL_CHART_HEIGHT"
@created="onChartCreated"
/>
</div>
</template>

View File

@ -0,0 +1,2 @@
export const MASTER_CHART_HEIGHT = 264;
export const INDIVIDUAL_CHART_HEIGHT = 216;

View File

@ -667,6 +667,17 @@ export const isInFuture = (date) =>
*/
export const fallsBefore = (dateA, dateB) => differenceInMilliseconds(dateA, dateB) > 0;
/**
* Checks whether date falls in the `start -> end` time period.
*
* @param {Date} date
* @param {Date} start
* @param {Date} end
* @return {Boolean} Returns true if date falls in the time period, otherwise false
*/
export const isInTimePeriod = (date, start, end) =>
differenceInMilliseconds(start, date) >= 0 && differenceInMilliseconds(date, end) >= 0;
/**
* Removes the time component of the date.
*

View File

@ -72,7 +72,7 @@ export default class ActivityCalendar {
this.clickDay = this.clickDay.bind(this);
this.currentSelectedDate = '';
this.daySpace = 1;
this.daySize = 15;
this.daySize = 14;
this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = [
__('Jan'),
@ -131,7 +131,6 @@ export default class ActivityCalendar {
this.renderDays();
this.renderMonths();
this.renderDayTitles();
this.renderKey();
}
// Add extra padding for the last month label if it is also the last column
@ -153,7 +152,7 @@ export default class ActivityCalendar {
.select(container)
.append('svg')
.attr('width', width)
.attr('height', 169)
.attr('height', 140)
.attr('class', 'contrib-calendar')
.attr('data-testid', 'contrib-calendar');
}
@ -257,25 +256,6 @@ export default class ActivityCalendar {
.text((date) => this.monthNames[date.month]);
}
renderKey() {
this.svg
.append('g')
.attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`)
.selectAll('rect')
.data(CONTRIB_LEGENDS)
.enter()
.append('rect')
.attr('width', this.daySize)
.attr('height', this.daySize)
.attr('x', (_, i) => this.daySizeWithSpace * i)
.attr('y', 0)
.attr('data-level', (_, i) => i)
.attr('class', 'user-contrib-cell has-tooltip contrib-legend')
.attr('title', (x) => x.title)
.attr('data-container', 'body')
.attr('data-html', true);
}
clickDay(stamp) {
if (this.currentSelectedDate !== stamp.date) {
this.currentSelectedDate = stamp.date;

View File

@ -1,8 +1,8 @@
// TODO: Remove this with the removal of the old navigation.
// See https://gitlab.com/gitlab-org/gitlab/-/issues/435899.
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import initReadMore from '~/read_more';
import Activities from '~/activities';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
@ -66,18 +66,35 @@ import UserOverviewBlock from './user_overview_block';
const CALENDAR_TEMPLATE = `
<div class="calendar">
<div class="js-contrib-calendar"></div>
<div class="calendar-hint"></div>
<div class="js-contrib-calendar gl-overflow-x-auto"></div>
<div class="calendar-help gl-display-flex gl-justify-content-space-between gl-ml-auto gl-mr-auto">
<div class="calendar-legend">
<svg width="80px" height="20px">
<g>
<rect width="13" height="13" x="2" y="2" data-level="0" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
'No contributions',
)}" data-container="body"></rect>
<rect width="13" height="13" x="17" y="2" data-level="1" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
'1-9 contributions',
)}" data-container="body"></rect>
<rect width="13" height="13" x="32" y="2" data-level="2" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
'10-19 contributions',
)}" data-container="body"></rect>
<rect width="13" height="13" x="47" y="2" data-level="3" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
'20-29 contributions',
)}" data-container="body"></rect>
<rect width="13" height="13" x="62" y="2" data-level="4" class="user-contrib-cell has-tooltip contrib-legend" title="${__(
'30+ contributions',
)}" data-container="body"></rect>
</g>
</svg>
</div>
<div class="calendar-hint gl-font-sm gl-text-secondary"></div>
</div>
</div>
`;
const CALENDAR_PERIOD_6_MONTHS = 6;
const CALENDAR_PERIOD_12_MONTHS = 12;
/* computation based on
* width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
* (see activity_calendar.js)
*/
const OVERVIEW_CALENDAR_BREAKPOINT = 918;
export default class UserTabs {
constructor({ defaultAction, action, parentEl }) {
@ -105,12 +122,6 @@ export default class UserTabs {
.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', (event) => this.tabShown(event))
.on('click', '.gl-pagination a', (event) => this.changeProjectsPage(event));
window.addEventListener('resize', () => this.onResize());
}
onResize() {
this.loadActivityCalendar();
}
changeProjectsPage(e) {
@ -194,19 +205,25 @@ export default class UserTabs {
return;
}
initReadMore();
this.loadActivityCalendar();
UserTabs.renderMostRecentBlocks('#js-overview .activities-block', {
requestParams: { limit: 15 },
});
UserTabs.renderMostRecentBlocks('#js-overview .projects-block', {
requestParams: { limit: 10, skip_pagination: true, skip_namespace: true, compact_mode: true },
requestParams: { limit: 3, skip_pagination: true, skip_namespace: true, card_mode: true },
});
this.loaded.overview = true;
}
static renderMostRecentBlocks(container, options) {
if ($(container).length === 0) {
return;
}
// eslint-disable-next-line no-new
new UserOverviewBlock({
container,
@ -218,8 +235,6 @@ export default class UserTabs {
loadActivityCalendar() {
const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar');
if (!$calendarWrap.length || bp.getBreakpointSize() === 'xs') return;
const calendarPath = $calendarWrap.data('calendarPath');
AjaxCache.retrieve(calendarPath)
@ -240,7 +255,6 @@ export default class UserTabs {
}
static renderActivityCalendar(data, $calendarWrap) {
const monthsAgo = UserTabs.getVisibleCalendarPeriod($calendarWrap);
const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath');
const utcOffset = $calendarWrap.data('utcOffset');
const calendarHint = __('Issues, merge requests, pushes, and comments.');
@ -257,8 +271,12 @@ export default class UserTabs {
calendarActivitiesPath,
utcOffset,
firstDayOfWeek: gon.first_day_of_week,
monthsAgo,
CALENDAR_PERIOD_12_MONTHS,
});
// Scroll to end
const calendarContainer = document.querySelector('.js-contrib-calendar');
calendarContainer.scrollLeft = calendarContainer.scrollWidth;
}
toggleLoading(status) {
@ -282,11 +300,4 @@ export default class UserTabs {
getCurrentAction() {
return this.$parentEl.find('.nav-links a.active').data('action');
}
static getVisibleCalendarPeriod($calendarWrap) {
const width = $calendarWrap.width();
return width < OVERVIEW_CALENDAR_BREAKPOINT
? CALENDAR_PERIOD_6_MONTHS
: CALENDAR_PERIOD_12_MONTHS;
}
}

View File

@ -1,12 +1,8 @@
<script>
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { debounce } from 'lodash';
import { __ } from '~/locale';
import AjaxCache from '~/lib/utils/ajax_cache';
import ActivityCalendar from '~/pages/users/activity_calendar';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { getVisibleCalendarPeriod } from '../utils';
export default {
@ -20,26 +16,14 @@ export default {
data() {
return {
isLoading: true,
showCalendar: true,
hasError: false,
};
},
mounted() {
this.renderActivityCalendar();
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize);
},
methods: {
async renderActivityCalendar() {
if (bp.getBreakpointSize() === 'xs') {
this.showCalendar = false;
return;
}
this.showCalendar = true;
this.isLoading = true;
this.hasError = false;
@ -66,9 +50,6 @@ export default {
this.hasError = true;
}
},
handleResize: debounce(function debouncedHandleResize() {
this.renderActivityCalendar();
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
handleClickDay() {
// Render activities for specific day.
// Blocked by https://gitlab.com/gitlab-org/gitlab/-/issues/378695
@ -78,8 +59,8 @@ export default {
</script>
<template>
<div v-if="showCalendar" ref="calendarContainer">
<gl-loading-icon v-if="isLoading" size="md" />
<div ref="calendarContainer" class="gl-pb-5 gl-border-b">
<gl-loading-icon v-if="isLoading" size="sm" />
<gl-alert
v-else-if="hasError"
:title="$options.i18n.errorAlertTitle"
@ -88,13 +69,11 @@ export default {
:primary-button-text="$options.i18n.retry"
@primaryAction="renderActivityCalendar"
/>
<div v-else class="gl-text-center">
<div class="gl-display-inline-block gl-relative">
<div ref="calendarSvgContainer"></div>
<p class="gl-absolute gl-right-0 gl-bottom-0 gl-mb-0 gl-font-sm">
{{ $options.i18n.calendarHint }}
</p>
</div>
<div v-else class="gl-display-inline-block gl-relative gl-w-full">
<div ref="calendarSvgContainer"></div>
<p class="gl-absolute gl-right-0 gl-bottom-0 gl-mb-0 gl-font-sm gl-text-secondary">
{{ $options.i18n.calendarHint }}
</p>
</div>
</div>
</template>

View File

@ -54,19 +54,19 @@ export default {
<template>
<gl-tab :title="$options.i18n.title">
<activity-calendar />
<div class="gl-mx-n5 gl-display-flex gl-flex-wrap">
<div class="gl-px-5 gl-w-full gl-lg-w-half" data-testid="activity-section">
<div class="gl-mt-5 gl-display-flex gl-flex-wrap">
<div class="gl-w-full" data-testid="activity-section">
<div
class="gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
>
<h4 class="gl-flex-grow-1">{{ $options.i18n.activity }}</h4>
<gl-link href="">{{ $options.i18n.viewAll }}</gl-link>
</div>
<activity-calendar />
<gl-loading-icon v-if="eventsLoading" class="gl-mt-5" size="md" />
<contribution-events v-else :events="events" />
</div>
<div class="gl-px-5 gl-w-full gl-lg-w-half" data-testid="personal-projects-section">
<div class="gl-w-full" data-testid="personal-projects-section">
<div
class="gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
>

View File

@ -81,7 +81,7 @@ export default {
},
async mounted() {
try {
const response = await getUserProjects(this.userId, { per_page: 10 });
const response = await getUserProjects(this.userId, { per_page: 3 });
this.personalProjects = convertObjectPropsToCamelCase(response.data, { deep: true }).map(
(project) => {
// This API does not return the `visibility` key if user is signed out.

View File

@ -28,6 +28,11 @@ export default {
},
},
},
computed: {
hasUserAchievements() {
return Boolean(this.userAchievements?.length);
},
},
methods: {
processNodes(nodes) {
return Object.entries(groupBy(nodes, 'achievement.id'))
@ -67,12 +72,16 @@ export default {
i18n: {
awardedBy: s__('Achievements|Awarded %{timeAgo} by %{namespace}'),
awardedByUnknownNamespace: s__('Achievements|Awarded %{timeAgo} by a private namespace'),
achievementsLabel: s__('Achievements|Achievements'),
},
};
</script>
<template>
<div class="gl-mb-3">
<div v-if="hasUserAchievements">
<h2 class="gl-font-base gl-mb-2 gl-mt-4">
{{ $options.i18n.achievementsLabel }}
</h2>
<div
v-for="userAchievement in userAchievements"
:key="userAchievement.id"
@ -85,7 +94,7 @@ export default {
:size="32"
tabindex="0"
shape="rect"
class="gl-mx-2 gl-p-1 gl-border-none"
class="gl-mr-2 gl-p-1 gl-border-none"
/>
<br />
<gl-badge v-if="showCountBadge(userAchievement.count)" variant="info" size="sm">{{

View File

@ -165,7 +165,7 @@ export default {
:id="`L${line}`"
:key="line"
class="gl-user-select-none gl-shadow-none! file-line-num"
:href="`#LC${line}`"
:href="`#L${line}`"
:data-line-number="line"
@click="scrollToLine(`#LC${line}`)"
>

View File

@ -29,97 +29,16 @@
}
}
.calendar-block {
padding-left: 0;
padding-right: 0;
border-top: 0;
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
overflow-x: auto;
}
.calendar-help {
// Match width of calendar
max-width: 864px;
}
.calendar-hint {
font-size: 12px;
direction: ltr;
margin-top: -23px;
float: right;
}
.cover-block {
text-align: center;
background: var(--gray-50, $gray-light);
padding-top: 44px;
position: relative;
.avatar-holder {
.avatar,
.identicon {
margin: 0 auto;
float: none;
}
.identicon {
border-radius: 50%;
}
}
.cover-title {
color: var(--gl-text-color, $gl-text-color);
font-size: 23px;
h1 {
color: var(--gl-text-color, $gl-text-color);
margin-bottom: 6px;
font-size: 23px;
}
.visibility-icon {
display: inline-block;
margin-left: 5px;
font-size: 18px;
color: color('gray');
}
p {
padding: 0 $gl-padding;
color: var(--gl-text-color, $gl-text-color);
}
}
.cover-controls {
@include media-breakpoint-up(sm) {
position: absolute;
top: 1rem;
right: 1.25rem;
}
&.left {
@include media-breakpoint-up(sm) {
left: 1.25rem;
right: auto;
}
}
}
&.user-cover-block {
padding: 24px 0 0;
.nav-links {
width: 100%;
float: none;
&.scrolling-tabs {
float: none;
}
}
li:first-child {
margin-left: auto;
}
li:last-child {
margin-right: auto;
.user-profile-image {
.gl-avatar {
@include media-breakpoint-up(md) {
height: 6.5rem;
width: 6.5rem;
}
}
}
@ -140,13 +59,10 @@
max-width: 600px;
}
.user-calendar {
text-align: center;
min-height: 172px;
.calendar {
display: inline-block;
}
.profile-readme-wrapper .read-more-trigger {
bottom: 0;
left: 1px;
right: 1px;
}
.user-calendar-activities {
@ -158,14 +74,17 @@
}
.user-contrib-text {
font-size: 12px;
font-size: 11px;
fill: $calendar-user-contrib-text;
}
.user-profile {
.profile-header {
.avatar-holder {
margin: 0 auto 10px;
@include media-breakpoint-up(lg) {
.profile-header {
position: sticky;
top: $calc-application-header-height;
height: $calc-application-viewport-height;
padding-left: $gl-spacing-scale-2;
}
}
@ -189,23 +108,9 @@
.gl-label-scoped {
--label-inset-border: inset 0 0 0 1px currentColor;
}
@include media-breakpoint-up(lg) {
margin-right: 5px;
}
}
.projects-block {
@include media-breakpoint-up(lg) {
margin-left: 5px;
}
}
@include media-breakpoint-down(xs) {
.cover-block {
padding-top: 20px;
}
.user-profile-nav {
a {
margin-right: 0;
@ -268,3 +173,45 @@
border: 0;
}
}
.user-profile {
position: relative;
@include media-breakpoint-up(lg) {
display: grid;
grid-template-columns: 1fr $right-sidebar-width;
gap: 2rem;
}
}
.user-profile-sidebar {
z-index: 2;
}
.user-profile-sidebar,
.user-profile-content {
min-width: 1px; // grid overflow fix
}
// Home panel show profile sidebar
// information on top
.user-profile {
@include media-breakpoint-down(md) {
display: flex;
flex-direction: column;
.user-overview-page.active {
display: flex;
flex-wrap: wrap;
.user-profile-content {
flex-basis: 100%;
}
}
.user-profile-sidebar {
order: -1;
flex-basis: 100%;
}
}
}

View File

@ -325,14 +325,6 @@
@include media-breakpoint-up(lg) {
justify-content: flex-start;
padding-right: $gl-spacing-scale-9;
&:not(.with-pipeline-status) {
.icon-wrapper:first-of-type {
@include media-breakpoint-up(lg) {
margin-left: $gl-spacing-scale-7;
}
}
}
}
}
@ -569,3 +561,8 @@
}
}
}
.projects-list .description p {
@include gl-line-clamp-2;
margin-bottom: 0;
}

View File

@ -143,13 +143,14 @@ class UsersController < ApplicationController
skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination])
skip_namespace = Gitlab::Utils.to_boolean(params[:skip_namespace])
compact_mode = Gitlab::Utils.to_boolean(params[:compact_mode])
card_mode = Gitlab::Utils.to_boolean(params[:card_mode])
respond_to do |format|
format.html { render 'show' }
format.json do
projects = yield
pager_json("shared/projects/_list", projects.count, projects: projects, skip_pagination: skip_pagination, skip_namespace: skip_namespace, compact_mode: compact_mode)
pager_json("shared/projects/_list", projects.count, projects: projects, skip_pagination: skip_pagination, skip_namespace: skip_namespace, compact_mode: compact_mode, card_mode: card_mode)
end
end
end

View File

@ -59,7 +59,8 @@ class AddressableUrlValidator < ActiveModel::EachValidator
deny_all_requests_except_allowed: false,
enforce_user: false,
enforce_sanitization: false,
dns_rebind_protection: false
dns_rebind_protection: false,
outbound_local_requests_allowlist: []
}.freeze
DEFAULT_OPTIONS = BLOCKER_VALIDATE_OPTIONS.merge({
@ -112,6 +113,8 @@ class AddressableUrlValidator < ActiveModel::EachValidator
if deny_all_requests_except_allowed?
args[:deny_all_requests_except_allowed] = true
end
args[:outbound_local_requests_allowlist] = ApplicationSetting.current&.outbound_local_requests_whitelist || [] # rubocop:disable Naming/InclusiveLanguage -- existing setting
end
end

View File

@ -10,6 +10,7 @@
- remote = false unless local_assigns[:remote] == true
- skip_pagination = false unless local_assigns[:skip_pagination] == true
- compact_mode = false unless local_assigns[:compact_mode] == true
- card_mode = local_assigns[:card_mode] == true
- css_classes = "#{'compact' if compact_mode} #{'explore' if explore_projects_tab?}"
- contributed_projects_current_user_empty_message_header = s_('UserProfile|Explore public groups to find projects to contribute to.')
- contributed_projects_visitor_empty_message = s_('UserProfile|This user hasn\'t contributed to any projects')
@ -33,14 +34,24 @@
- load_pipeline_status(projects) if pipeline_status
- load_max_project_member_accesses(projects) # Prime cache used in shared/projects/project view rendered below
- load_catalog_resources(projects)
%ul.projects-list.gl-text-secondary.gl-w-full.gl-my-2{ class: css_classes }
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, use_creator_avatar: use_creator_avatar,
forks: able_to_see_forks_count?(project, user), show_last_commit_as_description: show_last_commit_as_description,
user: user, merge_requests: able_to_see_merge_requests?(project, user), issues: able_to_see_issues?(project, user),
pipeline_status: pipeline_status, compact_mode: compact_mode
- if card_mode
.projects-list.gl-text-secondary.gl-w-full.gl-display-flex.gl-flex-direction-column.gl-lg-flex-direction-row.gl-gap-4.gl-overflow-x-auto{ class: css_classes }
- projects.take(projects_limit).each_with_index do |project, i| # rubocop: disable CodeReuse/ActiveRecord -- it's Enumerable#take
= render "shared/projects/project_card", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, use_creator_avatar: use_creator_avatar,
forks: able_to_see_forks_count?(project, user), show_last_commit_as_description: show_last_commit_as_description,
user: user, merge_requests: able_to_see_merge_requests?(project, user), issues: able_to_see_issues?(project, user),
pipeline_status: pipeline_status, compact_mode: compact_mode
- else
%ul.projects-list.gl-text-secondary.gl-w-full.gl-my-2{ class: css_classes }
- projects.take(projects_limit).each_with_index do |project, i| # rubocop: disable CodeReuse/ActiveRecord -- it's Enumerable#take
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, use_creator_avatar: use_creator_avatar,
forks: able_to_see_forks_count?(project, user), show_last_commit_as_description: show_last_commit_as_description,
user: user, merge_requests: able_to_see_merge_requests?(project, user), issues: able_to_see_issues?(project, user),
pipeline_status: pipeline_status, compact_mode: compact_mode
= paginate_collection(projects, remote: remote) unless skip_pagination
- else
- if @contributed_projects

View File

@ -1,31 +1,34 @@
- avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false
- forks = true unless local_assigns[:forks] == false
- merge_requests = true unless local_assigns[:merge_requests] == false
- issues = true unless local_assigns[:issues] == false
- pipeline_status = true unless local_assigns[:pipeline_status] == false
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- access = max_project_member_access(project)
- compact_mode = false unless local_assigns[:compact_mode] == true
- avatar = local_assigns[:avatar].nil? || local_assigns[:avatar]
- stars = local_assigns[:stars].nil? || local_assigns[:stars]
- forks = local_assigns[:forks].nil? || local_assigns[:forks]
- merge_requests = local_assigns[:merge_requests].nil? || local_assigns[:merge_requests]
- issues = local_assigns[:issues].nil? || local_assigns[:issues]
- pipeline_status = local_assigns[:pipeline_status].nil? || local_assigns[:pipeline_status]
- skip_namespace = local_assigns[:skip_namespace]
- compact_mode = local_assigns[:compact_mode]
- use_creator_avatar = local_assigns[:use_creator_avatar]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
- access = max_project_member_access(project)
- css_class = "gl-sm-display-flex gl-align-items-center gl-vertical-align-middle!" if project.description.blank? && !show_last_commit_as_description
- updated_tooltip = time_ago_with_tooltip(project.last_activity_at || project.updated_at)
- show_pipeline_status_icon = pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
- last_pipeline = last_pipeline_from_status_cache(project) if show_pipeline_status_icon
- css_controls_class = "with-pipeline-status" if show_pipeline_status_icon && last_pipeline.present?
- css_metadata_classes = "gl-display-flex gl-align-items-center gl-ml-5 gl-reset-color! icon-wrapper has-tooltip"
- css_metadata_classes = "gl-display-flex gl-align-items-center gl-reset-color! icon-wrapper has-tooltip"
%li.project-row
- if avatar
.project-cell.gl-w-11
= link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar
= render Pajamas::AvatarComponent.new(project.creator, size: 48, alt: '', class: 'gl-mr-5')
- else
= render Pajamas::AvatarComponent.new(project, size: 48, alt: '', class: 'gl-mr-5')
.project-avatar-container.gl-mr-5.gl-relative.gl-pb-4
= link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar
= render Pajamas::AvatarComponent.new(project.creator, size: 48, alt: '')
- else
= render Pajamas::AvatarComponent.new(project, size: 48, alt: '')
.project-cell{ class: css_class }
.project-details.gl-pr-9.gl-sm-pr-0.gl-w-full.gl-display-flex.gl-flex-direction-column{ data: { testid: 'project-content', qa_project_name: project.name } }
.gl-display-flex.gl-align-items-center.gl-flex-wrap
.gl-display-flex.gl-align-items-baseline.gl-flex-wrap
%h2.gl-font-base.gl-line-height-20.gl-my-0.gl-overflow-wrap-anywhere
= link_to project_path(project), class: 'text-plain gl-mr-3 js-prefetch-document', title: project.name do
%span.namespace-name.gl-font-weight-normal
@ -83,32 +86,31 @@
= _('Updated')
= updated_tooltip
.project-cell{ class: "#{css_class} gl-display-none! gl-sm-display-table-cell!" }
.project-controls.gl-display-flex.gl-flex-direction-column.gl-align-items-flex-end.gl-w-full{ data: { testid: 'project_controls'} }
.controls.gl-display-flex.gl-align-items-center.gl-mb-2{ class: "#{css_controls_class} gl-pr-0!" }
- if show_pipeline_status_icon && last_pipeline.present?
- pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
%span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
.project-cell.project-controls{ class: "#{css_class} gl-display-none! gl-sm-display-table-cell!", data: { testid: 'project_controls'} }
.controls.gl-display-flex.gl-align-items-center.gl-mb-2.gl-gap-4{ class: "#{css_controls_class} gl-pr-0! gl-justify-content-end!" }
- if show_pipeline_status_icon && last_pipeline.present?
- pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
%span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
= render_if_exists 'shared/projects/archived', project: project
- if stars
= link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
= sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
= badge_count(project.star_count)
- if show_count?(disabled: !forks, compact_mode: compact_mode)
= link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do
= sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
= badge_count(project.forks_count)
- if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
= sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_merge_requests_count)
- if show_count?(disabled: !issues, compact_mode: compact_mode)
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do
= sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_issues_count)
.updated-note.gl-font-sm.gl-white-space-nowrap.gl-justify-content-end
%span
= _('Updated')
= updated_tooltip
= render_if_exists 'shared/projects/archived', project: project
- if stars
= link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
= sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
= badge_count(project.star_count)
- if show_count?(disabled: !forks, compact_mode: compact_mode)
= link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do
= sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
= badge_count(project.forks_count)
- if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
= sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_merge_requests_count)
- if show_count?(disabled: !issues, compact_mode: compact_mode)
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do
= sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_issues_count)
.updated-note.gl-font-sm.gl-white-space-nowrap.gl-justify-content-end
%span
= _('Updated')
= updated_tooltip

View File

@ -0,0 +1,91 @@
- avatar = local_assigns[:avatar].nil? || local_assigns[:avatar]
- stars = local_assigns[:stars].nil? || local_assigns[:stars]
- forks = local_assigns[:forks].nil? || local_assigns[:forks]
- merge_requests = local_assigns[:merge_requests].nil? || local_assigns[:merge_requests]
- issues = local_assigns[:issues].nil? || local_assigns[:issues]
- pipeline_status = local_assigns[:pipeline_status].nil? || local_assigns[:pipeline_status]
- skip_namespace = local_assigns[:skip_namespace]
- compact_mode = local_assigns[:compact_mode]
- use_creator_avatar = local_assigns[:use_creator_avatar]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
- updated_tooltip = time_ago_with_tooltip(project.last_activity_at || project.updated_at)
- show_pipeline_status_icon = pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
- last_pipeline = last_pipeline_from_status_cache(project) if show_pipeline_status_icon
- css_controls_class = "with-pipeline-status" if show_pipeline_status_icon && last_pipeline.present?
- css_metadata_classes = "gl-display-flex gl-align-items-center gl-ml-0! gl-reset-color! icon-wrapper has-tooltip"
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card gl-justify-content-space-between gl-lg-w-25p gl-flex-grow-1 gl-shrink-0 gl-mt-0' }, header_options: { class: 'gl-new-card-header gl-flex-grow-1 gl-align-items-flex-start gl-border-b-0 gl-px-4 gl-gap-5' }, body_options: { class: 'gl-new-card-body gl-px-4 gl-py-4' }) do |c|
- c.with_header do
- if avatar
.project-avatar-container.gl-relative.gl-pb-4
= link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar
= render Pajamas::AvatarComponent.new(project.creator, size: 48, alt: '')
- else
= render Pajamas::AvatarComponent.new(project, size: 48, alt: '')
.gl-w-full.gl-pt-2.gl-word-break-word
.gl-display-flex.gl-align-items-center.gl-flex-wrap
%h2.gl-font-base.gl-line-height-20.gl-my-0
= link_to project_path(project), class: 'text-plain gl-mr-3 js-prefetch-document', title: project.name do
%span.namespace-name.gl-font-weight-normal
- if project.namespace && !skip_namespace
= project.namespace.human_name
\/
%span.project-name<
= project.name
= visibility_level_content(project)
- if show_last_commit_as_description
.description.gl-display-none.gl-sm-display-block.gl-mt-2.gl-font-sm
= link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
- elsif project.description.present?
.description.gl-display-none.gl-sm-display-block.gl-mt-2.gl-font-sm
= markdown_field(project, :description)
- if project.topics.any?
.gl-mt-3.gl-ml-n1
= render "shared/projects/topics", project: project.present(current_user: current_user)
- if project.catalog_resource
= render partial: 'shared/ci_catalog_badge', locals: { href: project_ci_catalog_resource_path(project, project.catalog_resource) }
- if explore_projects_tab? && project_license_name(project)
%span.gl-display-inline-flex.gl-align-items-center.gl-mr-3
= sprite_icon('scale', size: 14, css_class: 'gl-mr-2')
= project_license_name(project)
- if !explore_projects_tab?
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project, additional_classes: 'gl-ml-3!'
- c.with_body do
.project-controls{ data: { testid: 'project_controls'} }
.gl-display-flex.gl-align-items-center.gl-gap-2.gl-mb-2.gl-justify-content-space-between.gl-flex-wrap
.controls.gl-display-flex.gl-align-items-center.gl-gap-4{ class: "#{css_controls_class} gl-pr-0!" }
- if stars
= link_to project_starrers_path(project), class: "#{css_metadata_classes} stars", title: _('Stars'), data: { container: 'body', placement: 'top' } do
= sprite_icon('star-o', size: 14, css_class: 'gl-mr-2')
= badge_count(project.star_count)
- if show_count?(disabled: !forks, compact_mode: compact_mode)
= link_to project_forks_path(project), class: "#{css_metadata_classes} forks", title: _('Forks'), data: { container: 'body', placement: 'top' } do
= sprite_icon('fork', size: 14, css_class: 'gl-mr-2')
= badge_count(project.forks_count)
- if show_count?(disabled: !merge_requests, compact_mode: compact_mode)
= link_to project_merge_requests_path(project), class: "#{css_metadata_classes} merge-requests", title: _('Merge requests'), data: { container: 'body', placement: 'top' } do
= sprite_icon('git-merge', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_merge_requests_count)
- if show_count?(disabled: !issues, compact_mode: compact_mode)
= link_to project_issues_path(project), class: "#{css_metadata_classes} issues", title: _('Issues'), data: { container: 'body', placement: 'top' } do
= sprite_icon('issues', size: 14, css_class: 'gl-mr-2')
= badge_count(project.open_issues_count)
.gl-display-flex.gl-align-items-center.gl-gap-2.gl-mr-n2
- if show_pipeline_status_icon && last_pipeline.present?
- pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
%span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
= render_if_exists 'shared/projects/archived', project: project
.updated-note.gl-font-sm.gl-white-space-nowrap.gl-justify-content-start
%span
= _('Updated')
= updated_tooltip

View File

@ -1,11 +1,9 @@
- link_classes = "flex-grow-1 gl-display-inline-block"
- if current_user&.following_users_allowed?(@user)
- if current_user.following?(@user)
= form_tag user_unfollow_path(@user, :json), class: link_classes do
= render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-w-full', data: { track_action: 'click_button', track_label: 'unfollow_from_profile' } }) do
= form_tag user_unfollow_path(@user, :json) do
= render Pajamas::ButtonComponent.new(type: :submit, button_options: { data: { track_action: 'click_button', track_label: 'unfollow_from_profile' } }) do
= _('Unfollow')
- else
= form_tag user_follow_path(@user, :json), class: link_classes do
= render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { class: 'gl-w-full', data: { testid: 'follow-user-link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do
= form_tag user_follow_path(@user, :json) do
= render Pajamas::ButtonComponent.new(variant: :confirm, type: :submit, button_options: { data: { testid: 'follow-user-link', track_action: 'click_button', track_label: 'follow_from_profile' } }) do
= _('Follow')

View File

@ -1,46 +1,46 @@
- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6 gl-align-self-start"
- if can?(current_user, :read_cross_project) && @user.user_readme&.rich_viewer
.profile-readme-wrapper.gl-relative.gl-overflow-hidden.gl-w-full.gl-pt-5
.profile-readme.read-more-container.gl-relative.justify-content-center.gl-border.gl-rounded-base.gl-overflow-hidden{ data: { 'read-more-height': 400 } }
.read-more-content.read-more-content--has-scrim.gl-py-5.gl-px-6
.gl-display-flex
= render Pajamas::BreadcrumbComponent.new(class: 'gl-mb-4') do |c|
- c.with_item(text: @user.username, href: project_path(@user.user_project))
- c.with_item(text: @user.user_readme.path, href: @user.user_project.readme_url)
.row.d-none.d-sm-flex
.col-12.calendar-block.gl-my-3
.user-calendar.light{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_timezone_instance(@user.timezone).now.utc_offset } }
= gl_loading_icon(size: 'md', css_class: 'gl-my-8')
.user-calendar-error.invisible
= _('There was an error loading users activity calendar.')
%a.js-retry-load{ href: '#' }
= s_('UserProfile|Retry')
- if @user.user_readme&.rich_viewer
.row.justify-content-center
.col-12.col-md-10.col-lg-8.gl-my-6
.gl-display-flex
= render Pajamas::BreadcrumbComponent.new(class: 'gl-mb-4') do |c|
- c.with_item(text: @user.username, href: project_path(@user.user_project))
- c.with_item(text: @user.user_readme.path, href: @user.user_project.readme_url)
- if current_user == @user
.gl-ml-auto
= link_to _('Edit file'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path)
= render 'projects/blob/viewer', viewer: @user.user_readme.rich_viewer, load_async: false
.js-read-more-trigger.read-more-trigger.gl-h-8.gl-absolute.gl-z-index-2.gl-bg-white.gl-px-6.gl-rounded-bottom-base
= render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'gl-mt-4 gl-ml-n1', 'aria-label': _("Expand Readme") }) do
= sprite_icon('chevron-down', size: 14, css_class: 'gl-mr-1 gl-mb-n1')
= _("Read more")
- if current_user == @user
.gl-ml-auto
= link_to _('Edit'), edit_blob_path(@user.user_project, @user.user_project.default_branch, @user.user_readme.path)
= render 'projects/blob/viewer', viewer: @user.user_readme.rich_viewer, load_async: false
.row
.col-12.user-calendar-activities
.row
%div{ class: activity_pane_class }
- if can?(current_user, :read_cross_project)
.activities-block
.gl-mt-5
.gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
%h4.gl-flex-grow-1
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
.overview-content-list.user-activity-content{ data: { href: user_activity_path, testid: 'user-activity-content' } }
= gl_loading_icon(size: 'md', css_class: 'loading')
- if can?(current_user, :read_cross_project)
.gl-align-self-start.gl-overflow-hidden
.activities-block
.gl-display-flex.gl-align-items-baseline
%h2.gl-heading-3.gl-flex-grow-1{ class: 'gl-mt-5! gl-mb-3!' }
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
.col-md-12.col-lg-6
.projects-block
.gl-mt-5
.gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
%h4.gl-flex-grow-1
= s_('UserProfile|Personal projects')
= link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_projects_path } }
= gl_loading_icon(size: 'md', css_class: 'loading')
.user-calendar.gl-border.light.gl-rounded-base.gl-px-3.gl-pt-4.gl-text-center{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: local_timezone_instance(@user.timezone).now.utc_offset } }
= gl_loading_icon(size: 'md', css_class: 'gl-my-8')
.user-calendar-error.invisible
= _('There was an error loading users activity calendar.')
= render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'js-retry-load' }) do
= s_('UserProfile|Retry')
.user-calendar-activities
.overview-content-list.user-activity-content.gl-mb-5{ data: { href: user_activity_path, testid: 'user-activity-content' } }
= gl_loading_icon(size: 'md', css_class: 'loading')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
- if @user.personal_projects.any?
.projects-block.gl-w-full
.gl-display-flex.gl-align-items-baseline
%h2.gl-heading-3.gl-flex-grow-1{ class: 'gl-mt-5! gl-mb-3!' }
= s_('UserProfile|Personal projects')
= link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_projects_path } }
= gl_loading_icon(size: 'md', css_class: 'loading')

View File

@ -1,4 +1,4 @@
- if current_user && current_user.admin?
= render Pajamas::ButtonComponent.new(href: [:admin, @user],
icon: 'user',
button_options: { class: 'gl-flex-grow-1 has-tooltip', title: s_('UserProfile|View user in admin area'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } })
button_options: { class: 'has-tooltip', title: s_('UserProfile|View user in admin area'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } })

View File

@ -12,105 +12,170 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
.user-profile
.cover-block.user-cover-block.gl-border-t.gl-border-b.gl-mt-n1
%div{ class: container_class }
.cover-controls.gl-display-flex.gl-gap-3.gl-pb-4
%div{ class: container_class }
.user-profile-header.gl-display-flex.gl-justify-content-space-between.gl-flex-direction-column.gl-md-flex-direction-row-reverse.gl-mt-5.gl-mb-2
%div
.cover-controls.gl-display-flex.gl-gap-3.gl-mb-4.gl-md-justify-content-end.gl-md-flex-direction-row-reverse
.js-user-profile-actions{ data: user_profile_actions_data(@user) }
= render 'users/follow_user'
-# The following edit button is mutually exclusive to the follow user button, they won't be shown together
- if @user == current_user
= render Pajamas::ButtonComponent.new(href: user_settings_profile_path,
button_options: { class: 'gl-flex-grow-1', title: s_('UserProfile|Edit profile') }) do
button_options: { title: s_('UserProfile|Edit profile') }) do
= s_("UserProfile|Edit profile")
= render 'users/view_gpg_keys'
= render 'users/view_user_in_admin_area'
.js-user-profile-actions{ data: user_profile_actions_data(@user) }
.gl-display-flex.gl-flex-direction-row.gl-align-items-flex-start.gl-md-align-items-center.gl-column-gap-5.gl-mt-2.gl-sm-mt-0
.user-image.gl-relative.gl-py-3
= link_to avatar_icon_for_user(@user, 400, current_user: current_user), class: "user-profile-image", target: '_blank', rel: 'noopener noreferrer', title: s_('UserProfile|View large avatar') do
= render Pajamas::AvatarComponent.new(@user, alt: s_('UserProfile|User profile picture'), size: 64, avatar_options: { itemprop: "image" })
- if @user.status&.busy?
= render Pajamas::BadgeComponent.new(s_('UserProfile|Busy'), size: 'sm', variant: 'warning', class: 'gl-absolute gl-display-flex gl-justify-content-center gl-align-items-center gl-bottom-0 gl-left-50p gl-bg-gray-50 gl-border gl-border-white gl-translate-x-n50')
%div
%h1.gl-heading-1.gl-line-height-1.gl-mr-2{ class: 'gl-my-0!', itemprop: 'name' }
= user_display_name(@user)
.gl-font-size-h2.gl-text-gray-600.gl-font-weight-normal.gl-my-0
= @user.to_reference
- if !@user.blocked? && @user.confirmed? && @user.status&.customized?
.gl-my-2.cover-status.gl-font-sm.gl-pt-2.gl-display-flex.gl-flex-direction-column
.gl-display-inline-flex.gl-gap-3.gl-align-items-baseline
= emoji_icon(@user.status.emoji)
= markdown_field(@user.status, :message)
.user-profile
.user-profile-content
- if !profile_tabs.empty? && Feature.enabled?(:profile_tabs_vue, current_user)
#js-profile-tabs{ data: user_profile_tabs_app_data(@user) }
- unless Feature.enabled?(:profile_tabs_vue, current_user)
.tab-content
- if profile_tab?(:overview)
#js-overview.tab-pane.user-overview-page
= render "users/overview"
.profile-header.gl-mx-5.gl-mb-4{ class: [('gl-mb-6' if profile_tabs.empty?)] }
.gl-display-inline-block.gl-mx-8.gl-vertical-align-top
.avatar-holder
= link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do
= render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" })
- if @user.achievements_enabled && Ability.allowed?(current_user, :read_user_profile, @user)
#js-user-achievements{ data: { root_url: root_url, user_id: @user.id } }
.gl-display-inline-block.gl-vertical-align-top.gl-text-left.gl-max-w-80
- if @user.blocked? || !@user.confirmed?
.user-info
%h1.cover-title.gl-my-0
= user_display_name(@user)
= render "users/profile_basic_info"
- else
.user-info
%h1.cover-title.gl-my-0{ itemprop: 'name' }
= @user.name
- if @user.pronouns.present?
%span.gl-font-base.gl-text-gray-500.gl-vertical-align-middle
= "(#{@user.pronouns})"
- if @user.status&.busy?
= render Pajamas::BadgeComponent.new(s_('UserProfile|Busy'), size: 'sm', variant: 'warning', class: 'gl-vertical-align-middle')
- if profile_tab?(:activity)
#activity.tab-pane
.flash-container
- if can?(current_user, :read_cross_project)
.content_list.user-activity-content{ data: { href: user_activity_path } }
.loading
= gl_loading_icon(size: 'md')
- unless @user.bot?
- if profile_tab?(:groups)
#groups.tab-pane
-# This tab is always loaded via AJAX
- if @user.pronunciation.present?
.gl-align-items-center
%p.gl-mb-4.gl-text-gray-500= s_("UserProfile|Pronounced as: %{pronunciation}") % { pronunciation: @user.pronunciation }
- if profile_tab?(:contributed)
#contributed.tab-pane
-# This tab is always loaded via AJAX
- if @user.status&.customized?
.cover-status.gl-display-inline-flex.gl-align-items-baseline.gl-mb-3
= emoji_icon(@user.status.emoji, class: 'gl-mr-2')
= markdown_field(@user.status, :message)
= render "users/profile_basic_info"
- user_local_time = local_time(@user.timezone)
- if @user.location.present? || user_local_time.present? || work_information(@user).present?
.gl-text-gray-900
- if profile_tab?(:projects)
#projects.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:starred)
#starred.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:snippets)
#snippets.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:followers)
#followers.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:following)
#following.tab-pane
-# This tab is always loaded via AJAX
.loading.hide
.gl-spinner.gl-spinner-md
- if profile_tabs.empty?
.svg-content
= image_tag 'illustrations/profile_private_mode.svg'
.text-content.text-center
%h4
- if @user.blocked?
= s_('UserProfile|This user is blocked')
- else
= s_('UserProfile|This user has a private profile')
.user-profile-sidebar
.profile-header.gl-pb-5.gl-pt-3.gl-overflow-y-auto.gl-sm-pr-4
.gl-vertical-align-top.gl-text-left.gl-max-w-80.gl-overflow-wrap-anywhere
.user-info
- if !@user.blocked? && @user.confirmed?
.gl-display-flex.gl-gap-4.gl-flex-direction-column
- if @user.pronouns.present? || @user.pronunciation.present? || @user.bio.present?
%div
%h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|About')
.gl-display-flex.gl-gap-2.gl-flex-direction-column
- if @user.pronouns.present? || @user.pronunciation.present?
%div
- if @user.pronunciation.present?
%div= sprintf(s_("UserProfile|Pronounced as: %{div_start}%{pronunciation}%{div_end}"), { pronunciation: @user.pronunciation, div_start: '<div class="gl-font-sm gl-text-secondary gl-display-inline-flex">', div_end: '</div>' }).html_safe
- if @user.pronouns.present?
%div= sprintf(s_("UserProfile|Pronouns: %{div_start}%{pronouns}%{div_end}"), { pronouns: @user.pronouns, div_start: '<div class="gl-font-sm gl-text-secondary gl-display-inline-flex">', div_end: '</div>' }).html_safe
- if @user.bio.present?
%p.profile-user-bio.gl-mb-0
= @user.bio
- if @user.achievements_enabled && Ability.allowed?(current_user, :read_user_profile, @user)
#js-user-achievements{ data: { root_url: root_url, user_id: @user.id } }
- user_local_time = local_time(@user.timezone)
%div{ itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' }
%h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|Info')
- if work_information(@user).present?
.gl-mb-2
= sprite_icon('work', css_class: 'fgray')
%span.gl-ml-1
= work_information(@user, with_schema_markup: true)
- if @user.location.present?
= render 'middle_dot_divider', stacking: true, itemprop: 'address', itemscope: true, itemtype: 'https://schema.org/PostalAddress' do
.gl-mb-2
= sprite_icon('location', css_class: 'fgray')
%span{ itemprop: 'addressLocality' }
%span.gl-ml-1{ itemprop: 'addressLocality' }
= @user.location
- if user_local_time.present?
= render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do
.gl-mb-2{ data: { testid: 'user-local-time' } }
= sprite_icon('clock', css_class: 'fgray')
%span
%span.gl-ml-1
= user_local_time
- if work_information(@user).present?
= render 'middle_dot_divider', stacking: true do
= sprite_icon('work', css_class: 'fgray')
%span
= work_information(@user, with_schema_markup: true)
.gl-text-gray-900
- if @user.skype.present?
= render 'middle_dot_divider' do
= link_to "skype:#{@user.skype}", class: 'gl-hover-text-decoration-none', title: "Skype" do
= sprite_icon('skype', css_class: 'skype-icon')
- if @user.linkedin.present?
= render 'middle_dot_divider' do
= link_to linkedin_url(@user), class: 'gl-hover-text-decoration-none', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow' do
= sprite_icon('linkedin', css_class: 'linkedin-icon')
- if @user.twitter.present?
= render 'middle_dot_divider', breakpoint: 'sm' do
= link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: _("X (formerly Twitter)"), target: '_blank', rel: 'noopener noreferrer nofollow' do
= sprite_icon('x', css_class: 'x-icon')
- if @user.discord.present?
= render 'middle_dot_divider', breakpoint: 'sm' do
= link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
= sprite_icon('discord', css_class: 'discord-icon')
- if @user.mastodon.present?
= render 'middle_dot_divider', breakpoint: 'sm' do
= link_to mastodon_url(@user), class: 'gl-hover-text-decoration-none', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow' do
= sprite_icon('mastodon', css_class: 'mastodon-icon')
- if @user.website_url.present?
= render 'middle_dot_divider', stacking: true do
- if Feature.enabled?(:security_auto_fix) && @user.bot?
= sprite_icon('question-o', css_class: 'gl-text-blue-500')
= link_to @user.short_website_url, @user.full_website_url, target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url'
- if display_public_email?(@user)
= render 'middle_dot_divider', stacking: true do
= link_to @user.public_email, "mailto:#{@user.public_email}", itemprop: 'email'
= sprite_icon('calendar', css_class: 'fgray')
%span.gl-ml-1= s_('Member since %{date}') % { date: l(@user.created_at.to_date, format: :long) }
-# Ensure this stays indented one level less than the social links
-# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118314
- if @user.bio.present? && @user.confirmed? && !@user.blocked?
%p.profile-user-bio.gl-mb-3
= @user.bio
- if @user.website_url.present? || display_public_email?(@user) || @user.skype.present? || @user.linkedin.present? || @user.twitter.present? || @user.mastodon.present? || @user.discord.present?
.gl-text-gray-900
%h2.gl-font-base.gl-mb-2.gl-mt-4= s_('UserProfile|Contact')
- if @user.website_url.present?
.gl-mb-2
- if Feature.enabled?(:security_auto_fix) && @user.bot?
= sprite_icon('question-o', css_class: 'gl-text-blue-500')
= sprite_icon('earth', css_class: 'fgray')
= link_to @user.short_website_url, @user.full_website_url, class: 'gl-text-gray-900 gl-ml-1', target: '_blank', rel: 'me noopener noreferrer nofollow', itemprop: 'url'
- if display_public_email?(@user)
.gl-mb-2
= sprite_icon('mail', css_class: 'fgray')
= link_to @user.public_email, "mailto:#{@user.public_email}", class: 'gl-text-gray-900 gl-ml-1', itemprop: 'email'
- if @user.skype.present?
.gl-mb-2
= sprite_icon('skype', css_class: 'fgray')
= link_to @user.skype, "skype:#{@user.skype}", class: 'gl-text-gray-900 gl-ml-1', title: "Skype"
- if @user.linkedin.present?
.gl-mb-2
= sprite_icon('linkedin', css_class: 'fgray')
= link_to @user.linkedin, linkedin_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: "LinkedIn", target: '_blank', rel: 'noopener noreferrer nofollow'
- if @user.twitter.present?
.gl-mb-2
= sprite_icon('x', css_class: 'fgray')
= link_to @user.twitter, twitter_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: _("X (formerly Twitter)"), target: '_blank', rel: 'noopener noreferrer nofollow'
- if @user.mastodon.present?
.gl-mb-2
= sprite_icon('mastodon', css_class: 'fgray')
= link_to @user.mastodon, mastodon_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: "Mastodon", target: '_blank', rel: 'noopener noreferrer nofollow'
- if @user.discord.present?
.gl-mb-2
= sprite_icon('discord', css_class: 'fgray')
= link_to @user.discord, discord_url(@user), class: 'gl-text-gray-900 gl-ml-1', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow'
-# TODO: Remove this with the removal of the old navigation.
-# See https://gitlab.com/gitlab-org/gitlab/-/issues/435899.
@ -144,7 +209,7 @@
= s_('UserProfile|Personal projects')
- if profile_tab?(:starred)
%li.js-starred-tab
= link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do
= link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json), card_mode: true } do
= s_('UserProfile|Starred projects')
- if profile_tab?(:snippets)
%li.js-snippets-tab
@ -157,67 +222,7 @@
= gl_badge_tag @user.followers.count, size: :sm
- if profile_tab?(:following)
%li.js-following-tab
= link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json) } do
= link_to user_following_path, data: { target: 'div#following', action: 'following', toggle: 'tab', endpoint: user_following_path(format: :json), testid: 'following_tab' } do
= s_('UserProfile|Following')
= gl_badge_tag @user.followees.count, size: :sm
- if !profile_tabs.empty? && Feature.enabled?(:profile_tabs_vue, current_user)
#js-profile-tabs{ data: user_profile_tabs_app_data(@user) }
%div{ class: container_class }
- unless Feature.enabled?(:profile_tabs_vue, current_user)
.tab-content
- if profile_tab?(:overview)
#js-overview.tab-pane
= render "users/overview"
- if profile_tab?(:activity)
#activity.tab-pane
.row
.col-12
.flash-container
- if can?(current_user, :read_cross_project)
%h4.prepend-top-20
= s_('UserProfile|Most Recent Activity')
.content_list.user-activity-content{ data: { href: user_activity_path } }
.loading
= gl_loading_icon(size: 'md')
- unless @user.bot?
- if profile_tab?(:groups)
#groups.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:contributed)
#contributed.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:projects)
#projects.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:starred)
#starred.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:snippets)
#snippets.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:followers)
#followers.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:following)
#following.tab-pane
-# This tab is always loaded via AJAX
.loading.hide
.gl-spinner.gl-spinner-md
- if profile_tabs.empty?
.svg-content
= image_tag 'illustrations/profile_private_mode.svg'
.text-content.text-center
%h4
- if @user.blocked?
= s_('UserProfile|This user is blocked')
- else
= s_('UserProfile|This user has a private profile')

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddNotNullConstraintToNotesNoteableType < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '16.9'
def up
add_not_null_constraint :notes, :noteable_type, validate: false
end
def down
remove_not_null_constraint :notes, :noteable_type
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddIndexToNotesWhereNoteableTypeIsNullAsync < Gitlab::Database::Migration[2.2]
milestone '16.9'
TABLE_NAME = :notes
INDEX_NAME = 'temp_index_on_notes_with_null_noteable_type'
def up
prepare_async_index TABLE_NAME, :id, where: "noteable_type IS NULL", name: INDEX_NAME
end
def down
unprepare_async_index TABLE_NAME, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
e99c70a43d8171f44f4e7b0350053179db73d8b7908173381edff65b7095c844

View File

@ -0,0 +1 @@
f5339eae21e720545dc09ede462bcdadb20a278a66134c3fe3cf4fa65d2a0a43

View File

@ -29219,6 +29219,9 @@ ALTER TABLE ONLY chat_names
ALTER TABLE ONLY chat_teams
ADD CONSTRAINT chat_teams_pkey PRIMARY KEY (id);
ALTER TABLE notes
ADD CONSTRAINT check_1244cbd7d0 CHECK ((noteable_type IS NOT NULL)) NOT VALID;
ALTER TABLE workspaces
ADD CONSTRAINT check_2a89035b04 CHECK ((personal_access_token_id IS NOT NULL)) NOT VALID;

View File

@ -1,10 +1,10 @@
---
status: ongoing
creation-date: "2023-11-23"
authors: [ "@lohrc", "@g.hickman" ]
authors: [ "@Andysoiron", "@g.hickman" ]
coach: "@fabiopitino"
approvers: [ "@g.hickman" ]
owning-stage: ~"devops::govern"
owning-stage: "~devops::govern"
participating-stages: ["~devops::verify"]
---

View File

@ -11,34 +11,42 @@ info: Any user with at least the Maintainer role can merge updates to this conte
The recommended setup for locally developing and debugging Code Suggestions is to have all 3 different components running:
- IDE Extension (e.g. VS Code Extension)
- Main application configured correctly
- Main application configured correctly (e.g. GDK)
- [AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist)
This should enable everyone to see locally any change in an IDE being sent to the main application transformed to a prompt which is then sent to the respective model.
### Setup instructions
1. Install and run locally the [VSCode Extension](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/main/CONTRIBUTING.md#configuring-development-environment)
1. Add the ```"gitlab.debug": true,``` info to the Code Suggestions development config
1. Install and run locally the [VSCode Extension](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/main/CONTRIBUTING.md#configuring-development-environment):
1. Add the ```"gitlab.debug": true,``` info to the Code Suggestions development config:
1. In VS Code navigate to the Extensions page and find "GitLab Workflow" in the list
1. Open the extension settings by clicking a small cog icon and select "Extension Settings" option
1. Check a "GitLab: Debug" checkbox.
1. If you'd like to test code suggestions are working from inside the VS Code Extension, then follow the [steps to setup a personal access token](https://gitlab.com/gitlab-org/gitlab-vscode-extension/#setup) with your GDK inside the new window of VS Code that pops up when you run the "Run and Debug" command
- Once you complete the steps below, to test you are hitting your local `/code_suggestions/completions` endpoint and not production, follow these steps:
1. Inside the new window, in the built in terminal select the "Output" tab then "GitLab Language Server" from the drop down menu on the right
1. Open a new file inside of this VS Code window and begin typing to see code suggestions in action
1. You will see completion request URLs being fetched that match the Git remote URL for your GDK
1. Main Application (GDK):
1. Install the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/index.md#one-line-installation).
1. Enable Feature Flag ```code_suggestions_tokens_api```
1. Enable Feature Flag ```code_suggestions_tokens_api```:
1. In your terminal, navigate to your `gitlab-development-kit` > `gitlab` directory.
1. Run `gdk rails console` or `bundle exec rails c` to start a Rails console.
1. [Enable the Feature Flag](../../administration/feature_flags.md#enable-or-disable-the-feature) for the code suggestions tokens API by calling
1. [Enable the Feature Flag](../../administration/feature_flags.md#enable-or-disable-the-feature) for the code suggestions tokens API by calling
`Feature.enable(:code_suggestions_tokens_api)` from the console.
1. Run the GDK with ```export AI_GATEWAY_URL=http://localhost:5052```
1. Set the AI Gateway URL environmental variable by running ```export AI_GATEWAY_URL=http://localhost:5052```
1. Run your GDK server with `gdk start` if it's not already running
1. [Setup AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist):
1. Complete the steps to [run the server locally](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist#how-to-run-the-server-locally).
- If running `asdf install` doesn't install the dependencies in ``.tool-versions``, you may need to run `asdf plugin add <name>` for each dependency first.
1. Inside ``poetry shell``, build tree sitter libraries by running ```poetry run scripts/build-tree-sitter-lib.py```
1. Add the following variables to the `.env` file for all debugging insights:
1. `AIGW_LOGGING__LEVEL=DEBUG`
1. Uncomment or add the following variables in the `.env` file for all debugging insights:
1. `AIGW_LOGGING__LEVEL=debug`
1. `AIGW_LOGGING__FORMAT_JSON=false`
1. `AIGW_LOGGING__TO_FILE=true`
1. `AIGW_LOGGING__TO_FILE=../modelgateway_debug.log`
1. Note you may need to adjust the filepath (remove `..`) for this log to show in the `ai-assist` root directory
1. If you run into an error with `tree-sitter`, you may also need to explicitly define the path to the `lib` directory by adding something like `LIB_DIR=/Users/username/src/ai-assist/scripts/lib`
1. Watch the new log file ```modelgateway_debug.log``` , e.g. ```tail -f modelgateway_debug.log | fblog -a prefix -a suffix -a current_file_name -a suggestion -a language -a input -a parameters -a score -a exception```
### Setup instructions to use staging AI Gateway
@ -46,7 +54,7 @@ This should enable everyone to see locally any change in an IDE being sent to th
When testing interactions with the AI Gateway, you might want to integrate your local GDK
with the deployed staging AI Gateway. To do this:
1. You need a [cloud staging license](../../user/project/repository/code_suggestions/self_managed_prior_versions.md#upgrade-to-gitlab-163) that has the Code Suggestions add-on, because add-ons are enabled on staging. Drop a note in the `#s_fulfillment` internal Slack channel to request an add-on to your license. See this [handbook page](https://handbook.gitlab.com/handbook/developer-onboarding/#working-on-gitlab-ee-developer-licenses) for how to request a license for local development.
1. You need a [cloud staging license](../../user/project/repository/code_suggestions/self_managed_prior_versions.md#upgrade-to-gitlab-163) that has the Code Suggestions add-on, because add-ons are enabled on staging. Drop a note in the `#s_fulfillment` or `s_fulfillment_engineering` internal Slack channel to request an add-on to your license. See this [handbook page](https://handbook.gitlab.com/handbook/developer-onboarding/#working-on-gitlab-ee-developer-licenses) for how to request a license for local development.
1. Set environment variables to point customers-dot to staging, and the AI Gateway to staging:
```shell
@ -58,3 +66,24 @@ with the deployed staging AI Gateway. To do this:
1. Restart the GDK.
1. Ensure you followed the necessary [steps to enable the Code Suggestions feature](../../user/project/repository/code_suggestions/self_managed.md).
1. Test out the Code Suggestions feature by opening the Web IDE for a project.
### Setup instructions to use GDK with the Code Suggestions Add-on
On February 15, 2023 we will require the code suggestions add-on subscription to be able to use code suggestions.
To setup your GDK for local development using the add-on, please follow these steps:
1. Drop a note in the `#s_fulfillment` or `s_fulfillment_engineering` internal Slack channel to request an activation code with the Code Suggestions add-on
1. Someone will reach out to you with a code
1. Follow the [activation instructions](https://gitlab.com/gitlab-org/customers-gitlab-com/-/blob/main/doc/license/cloud_license.md?ref_type=heads#testing-activation):
1. Set environment variables:
```shell
export GITLAB_LICENSE_MODE=test
export CUSTOMER_PORTAL_URL=https://customers.staging.gitlab.com
export GITLAB_SIMULATE_SAAS=0
```
1. Restart your GDK
1. Navigate to `/admin/subscription`
1. Remove any active license
1. Add the new activation code

View File

@ -15,7 +15,7 @@ When working with dependency scanning, you might encounter the following issues.
## Debug-level logging
Debug-level logging can help when troubleshooting. For details, see
[debug-level logging](../index.md#debug-level-logging).
[debug-level logging](../../application_security/troubleshooting_application_security.md#debug-level-logging).
### Working around missing support for certain languages or package managers
@ -79,7 +79,7 @@ scanning job might be triggered even if the scanner doesn't support your project
## Error: `dependency_scanning is used for configuration only, and its script should not be executed`
For information, see the [GitLab Secure troubleshooting section](../index.md#error-job-is-used-for-configuration-only-and-its-script-should-not-be-executed).
For information, see the [GitLab Secure troubleshooting section](../../application_security/troubleshooting_application_security.md#error-job-is-used-for-configuration-only-and-its-script-should-not-be-executed).
## Import multiple certificates for Java-based projects

View File

@ -579,221 +579,3 @@ Additional details about the differences between the two solutions are outlined
| **Ability to apply one standard to multiple projects** | The same compliance framework label can be applied to multiple projects inside a group. | The same security policy project can be used for multiple projects across GitLab with no requirement of being located in the same group. |
Feedback is welcome on our vision for [unifying the user experience for these two features](https://gitlab.com/groups/gitlab-org/-/epics/7312)
## Troubleshooting
### Logging level
The verbosity of logs output by GitLab analyzers is determined by the `SECURE_LOG_LEVEL` environment
variable. Messages of this logging level or higher are output.
From highest to lowest severity, the logging levels are:
- `fatal`
- `error`
- `warn`
- `info` (default)
- `debug`
#### Debug-level logging
WARNING:
Debug logging can be a serious security risk. The output may contain the content of
environment variables and other secrets available to the job. The output is uploaded
to the GitLab server and is visible in job logs.
To enable debug-level logging, add the following to your `.gitlab-ci.yml` file:
```yaml
variables:
SECURE_LOG_LEVEL: "debug"
```
This indicates to all GitLab analyzers that they are to output **all** messages. For more details,
see [logging level](#logging-level).
<!-- NOTE: The below subsection(`### Secure job failing with exit code 1`) documentation URL is referred in the [/gitlab-org/security-products/analyzers/command](https://gitlab.com/gitlab-org/security-products/analyzers/command/-/blob/main/command.go#L19) repository. If this section/subsection changes, ensure to update the corresponding URL in the mentioned repository.
-->
### Secure job failing with exit code 1
If a Secure job is failing and it's unclear why:
1. Enable [debug-level logging](#debug-level-logging).
1. Run the job.
1. Examine the job's output.
1. Remove the `debug` log level to return to the default `info` value.
### Outdated security reports
When a security report generated for a merge request becomes outdated, the merge request shows a
warning message in the security widget and prompts you to take an appropriate action.
This can happen in two scenarios:
- Your [source branch is behind the target branch](#source-branch-is-behind-the-target-branch).
- The [target branch security report is out of date](#target-branch-security-report-is-out-of-date).
#### Source branch is behind the target branch
A security report can be out of date when the most recent common ancestor commit between the
target branch and the source branch is not the most recent commit on the target branch.
To fix this issue, rebase or merge to incorporate the changes from the target branch.
![Incorporate target branch changes](img/outdated_report_branch_v12_9.png)
#### Target branch security report is out of date
This can happen for many reasons, including failed jobs or new advisories. When the merge request
shows that a security report is out of date, you must run a new pipeline on the target branch.
Select **new pipeline** to run a new pipeline.
![Run a new pipeline](img/outdated_report_pipeline_v12_9.png)
### Getting warning messages `… report.json: no matching files`
WARNING:
Debug logging can be a serious security risk. The output may contain the content of
environment variables and other secrets available to the job. The output is uploaded
to the GitLab server and visible in job logs.
This message is often followed by the [error `No files to upload`](../../ci/jobs/job_artifacts_troubleshooting.md#error-message-no-files-to-upload),
and preceded by other errors or warnings that indicate why the JSON report wasn't generated. Check
the entire job log for such messages. If you don't find these messages, retry the failed job after
setting `SECURE_LOG_LEVEL: "debug"` as a [custom CI/CD variable](../../ci/variables/index.md#for-a-project).
This provides extra information to investigate further.
### Getting error message `sast job: config key may not be used with 'rules': only/except`
When [including](../../ci/yaml/index.md#includetemplate) a `.gitlab-ci.yml` template
like [`SAST.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml),
the following error may occur, depending on your GitLab CI/CD configuration:
```plaintext
Unable to create pipeline
jobs:sast config key may not be used with `rules`: only/except
```
This error appears when the included job's `rules` configuration has been [overridden](sast/index.md#overriding-sast-jobs)
with [the deprecated `only` or `except` syntax.](../../ci/yaml/index.md#only--except)
To fix this issue, you must either:
- [Transition your `only/except` syntax to `rules`](#transitioning-your-onlyexcept-syntax-to-rules).
- (Temporarily) [Pin your templates to the deprecated versions](#pin-your-templates-to-the-deprecated-versions)
For more information, see [Overriding SAST jobs](sast/index.md#overriding-sast-jobs).
#### Transitioning your `only/except` syntax to `rules`
When overriding the template to control job execution, previous instances of
[`only` or `except`](../../ci/yaml/index.md#only--except) are no longer compatible
and must be transitioned to [the `rules` syntax](../../ci/yaml/index.md#rules).
If your override is aimed at limiting jobs to only run on `main`, the previous syntax
would look similar to:
```yaml
include:
- template: Security/SAST.gitlab-ci.yml
# Ensure that the scanning is only executed on main or merge requests
spotbugs-sast:
only:
refs:
- main
- merge_requests
```
To transition the above configuration to the new `rules` syntax, the override
would be written as follows:
```yaml
include:
- template: Security/SAST.gitlab-ci.yml
# Ensure that the scanning is only executed on main or merge requests
spotbugs-sast:
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_MERGE_REQUEST_ID
```
If your override is aimed at limiting jobs to only run on branches, not tags,
it would look similar to:
```yaml
include:
- template: Security/SAST.gitlab-ci.yml
# Ensure that the scanning is not executed on tags
spotbugs-sast:
except:
- tags
```
To transition to the new `rules` syntax, the override would be rewritten as:
```yaml
include:
- template: Security/SAST.gitlab-ci.yml
# Ensure that the scanning is not executed on tags
spotbugs-sast:
rules:
- if: $CI_COMMIT_TAG == null
```
For more information, see [`rules`](../../ci/yaml/index.md#rules).
#### Pin your templates to the deprecated versions
To ensure the latest support, we **strongly** recommend that you migrate to [`rules`](../../ci/yaml/index.md#rules).
If you're unable to immediately update your CI configuration, there are several workarounds that
involve pinning to the previous template versions, for example:
```yaml
include:
remote: 'https://gitlab.com/gitlab-org/gitlab/-/raw/12-10-stable-ee/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml'
```
Additionally, we provide a dedicated project containing the versioned legacy templates.
This can be used for offline setups or for anyone wishing to use [Auto DevOps](../../topics/autodevops/index.md).
Instructions are available in the [legacy template project](https://gitlab.com/gitlab-org/auto-devops-v12-10).
#### Vulnerabilities are found, but the job succeeds. How can you have a pipeline fail instead?
In these circumstances, that the job succeeds is the default behavior. The job's status indicates
success or failure of the analyzer itself. Analyzer results are displayed in the
[job logs](../../ci/jobs/index.md#expand-and-collapse-job-log-sections),
[merge request widget](#merge-request), or
[security dashboard](security_dashboard/index.md).
### Error: job `is used for configuration only, and its script should not be executed`
[Changes made in GitLab 13.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41260)
to the `Security/Dependency-Scanning.gitlab-ci.yml` and `Security/SAST.gitlab-ci.yml`
templates mean that if you enable the `sast` or `dependency_scanning` jobs by setting the `rules` attribute,
they fail with the error `(job) is used for configuration only, and its script should not be executed`.
The `sast` or `dependency_scanning` stanzas can be used to make changes to all SAST or Dependency Scanning,
such as changing `variables` or the `stage`, but they cannot be used to define shared `rules`.
There [is an issue open to improve extendability](https://gitlab.com/gitlab-org/gitlab/-/issues/218444).
You can upvote the issue to help with prioritization, and
[contributions are welcomed](https://about.gitlab.com/community/contribute/).
### Empty Vulnerability Report, Dependency List, License list pages
If the pipeline has manual steps with a job that has the `allow_failure: false` option, and this job is not finished,
GitLab can't populate listed pages with the data from security reports.
In this case, [the Vulnerability Report](vulnerability_report/index.md), [the Dependency List](dependency_list/index.md),
and [the License list](../compliance/license_list.md) pages are empty.
These security pages can be populated by running the jobs from the manual step of the pipeline.
There is [an issue open to handle this scenario](https://gitlab.com/gitlab-org/gitlab/-/issues/346843).
You can upvote the issue to help with prioritization, and
[contributions are welcomed](https://about.gitlab.com/community/contribute/).

View File

@ -13,7 +13,7 @@ DETAILS:
## Debug-level logging
Debug-level logging can help when troubleshooting. For details, see
[debug-level logging](../index.md#debug-level-logging).
[debug-level logging](../../application_security/troubleshooting_application_security.md#debug-level-logging).
## Pipeline errors related to changes in the GitLab-managed CI/CD template
@ -58,7 +58,7 @@ For information on this, see the [general Application Security troubleshooting s
## Error: `sast is used for configuration only, and its script should not be executed`
For information on this, see the [GitLab Secure troubleshooting section](../index.md#error-job-is-used-for-configuration-only-and-its-script-should-not-be-executed).
For information on this, see the [GitLab Secure troubleshooting section](../../application_security/troubleshooting_application_security.md#error-job-is-used-for-configuration-only-and-its-script-should-not-be-executed).
## SAST jobs are running unexpectedly

View File

@ -634,7 +634,7 @@ This feature is separate from Secret Detection scanning, which checks your Git r
### Debug-level logging
Debug-level logging can help when troubleshooting. For details, see
[debug-level logging](../index.md#debug-level-logging).
[debug-level logging](../../application_security/troubleshooting_application_security.md#debug-level-logging).
### Warning: `gl-secret-detection-report.json: no matching files`
@ -652,7 +652,7 @@ For example, you could have a pipeline triggered from a merge request containing
clone is not deep enough to contain all of the relevant commits. To verify the current value, see
[pipeline configuration](../../../ci/pipelines/settings.md#limit-the-number-of-changes-fetched-during-clone).
To confirm this as the cause of the error, enable [debug-level logging](../index.md#debug-level-logging),
To confirm this as the cause of the error, enable [debug-level logging](../../application_security/troubleshooting_application_security.md#debug-level-logging),
then rerun the pipeline. The logs should look similar to the following example. The text
"object not found" is a symptom of this error.

View File

@ -0,0 +1,229 @@
---
stage: Secure
group: Static Analysis
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Troubleshooting application security
DETAILS:
**Tier:** Ultimate
**Offering:** SaaS, Self-managed
When working with application security features, you might encounter the following issues.
## Logging level
The verbosity of logs output by GitLab analyzers is determined by the `SECURE_LOG_LEVEL` environment
variable. Messages of this logging level or higher are output.
From highest to lowest severity, the logging levels are:
- `fatal`
- `error`
- `warn`
- `info` (default)
- `debug`
### Debug-level logging
WARNING:
Debug logging can be a serious security risk. The output may contain the content of
environment variables and other secrets available to the job. The output is uploaded
to the GitLab server and is visible in job logs.
To enable debug-level logging, add the following to your `.gitlab-ci.yml` file:
```yaml
variables:
SECURE_LOG_LEVEL: "debug"
```
This indicates to all GitLab analyzers that they are to output **all** messages. For more details,
see [logging level](#logging-level).
<!-- NOTE: The below subsection(`### Secure job failing with exit code 1`) documentation URL is referred in the [/gitlab-org/security-products/analyzers/command](https://gitlab.com/gitlab-org/security-products/analyzers/command/-/blob/main/command.go#L19) repository. If this section/subsection changes, ensure to update the corresponding URL in the mentioned repository.
-->
## Secure job failing with exit code 1
If a Secure job is failing and it's unclear why:
1. Enable [debug-level logging](#debug-level-logging).
1. Run the job.
1. Examine the job's output.
1. Remove the `debug` log level to return to the default `info` value.
## Outdated security reports
When a security report generated for a merge request becomes outdated, the merge request shows a
warning message in the security widget and prompts you to take an appropriate action.
This can happen in two scenarios:
- Your [source branch is behind the target branch](#source-branch-is-behind-the-target-branch).
- The [target branch security report is out of date](#target-branch-security-report-is-out-of-date).
### Source branch is behind the target branch
A security report can be out of date when the most recent common ancestor commit between the
target branch and the source branch is not the most recent commit on the target branch.
To fix this issue, rebase or merge to incorporate the changes from the target branch.
![Incorporate target branch changes](img/outdated_report_branch_v12_9.png)
### Target branch security report is out of date
This can happen for many reasons, including failed jobs or new advisories. When the merge request
shows that a security report is out of date, you must run a new pipeline on the target branch.
Select **new pipeline** to run a new pipeline.
![Run a new pipeline](img/outdated_report_pipeline_v12_9.png)
## Getting warning messages `… report.json: no matching files`
WARNING:
Debug logging can be a serious security risk. The output may contain the content of
environment variables and other secrets available to the job. The output is uploaded
to the GitLab server and visible in job logs.
This message is often followed by the [error `No files to upload`](../../ci/jobs/job_artifacts_troubleshooting.md#error-message-no-files-to-upload),
and preceded by other errors or warnings that indicate why the JSON report wasn't generated. Check
the entire job log for such messages. If you don't find these messages, retry the failed job after
setting `SECURE_LOG_LEVEL: "debug"` as a [custom CI/CD variable](../../ci/variables/index.md#for-a-project).
This provides extra information to investigate further.
## Getting error message `sast job: config key may not be used with 'rules': only/except`
When [including](../../ci/yaml/index.md#includetemplate) a `.gitlab-ci.yml` template
like [`SAST.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml),
the following error may occur, depending on your GitLab CI/CD configuration:
```plaintext
Unable to create pipeline
jobs:sast config key may not be used with `rules`: only/except
```
This error appears when the included job's `rules` configuration has been [overridden](sast/index.md#overriding-sast-jobs)
with [the deprecated `only` or `except` syntax.](../../ci/yaml/index.md#only--except)
To fix this issue, you must either:
- [Transition your `only/except` syntax to `rules`](#transitioning-your-onlyexcept-syntax-to-rules).
- (Temporarily) [Pin your templates to the deprecated versions](#pin-your-templates-to-the-deprecated-versions)
For more information, see [Overriding SAST jobs](sast/index.md#overriding-sast-jobs).
### Transitioning your `only/except` syntax to `rules`
When overriding the template to control job execution, previous instances of
[`only` or `except`](../../ci/yaml/index.md#only--except) are no longer compatible
and must be transitioned to [the `rules` syntax](../../ci/yaml/index.md#rules).
If your override is aimed at limiting jobs to only run on `main`, the previous syntax
would look similar to:
```yaml
include:
- template: Security/SAST.gitlab-ci.yml
# Ensure that the scanning is only executed on main or merge requests
spotbugs-sast:
only:
refs:
- main
- merge_requests
```
To transition the above configuration to the new `rules` syntax, the override
would be written as follows:
```yaml
include:
- template: Security/SAST.gitlab-ci.yml
# Ensure that the scanning is only executed on main or merge requests
spotbugs-sast:
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_MERGE_REQUEST_ID
```
If your override is aimed at limiting jobs to only run on branches, not tags,
it would look similar to:
```yaml
include:
- template: Security/SAST.gitlab-ci.yml
# Ensure that the scanning is not executed on tags
spotbugs-sast:
except:
- tags
```
To transition to the new `rules` syntax, the override would be rewritten as:
```yaml
include:
- template: Security/SAST.gitlab-ci.yml
# Ensure that the scanning is not executed on tags
spotbugs-sast:
rules:
- if: $CI_COMMIT_TAG == null
```
For more information, see [`rules`](../../ci/yaml/index.md#rules).
### Pin your templates to the deprecated versions
To ensure the latest support, we **strongly** recommend that you migrate to [`rules`](../../ci/yaml/index.md#rules).
If you're unable to immediately update your CI configuration, there are several workarounds that
involve pinning to the previous template versions, for example:
```yaml
include:
remote: 'https://gitlab.com/gitlab-org/gitlab/-/raw/12-10-stable-ee/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml'
```
Additionally, we provide a dedicated project containing the versioned legacy templates.
This can be used for offline setups or for anyone wishing to use [Auto DevOps](../../topics/autodevops/index.md).
Instructions are available in the [legacy template project](https://gitlab.com/gitlab-org/auto-devops-v12-10).
### Vulnerabilities are found, but the job succeeds. How can you have a pipeline fail instead?
In these circumstances, that the job succeeds is the default behavior. The job's status indicates
success or failure of the analyzer itself. Analyzer results are displayed in the
[job logs](../../ci/jobs/index.md#expand-and-collapse-job-log-sections),
[merge request widget](index.md#merge-request), or
[security dashboard](security_dashboard/index.md).
## Error: job `is used for configuration only, and its script should not be executed`
[Changes made in GitLab 13.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41260)
to the `Security/Dependency-Scanning.gitlab-ci.yml` and `Security/SAST.gitlab-ci.yml`
templates mean that if you enable the `sast` or `dependency_scanning` jobs by setting the `rules` attribute,
they fail with the error `(job) is used for configuration only, and its script should not be executed`.
The `sast` or `dependency_scanning` stanzas can be used to make changes to all SAST or Dependency Scanning,
such as changing `variables` or the `stage`, but they cannot be used to define shared `rules`.
There [is an issue open to improve extendability](https://gitlab.com/gitlab-org/gitlab/-/issues/218444).
You can upvote the issue to help with prioritization, and
[contributions are welcomed](https://about.gitlab.com/community/contribute/).
## Empty Vulnerability Report, Dependency List, License list pages
If the pipeline has manual steps with a job that has the `allow_failure: false` option, and this job is not finished,
GitLab can't populate listed pages with the data from security reports.
In this case, [the Vulnerability Report](vulnerability_report/index.md), [the Dependency List](dependency_list/index.md),
and [the License list](../compliance/license_list.md) pages are empty.
These security pages can be populated by running the jobs from the manual step of the pipeline.
There is [an issue open to handle this scenario](https://gitlab.com/gitlab-org/gitlab/-/issues/346843).
You can upvote the issue to help with prioritization, and
[contributions are welcomed](https://about.gitlab.com/community/contribute/).

View File

@ -437,121 +437,3 @@ When this feature flag is enabled, the notifications and to-do item buttons are
- [Suggest code changes](reviews/suggestions.md)
- [CI/CD pipelines](../../../ci/index.md)
- [Push options](../push_options.md) for merge requests
## Troubleshooting
### Rebase a merge request from the Rails console
DETAILS:
**tier:** Free, Premium, Ultimate
**Offering:** Self-managed
In addition to the `/rebase` [quick action](../quick_actions.md#issues-merge-requests-and-epics),
users with access to the [Rails console](../../../administration/operations/rails_console.md)
can rebase a merge request from the Rails console. Replace `<username>`,
`<namespace/project>`, and `<iid>` with appropriate values:
WARNING:
Any command that changes data directly could be damaging if not run correctly,
or under the right conditions. We highly recommend running them in a test environment
with a backup of the instance ready to be restored, just in case.
```ruby
u = User.find_by_username('<username>')
p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
MergeRequests::RebaseService.new(project: m.target_project, current_user: u).execute(m)
```
### Fix incorrect merge request status
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** Self-managed
If a merge request remains **Open** after its changes are merged,
users with access to the [Rails console](../../../administration/operations/rails_console.md)
can correct the merge request's status. Replace `<username>`, `<namespace/project>`,
and `<iid>` with appropriate values:
WARNING:
Any command that changes data directly could be damaging if not run correctly,
or under the right conditions. We highly recommend running them in a test environment
with a backup of the instance ready to be restored, just in case.
```ruby
u = User.find_by_username('<username>')
p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
MergeRequests::PostMergeService.new(project: p, current_user: u).execute(m)
```
Running this command against a merge request with unmerged changes causes the
merge request to display an incorrect message: `merged into <branch-name>`.
### Close a merge request from the Rails console
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** Self-managed
If closing a merge request doesn't work through the UI or API, you might want to attempt to close it in a [Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session):
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
```ruby
u = User.find_by_username('<username>')
p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
MergeRequests::CloseService.new(project: p, current_user: u).execute(m)
```
### Delete a merge request from the Rails console
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** Self-managed
If deleting a merge request doesn't work through the UI or API, you might want to attempt to delete it in a [Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session):
WARNING:
Any command that changes data directly could be damaging if not run correctly,
or under the right conditions. We highly recommend running them in a test environment
with a backup of the instance ready to be restored, just in case.
```ruby
u = User.find_by_username('<username>')
p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
Issuable::DestroyService.new(container: m.project, current_user: u).execute(m)
```
### Merge request pre-receive hook failed
If a merge request times out, you might see messages that indicate a Puma worker
timeout problem:
- In the GitLab UI:
```plaintext
Something went wrong during merge pre-receive hook.
500 Internal Server Error. Try again.
```
- In the `gitlab-rails/api_json.log` log file:
```plaintext
Rack::Timeout::RequestTimeoutException
Request ran for longer than 60000ms
```
This error can happen if your merge request:
- Contains many diffs.
- Is many commits behind the target branch.
- References a Git LFS file that is locked.
Users in self-managed installations can request an administrator review server logs
to determine the cause of the error. GitLab SaaS users should
[contact Support](https://about.gitlab.com/support/#contact-support) for help.

View File

@ -0,0 +1,249 @@
---
stage: Create
group: Code Review
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Merge request troubleshooting
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** SaaS, self-managed
When working with merge requests, you might encounter the following issues.
## Merge request cannot retrieve the pipeline status
This can occur if Sidekiq doesn't pick up the changes fast enough.
### Sidekiq
Sidekiq didn't process the CI state change fast enough. Wait a few
seconds and the status should update automatically.
### Pipeline status cannot be retrieved
Merge request pipeline statuses can't be retrieved when the following occurs:
1. A merge request is created
1. The merge request is closed
1. Changes are made in the project
1. The merge request is reopened
To enable the pipeline status to be properly retrieved, close and reopen the
merge request again.
## Rebase a merge request from the Rails console
DETAILS:
**tier:** Free, Premium, Ultimate
**Offering:** Self-managed
In addition to the `/rebase` [quick action](../quick_actions.md#issues-merge-requests-and-epics),
users with access to the [Rails console](../../../administration/operations/rails_console.md)
can rebase a merge request from the Rails console. Replace `<username>`,
`<namespace/project>`, and `<iid>` with appropriate values:
WARNING:
Any command that changes data directly could be damaging if not run correctly,
or under the right conditions. We highly recommend running them in a test environment
with a backup of the instance ready to be restored, just in case.
```ruby
u = User.find_by_username('<username>')
p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
MergeRequests::RebaseService.new(project: m.target_project, current_user: u).execute(m)
```
## Fix incorrect merge request status
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** Self-managed
If a merge request remains **Open** after its changes are merged,
users with access to the [Rails console](../../../administration/operations/rails_console.md)
can correct the merge request's status. Replace `<username>`, `<namespace/project>`,
and `<iid>` with appropriate values:
WARNING:
Any command that changes data directly could be damaging if not run correctly,
or under the right conditions. We highly recommend running them in a test environment
with a backup of the instance ready to be restored, just in case.
```ruby
u = User.find_by_username('<username>')
p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
MergeRequests::PostMergeService.new(project: p, current_user: u).execute(m)
```
Running this command against a merge request with unmerged changes causes the
merge request to display an incorrect message: `merged into <branch-name>`.
## Close a merge request from the Rails console
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** Self-managed
If closing a merge request doesn't work through the UI or API, you might want to attempt to close it in a [Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session):
WARNING:
Commands that change data can cause damage if not run correctly or under the right conditions. Always run commands in a test environment first and have a backup instance ready to restore.
```ruby
u = User.find_by_username('<username>')
p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
MergeRequests::CloseService.new(project: p, current_user: u).execute(m)
```
## Delete a merge request from the Rails console
DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** Self-managed
If deleting a merge request doesn't work through the UI or API, you might want to attempt to delete it in a [Rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session):
WARNING:
Any command that changes data directly could be damaging if not run correctly,
or under the right conditions. We highly recommend running them in a test environment
with a backup of the instance ready to be restored, just in case.
```ruby
u = User.find_by_username('<username>')
p = Project.find_by_full_path('<namespace/project>')
m = p.merge_requests.find_by(iid: <iid>)
Issuable::DestroyService.new(container: m.project, current_user: u).execute(m)
```
## Merge request pre-receive hook failed
If a merge request times out, you might see messages that indicate a Puma worker
timeout problem:
- In the GitLab UI:
```plaintext
Something went wrong during merge pre-receive hook.
500 Internal Server Error. Try again.
```
- In the `gitlab-rails/api_json.log` log file:
```plaintext
Rack::Timeout::RequestTimeoutException
Request ran for longer than 60000ms
```
This error can happen if your merge request:
- Contains many diffs.
- Is many commits behind the target branch.
- References a Git LFS file that is locked.
Users in self-managed installations can request an administrator review server logs
to determine the cause of the error. GitLab SaaS users should
[contact Support](https://about.gitlab.com/support/#contact-support) for help.
## Cached merge request count
In a group, the sidebar displays the total count of open merge requests. This value is cached if it's greater than
than 1000. The cached value is rounded to thousands (or millions) and updated every 24 hours.
## Check out merge requests locally through the `head` ref
> - Deleting `head` refs 14 days after a merge request closes or merges [enabled on self-managed and GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130098) in GitLab 16.4.
> - Deleting `head` refs 14 days after a merge request closes or merges [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/336070) in GitLab 16.6. Feature flag `merge_request_refs_cleanup` removed.
A merge request contains all the history from a repository, plus the additional
commits added to the branch associated with the merge request. Here's a few
ways to check out a merge request locally.
You can check out a merge request locally even if the source
project is a fork (even a private fork) of the target project.
This relies on the merge request `head` ref (`refs/merge-requests/:iid/head`)
that is available for each merge request. It allows checking out a merge
request by using its ID instead of its branch.
In GitLab 16.6 and later, the merge request `head` ref is deleted 14 days after
a merge request is closed or merged. The merge request is then no longer available
for local checkout from the merge request `head` ref anymore. The merge request
can still be re-opened. If the merge request's branch
exists, you can still check out the branch, as it isn't affected.
### Check out locally by adding a Git alias
Add the following alias to your `~/.gitconfig`:
```plaintext
[alias]
mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
```
Now you can check out a particular merge request from any repository and any
remote. For example, to check out the merge request with ID 5 as shown in GitLab
from the `origin` remote, do:
```shell
git mr origin 5
```
This fetches the merge request into a local `mr-origin-5` branch and check
it out.
### Check out locally by modifying `.git/config` for a given repository
Locate the section for your GitLab remote in the `.git/config` file. It looks
like this:
```plaintext
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-foss.git
fetch = +refs/heads/*:refs/remotes/origin/*
```
You can open the file with:
```shell
git config -e
```
Now add the following line to the above section:
```plaintext
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
In the end, it should look like this:
```plaintext
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-foss.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
Now you can fetch all the merge requests:
```shell
git fetch origin
...
From https://gitlab.com/gitlab-org/gitlab-foss.git
* [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
* [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
...
```
And to check out a particular merge request:
```shell
git checkout origin/merge-requests/1
```
All the above can be done with the [`git-mr`](https://gitlab.com/glensc/git-mr) script.

View File

@ -326,145 +326,6 @@ These features are associated with merge requests:
- [Keyboard shortcuts](../../../shortcuts.md#merge-requests):
Access and modify specific parts of a merge request with keyboard commands.
## Troubleshooting
Sometimes things don't go as expected in a merge request. Here are some
troubleshooting steps.
### Merge request cannot retrieve the pipeline status
This can occur if Sidekiq doesn't pick up the changes fast enough.
#### Sidekiq
Sidekiq didn't process the CI state change fast enough. Wait a few
seconds and the status should update automatically.
#### Bug
Merge request pipeline statuses can't be retrieved when the following occurs:
1. A merge request is created
1. The merge request is closed
1. Changes are made in the project
1. The merge request is reopened
To enable the pipeline status to be properly retrieved, close and reopen the
merge request again.
## Tips
Here are some tips to help you be more efficient with merge requests in
the command line.
### Copy the branch name for local checkout
The merge request sidebar contains the branch reference for the source branch
used to contribute changes for this merge request.
To copy the branch reference into your clipboard, select the **Copy branch name** button
(**{copy-to-clipboard}**) in the right sidebar. Use it to check out the branch locally
from the command line by running `git checkout <branch-name>`.
### Checkout merge requests locally through the `head` ref
> - Deleting `head` refs 14 days after a merge request closes or merges [enabled on self-managed and GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130098) in GitLab 16.4.
> - Deleting `head` refs 14 days after a merge request closes or merges [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/336070) in GitLab 16.6. Feature flag `merge_request_refs_cleanup` removed.
A merge request contains all the history from a repository, plus the additional
commits added to the branch associated with the merge request. Here's a few
ways to check out a merge request locally.
You can check out a merge request locally even if the source
project is a fork (even a private fork) of the target project.
This relies on the merge request `head` ref (`refs/merge-requests/:iid/head`)
that is available for each merge request. It allows checking out a merge
request by using its ID instead of its branch.
In GitLab 16.6 and later, the merge request `head` ref is deleted 14 days after
a merge request is closed or merged. The merge request is then no longer available
for local checkout from the merge request `head` ref anymore. The merge request
can still be re-opened. If the merge request's branch
exists, you can still check out the branch, as it isn't affected.
#### Checkout locally by adding a Git alias
Add the following alias to your `~/.gitconfig`:
```plaintext
[alias]
mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
```
Now you can check out a particular merge request from any repository and any
remote. For example, to check out the merge request with ID 5 as shown in GitLab
from the `origin` remote, do:
```shell
git mr origin 5
```
This fetches the merge request into a local `mr-origin-5` branch and check
it out.
#### Checkout locally by modifying `.git/config` for a given repository
Locate the section for your GitLab remote in the `.git/config` file. It looks
like this:
```plaintext
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-foss.git
fetch = +refs/heads/*:refs/remotes/origin/*
```
You can open the file with:
```shell
git config -e
```
Now add the following line to the above section:
```plaintext
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
In the end, it should look like this:
```plaintext
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-foss.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
Now you can fetch all the merge requests:
```shell
git fetch origin
...
From https://gitlab.com/gitlab-org/gitlab-foss.git
* [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
* [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
...
```
And to check out a particular merge request:
```shell
git checkout origin/merge-requests/1
```
All the above can be done with the [`git-mr`](https://gitlab.com/glensc/git-mr) script.
## Cached merge request count
In a group, the sidebar displays the total count of open merge requests. This value is cached if it's greater than
than 1000. The cached value is rounded to thousands (or millions) and updated every 24 hours.
## Related topics
- [Merge methods](../methods/index.md)

View File

@ -159,7 +159,9 @@ module Gitlab
key_identifier = get_certificate_extension('authorityKeyIdentifier')
return if key_identifier.nil?
key_identifier.gsub("keyid:", "").delete!("\n")
key_identifier.gsub!("keyid:", "")
key_identifier.chomp!
key_identifier
end
def certificate_subject_key_identifier

View File

@ -43,7 +43,7 @@ module OmniAuth
when *%w[RS256 RS384 RS512]
OpenSSL::PKey::RSA.new(options.secret).public_key
when *%w[ES256 ES384 ES512]
OpenSSL::PKey::EC.new(options.secret).tap { |key| key.private_key = nil }
OpenSSL::PKey::EC.new(options.secret)
when *%w[HS256 HS384 HS512]
options.secret
else

View File

@ -2722,6 +2722,9 @@ msgstr ""
msgid "Achievements|%{namespace_link} awarded you the %{bold_start}%{achievement_name}%{bold_end} achievement!"
msgstr ""
msgid "Achievements|Achievements"
msgstr ""
msgid "Achievements|Awarded %{timeAgo} by %{namespace}"
msgstr ""
@ -18594,6 +18597,9 @@ msgstr ""
msgid "Edit environment"
msgstr ""
msgid "Edit file"
msgstr ""
msgid "Edit files in the editor and commit changes here"
msgstr ""
@ -20329,6 +20335,9 @@ msgstr ""
msgid "Expand AI-generated summary"
msgstr ""
msgid "Expand Readme"
msgstr ""
msgid "Expand all"
msgstr ""
@ -53968,6 +53977,9 @@ msgstr ""
msgid "UserProfile|%{id} · created %{created} by %{author}"
msgstr ""
msgid "UserProfile|About"
msgstr ""
msgid "UserProfile|Activity"
msgstr ""
@ -53992,6 +54004,9 @@ msgstr ""
msgid "UserProfile|Busy"
msgstr ""
msgid "UserProfile|Contact"
msgstr ""
msgid "UserProfile|Contributed projects"
msgstr ""
@ -54022,10 +54037,10 @@ msgstr ""
msgid "UserProfile|Groups are the best way to manage projects and members."
msgstr ""
msgid "UserProfile|Join or create a group to start contributing by commenting on issues or submitting merge requests!"
msgid "UserProfile|Info"
msgstr ""
msgid "UserProfile|Most Recent Activity"
msgid "UserProfile|Join or create a group to start contributing by commenting on issues or submitting merge requests!"
msgstr ""
msgid "UserProfile|No snippets found."
@ -54037,7 +54052,10 @@ msgstr ""
msgid "UserProfile|Personal projects"
msgstr ""
msgid "UserProfile|Pronounced as: %{pronunciation}"
msgid "UserProfile|Pronounced as: %{div_start}%{pronunciation}%{div_end}"
msgstr ""
msgid "UserProfile|Pronouns: %{div_start}%{pronouns}%{div_end}"
msgstr ""
msgid "UserProfile|Retry"
@ -54100,9 +54118,15 @@ msgstr ""
msgid "UserProfile|User profile navigation"
msgstr ""
msgid "UserProfile|User profile picture"
msgstr ""
msgid "UserProfile|View all"
msgstr ""
msgid "UserProfile|View large avatar"
msgstr ""
msgid "UserProfile|View user in admin area"
msgstr ""

View File

@ -224,24 +224,6 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
end
end
describe 'on smaller screens' do
shared_examples 'hidden activity calendar' do
include_context 'when user page is visited'
it 'hides the activity calender' do
expect(find('#js-overview')).not_to have_css('.js-contrib-calendar')
end
end
context 'when screen size is xs' do
before do
resize_screen_xs
end
it_behaves_like 'hidden activity calendar'
end
end
describe 'first_day_of_week setting' do
context 'when first day of the week is set to Monday' do
before do
@ -356,24 +338,6 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
end
end
describe 'on smaller screens' do
shared_examples 'hidden activity calendar' do
include_context 'when user page is visited'
it 'hides the activity calender' do
expect(page).not_to have_css('[data-testid="contrib-calendar"]')
end
end
context 'when screen size is xs' do
before do
resize_screen_xs
end
it_behaves_like 'hidden activity calendar'
end
end
describe 'first_day_of_week setting' do
context 'when first day of the week is set to Monday' do
before do

View File

@ -51,14 +51,14 @@ RSpec.describe 'Profile > Account', :js, feature_category: :user_profile do
update_username(new_username)
visit new_user_path
expect(page).to have_current_path(new_user_path, ignore_query: true)
expect(find('.user-info')).to have_content(new_username)
expect(find('.user-profile-header')).to have_content(new_username)
end
it 'the old user path redirects to the new path' do
update_username(new_username)
visit old_user_path
expect(page).to have_current_path(new_user_path, ignore_query: true)
expect(find('.user-info')).to have_content(new_username)
expect(find('.user-profile-header')).to have_content(new_username)
end
context 'with a project' do

View File

@ -48,7 +48,7 @@ RSpec.describe 'User visits their profile', feature_category: :user_profile do
it 'shows expected content', :js do
visit(user_path(user))
page.within ".cover-block" do
page.within ".user-profile-header" do
expect(page).to have_content user.name
expect(page).to have_content user.username
end

View File

@ -51,7 +51,7 @@ RSpec.describe 'User uploads avatar to profile', feature_category: :user_profile
visit user_path(user)
expect(page).to have_selector(%(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=96"]))
expect(page).to have_selector(%(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=64"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist

View File

@ -86,22 +86,6 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
describe 'projects section' do
describe 'user has no personal projects' do
include_context 'visit overview tab'
it 'shows an empty project list with an info message' do
page.within('.projects-block') do
expect(page).to have_selector('.loading', visible: false)
expect(page).to have_content('You haven\'t created any personal projects.')
expect(page).not_to have_selector('.project-row')
end
end
it 'does not show a link to the project list' do
expect(find('#js-overview .projects-block')).to have_selector('.js-view-all', visible: false)
end
end
describe 'user has a personal project' do
before do
create(:project, :private, namespace: user.namespace, creator: user) { |p| p.add_maintainer(user) }
@ -111,7 +95,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
it 'shows one entry in the list of projects' do
page.within('.projects-block') do
expect(page).to have_selector('.project-row', count: 1)
expect(page).to have_selector('.gl-card', count: 1)
end
end
@ -119,9 +103,9 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
expect(find('#js-overview .projects-block')).to have_selector('.js-view-all', visible: true)
end
it 'shows projects in "compact mode"' do
it 'shows projects in "card mode"' do
page.within('#js-overview .projects-block') do
expect(find('.js-projects-list-holder')).to have_selector('.compact')
expect(find('.js-projects-list-holder')).to have_css('.gl-card')
end
end
end
@ -135,9 +119,9 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
include_context 'visit overview tab'
it 'shows max. ten entries in the list of projects' do
it 'shows max. 3 entries in the list of projects' do
page.within('.projects-block') do
expect(page).to have_selector('.project-row', count: 10)
expect(page).to have_selector('.gl-card', count: 3)
end
end
@ -315,7 +299,7 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr
end
it 'shows projects panel' do
expect(page).to have_selector('.projects-block')
expect(page).not_to have_selector('.projects-block')
end
end
end

View File

@ -13,7 +13,7 @@ RSpec.describe 'User RSS', feature_category: :user_profile do
end
it 'shows the RSS link with overflow menu', :js do
page.within('.user-cover-block') do
page.within('.user-profile-header') do
find_by_testid('base-dropdown-toggle').click
end
@ -27,7 +27,7 @@ RSpec.describe 'User RSS', feature_category: :user_profile do
end
it 'has an RSS without a feed token', :js do
page.within('.user-cover-block') do
page.within('.user-profile-header') do
find_by_testid('base-dropdown-toggle').click
end

View File

@ -12,7 +12,7 @@ RSpec.describe 'User page', feature_category: :user_profile do
it 'shows copy user id action in the dropdown', :js do
subject
page.within('.user-cover-block') do
page.within('.cover-controls') do
find_by_testid('base-dropdown-toggle').click
end
@ -305,7 +305,7 @@ RSpec.describe 'User page', feature_category: :user_profile do
end
it 'shows user name as blocked' do
expect(page).to have_css(".cover-title", text: 'Blocked user')
expect(page).to have_css(".user-profile-header", text: 'Blocked user')
end
it 'shows no additional fields' do
@ -343,7 +343,7 @@ RSpec.describe 'User page', feature_category: :user_profile do
end
it 'shows user name as unconfirmed' do
expect(page).to have_css(".cover-title", text: 'Unconfirmed user')
expect(page).to have_css(".user-profile-header", text: 'Unconfirmed user')
end
it 'shows no tab' do
@ -393,7 +393,7 @@ RSpec.describe 'User page', feature_category: :user_profile do
subject
expect(page).to have_content("(they/them)")
expect(page).to have_content("Pronouns: they/them")
end
it 'shows the pronunctiation of the user if there was one' do
@ -435,12 +435,6 @@ RSpec.describe 'User page', feature_category: :user_profile do
stub_feature_flags(profile_tabs_vue: false)
end
it 'shows the most recent activity' do
subject
expect(page).to have_content('Most Recent Activity')
end
context 'when external authorization is enabled' do
before do
enable_external_authorization_service_check

View File

@ -1,120 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Contributors charts should render charts and a RefSelector when loading completed and there is chart data 1`] = `
<div>
<div
class="gl-bg-gray-10 gl-border-b gl-border-gray-100 gl-mb-6 gl-p-5"
>
<div
class="gl-display-flex"
>
<div
class="gl-mr-3"
>
<refselector-stub
enabledreftypes="REF_TYPE_BRANCHES,REF_TYPE_TAGS"
name=""
projectid="23"
state="true"
translations="[object Object]"
value="main"
/>
</div>
<a
class="btn btn-default btn-md gl-button"
data-testid="history-button"
href="some/path"
>
<span
class="gl-button-text"
>
History
</span>
</a>
</div>
</div>
<div
data-testid="contributors-charts"
>
<h4
class="gl-mb-2 gl-mt-5"
>
Commits to main
</h4>
<span>
Excluding merge commits. Limited to 6,000 commits.
</span>
<glareachart-stub
class="gl-mb-5"
data="[object Object]"
format-tooltip-text="function () { [native code] }"
height="264"
option="[object Object]"
responsive=""
width="auto"
>
<div
data-testid="tooltip-title"
/>
<div
class="gl-display-flex gl-gap-6 gl-justify-content-space-between"
>
<span
data-testid="tooltip-label"
>
Number of commits
</span>
<span
data-testid="tooltip-value"
>
[]
</span>
</div>
</glareachart-stub>
<div
class="row"
>
<div
class="col-12 col-lg-6 gl-my-5"
>
<h4
class="gl-mb-2 gl-mt-0"
>
John
</h4>
<p
class="gl-mb-3"
>
2 commits (jawnnypoo@gmail.com)
</p>
<glareachart-stub
data="[object Object]"
format-tooltip-text="function () { [native code] }"
height="216"
option="[object Object]"
responsive=""
width="auto"
>
<div
data-testid="tooltip-title"
/>
<div
class="gl-display-flex gl-gap-6 gl-justify-content-space-between"
>
<span
data-testid="tooltip-label"
>
Commits
</span>
<span
data-testid="tooltip-value"
>
[]
</span>
</div>
</glareachart-stub>
</div>
</div>
</div>
</div>
`;

View File

@ -1,14 +1,17 @@
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ContributorsCharts from '~/contributors/components/contributors.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Contributors from '~/contributors/components/contributors.vue';
import { createStore } from '~/contributors/stores';
import { MASTER_CHART_HEIGHT } from '~/contributors/constants';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import { SET_CHART_DATA, SET_LOADING_STATE } from '~/contributors/stores/mutation_types';
import ContributorAreaChart from '~/contributors/components/contributor_area_chart.vue';
import IndividualChart from '~/contributors/components/individual_chart.vue';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@ -28,36 +31,32 @@ const chartData = [
const projectId = '23';
const commitsPath = 'some/path';
function factory() {
const createWrapper = () => {
mock = new MockAdapter(axios);
jest.spyOn(axios, 'get');
mock.onGet().reply(HTTP_STATUS_OK, chartData);
store = createStore();
wrapper = mountExtended(ContributorsCharts, {
wrapper = shallowMountExtended(Contributors, {
propsData: {
endpoint,
branch,
projectId,
commitsPath,
},
stubs: {
GlLoadingIcon: true,
GlAreaChart: true,
RefSelector: true,
},
store,
});
}
};
const findLoadingIcon = () => wrapper.findByTestId('loading-app-icon');
const findRefSelector = () => wrapper.findComponent(RefSelector);
const findHistoryButton = () => wrapper.findByTestId('history-button');
const findContributorsCharts = () => wrapper.findByTestId('contributors-charts');
const findMasterChart = () => wrapper.findComponent(ContributorAreaChart);
const findIndividualCharts = () => wrapper.findAllComponents(IndividualChart);
describe('Contributors charts', () => {
describe('Contributors', () => {
beforeEach(() => {
factory();
createWrapper();
});
afterEach(() => {
@ -74,43 +73,95 @@ describe('Contributors charts', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('should render charts and a RefSelector when loading completed and there is chart data', async () => {
store.commit(SET_LOADING_STATE, false);
store.commit(SET_CHART_DATA, chartData);
await nextTick();
expect(findLoadingIcon().exists()).toBe(false);
expect(findRefSelector().exists()).toBe(true);
expect(findRefSelector().props()).toMatchObject({
enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
value: branch,
projectId,
translations: { dropdownHeader: 'Switch branch/tag' },
useSymbolicRefNames: false,
state: true,
name: '',
describe('loading complete', () => {
beforeEach(() => {
store.commit(SET_LOADING_STATE, false);
store.commit(SET_CHART_DATA, chartData);
return nextTick();
});
expect(findContributorsCharts().exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
it('should have a history button with a set href attribute', async () => {
store.commit(SET_LOADING_STATE, false);
store.commit(SET_CHART_DATA, chartData);
await nextTick();
it('does not display loading spinner', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
const historyButton = findHistoryButton();
expect(historyButton.exists()).toBe(true);
expect(historyButton.attributes('href')).toBe(commitsPath);
});
it('renders the RefSelector', () => {
expect(findRefSelector().props()).toMatchObject({
enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
value: branch,
projectId,
translations: { dropdownHeader: 'Switch branch/tag' },
useSymbolicRefNames: false,
state: true,
name: '',
});
});
it('visits a URL when clicking on a branch/tag', async () => {
store.commit(SET_LOADING_STATE, false);
store.commit(SET_CHART_DATA, chartData);
await nextTick();
it('should have a history button with a set href attribute', () => {
const historyButton = findHistoryButton();
expect(historyButton.exists()).toBe(true);
expect(historyButton.attributes('href')).toBe(commitsPath);
});
findRefSelector().vm.$emit('input', branch);
it('visits a URL when clicking on a branch/tag', () => {
findRefSelector().vm.$emit('input', branch);
expect(visitUrl).toHaveBeenCalledWith(`${endpoint}/${branch}`);
expect(visitUrl).toHaveBeenCalledWith(`${endpoint}/${branch}`);
});
it('renders the master chart', () => {
expect(findMasterChart().props()).toMatchObject({
data: [{ name: 'Commits', data: expect.any(Array) }],
height: MASTER_CHART_HEIGHT,
option: {
xAxis: {
data: expect.any(Array),
splitNumber: 24,
min: '2019-03-03',
max: '2019-05-05',
},
yAxis: { name: 'Number of commits' },
grid: { bottom: 64, left: 64, right: 20, top: 20 },
},
});
});
it('renders the individual charts', () => {
expect(findIndividualCharts().length).toBe(1);
expect(findIndividualCharts().at(0).props()).toMatchObject({
contributor: {
name: 'John',
email: 'jawnnypoo@gmail.com',
commits: 2,
dates: [expect.any(Object)],
},
chartOptions: {
xAxis: {
data: expect.any(Array),
splitNumber: 18,
min: '2019-03-03',
max: '2019-05-05',
},
yAxis: { name: 'Commits', max: 1 },
grid: { bottom: 27, left: 64, right: 20, top: 8 },
},
zoom: {},
});
});
describe('master chart was zoomed', () => {
const zoom = { startValue: 100, endValue: 200 };
beforeEach(() => {
findMasterChart().vm.$emit('created', {
setOption: jest.fn(),
on: jest.fn().mockImplementation((_, callback) => callback()),
getOption: jest.fn().mockImplementation(() => ({ dataZoom: [zoom] })),
});
});
it('sets the individual chart zoom', () => {
expect(findIndividualCharts().at(0).props('zoom')).toEqual(zoom);
});
});
});
});

View File

@ -0,0 +1,114 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { INDIVIDUAL_CHART_HEIGHT } from '~/contributors/constants';
import IndividualChart from '~/contributors/components/individual_chart.vue';
import ContributorAreaChart from '~/contributors/components/contributor_area_chart.vue';
describe('Individual chart', () => {
let wrapper;
let mockChart;
const commitData = [
['2010-04-15', 5],
['2010-05-15', 5],
['2010-06-15', 5],
['2010-07-15', 5],
['2010-08-15', 5],
];
const defaultContributor = {
name: 'Razputin Aquato',
email: 'razputin.aquato@psychonauts.com',
commits: 25,
dates: [{ name: 'Commits', data: commitData }],
};
const findHeader = () => wrapper.findByTestId('chart-header');
const findCommitCount = () => wrapper.findByTestId('commit-count');
const findContributorAreaChart = () => wrapper.findComponent(ContributorAreaChart);
const createWrapper = (props = {}) => {
mockChart = { setOption: jest.fn() };
wrapper = shallowMountExtended(IndividualChart, {
propsData: {
contributor: defaultContributor,
chartOptions: {},
zoom: {},
...props,
},
});
findContributorAreaChart().vm.$emit('created', mockChart);
};
describe('when not zoomed', () => {
beforeEach(() => {
createWrapper();
});
it('shows the contributor name as the chart header', () => {
expect(findHeader().text()).toBe(defaultContributor.name);
});
it('shows the total commit count', () => {
const { commits, email } = defaultContributor;
expect(findCommitCount().text()).toBe(`${commits} commits (${email})`);
});
it('renders the area chart with the given options', () => {
expect(findContributorAreaChart().props()).toMatchObject({
data: defaultContributor.dates,
option: {},
height: INDIVIDUAL_CHART_HEIGHT,
});
});
});
describe('when zoomed', () => {
const zoom = {
startValue: new Date('2010-05-01').getTime(),
endValue: new Date('2010-08-01').getTime(),
};
beforeEach(() => {
createWrapper({ zoom });
});
it('shows only the commits for the zoomed time period', () => {
const { email } = defaultContributor;
expect(findCommitCount().text()).toBe(`15 commits (${email})`);
});
it('sets the dataZoom chart option', () => {
const { startValue, endValue } = zoom;
expect(mockChart.setOption).toHaveBeenCalledWith(
{ dataZoom: { startValue, endValue, show: false } },
{ lazyUpdate: true },
);
});
describe('when zoom is changed', () => {
const newZoom = {
startValue: new Date('2010-04-01').getTime(),
endValue: new Date('2010-05-01').getTime(),
};
beforeEach(() => {
wrapper.setProps({ zoom: newZoom });
});
it('shows only the commits for the zoomed time period', () => {
const { email } = defaultContributor;
expect(findCommitCount().text()).toBe(`5 commits (${email})`);
});
it('sets the dataZoom chart option', () => {
const { startValue, endValue } = newZoom;
expect(mockChart.setOption).toHaveBeenCalledWith(
{ dataZoom: { startValue, endValue, show: false } },
{ lazyUpdate: true },
);
});
});
});
});

View File

@ -5,6 +5,7 @@ import {
nSecondsAfter,
nSecondsBefore,
isToday,
isInTimePeriod,
} from '~/lib/utils/datetime/date_calculation_utility';
import { useFakeDate } from 'helpers/fake_date';
@ -93,3 +94,21 @@ describe('getCurrentUtcDate', () => {
expect(getCurrentUtcDate()).toEqual(new Date('2022-12-05T00:00:00.000Z'));
});
});
describe('isInTimePeriod', () => {
const date = '2022-03-22T01:23:45.000Z';
it.each`
start | end | expected
${'2022-03-21'} | ${'2022-03-23'} | ${true}
${'2022-03-20'} | ${'2022-03-21'} | ${false}
${'2022-03-23'} | ${'2022-03-24'} | ${false}
${date} | ${'2022-03-24'} | ${true}
${'2022-03-21'} | ${date} | ${true}
${'2022-03-22T00:23:45.000Z'} | ${'2022-03-22T02:23:45.000Z'} | ${true}
${'2022-03-22T00:23:45.000Z'} | ${'2022-03-22T00:25:45.000Z'} | ${false}
${'2022-03-22T02:23:45.000Z'} | ${'2022-03-22T03:25:45.000Z'} | ${false}
`('returns $expected for range: $start -> $end', ({ start, end, expected }) => {
expect(isInTimePeriod(new Date(date), new Date(start), new Date(end))).toBe(expected);
});
});

View File

@ -1,5 +1,4 @@
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import * as GitLabUIUtils from '@gitlab/ui/dist/utils';
import ActivityCalendar from '~/profile/components/activity_calendar.vue';
import AjaxCache from '~/lib/utils/ajax_cache';
@ -57,23 +56,6 @@ describe('ActivityCalendar', () => {
expect(findCalendar().exists()).toBe(true);
expect(wrapper.findByText(ActivityCalendar.i18n.calendarHint).exists()).toBe(true);
});
describe('when window is resized', () => {
it('re-renders the calendar', async () => {
createComponent();
await waitForPromises();
mockSuccessfulApiRequest();
window.innerWidth = 1200;
window.dispatchEvent(new Event('resize'));
await waitForPromises();
expect(findCalendar().exists()).toBe(true);
expect(AjaxCache.retrieve).toHaveBeenCalledTimes(2);
});
});
});
describe('when API request is not successful', () => {
@ -105,16 +87,4 @@ describe('ActivityCalendar', () => {
});
});
});
describe('when screen is extra small', () => {
beforeEach(() => {
GitLabUIUtils.GlBreakpointInstance.getBreakpointSize.mockReturnValueOnce('xs');
});
it('does not render the calendar', () => {
createComponent();
expect(findCalendar().exists()).toBe(false);
});
});
});

View File

@ -85,3 +85,41 @@ describe('Read more click-to-expand functionality', () => {
});
});
});
describe('data-read-more-height defines when to show the read-more button', () => {
const findTrigger = () => document.querySelectorAll('.js-read-more-trigger');
afterEach(() => {
resetHTMLFixture();
});
it('if not set shows button all the time', () => {
setHTMLFixture(`
<div class="read-more-container">
<p class="read-more-content">Occaecat voluptate exercitation aliqua et duis eiusmod mollit esse ea laborum amet consectetur officia culpa anim. Fugiat laboris eu irure deserunt excepteur laboris irure quis. Occaecat nostrud irure do officia ea laborum velit sunt. Aliqua incididunt non deserunt proident magna aliqua sunt laborum laborum eiusmod ullamco. Et elit commodo irure. Labore eu nisi proident.</p>
<button type="button" class="js-read-more-trigger">
Button text
</button>
</div>
`);
initReadMore();
expect(findTrigger().length).toBe(1);
});
it('if set hides button as threshold is met', () => {
setHTMLFixture(`
<div class="read-more-container" data-read-more-height="120">
<p class="read-more-content">Occaecat voluptate exercitation aliqua et duis eiusmod mollit esse ea laborum amet consectetur officia culpa anim. Fugiat laboris eu irure deserunt excepteur laboris irure quis. Occaecat nostrud irure do officia ea laborum velit sunt. Aliqua incididunt non deserunt proident magna aliqua sunt laborum laborum eiusmod ullamco. Et elit commodo irure. Labore eu nisi proident.</p>
<button type="button" class="js-read-more-trigger">
Button text
</button>
</div>
`);
initReadMore();
expect(findTrigger().length).toBe(0);
});
});

View File

@ -18,7 +18,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
<a
class="file-line-num gl-shadow-none! gl-user-select-none"
data-line-number="1"
href="#LC1"
href="#L1"
id="reference-0"
>
1
@ -34,7 +34,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
<a
class="file-line-num gl-shadow-none! gl-user-select-none"
data-line-number="2"
href="#LC2"
href="#L2"
id="reference-1"
>
2
@ -50,7 +50,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
<a
class="file-line-num gl-shadow-none! gl-user-select-none"
data-line-number="3"
href="#LC3"
href="#L3"
id="reference-2"
>
3

View File

@ -9,7 +9,10 @@ RSpec.describe Gitlab::Ci::SecureFiles::P12 do
describe '#certificate_data' do
it 'assigns the error message and returns nil' do
expect(invalid_certificate.certificate_data).to be nil
expect(invalid_certificate.error).to eq('PKCS12_parse: mac verify failure')
# OpenSSL v3+ reports `PKCS12_parse: parse error` while
# OpenSSL v1.1 reports `PKCS12_parse: mac verify failure`. Unfortunately, we
# can't tell what underlying library is used, so just look for an error.
expect(invalid_certificate.error).to match(/PKCS12_parse:/)
end
end

View File

@ -229,6 +229,7 @@ RSpec.describe Gitlab::MarkdownCache::ActiveRecord::Extension do
before do
thing.note = "hello world"
thing.noteable_type = "Issue"
end
it 'calls store_mentions!' do

View File

@ -56,8 +56,7 @@ RSpec.describe OmniAuth::Strategies::Jwt do
private_key_class.generate(2048)
.to_pem
elsif private_key_class == OpenSSL::PKey::EC
private_key_class.new(ecdsa_named_curves[algorithm])
.tap { |key| key.generate_key! }
private_key_class.generate(ecdsa_named_curves[algorithm])
.to_pem
else
private_key_class.new(jwt_config.strategy.secret)

View File

@ -85,6 +85,29 @@ RSpec.describe Cli, feature_category: :service_ping do
end
end
shared_examples 'definition fixtures are valid' do |directory, schema_path|
let(:schema) { ::JSONSchemer.schema(Pathname(schema_path)) }
# The generator can return an invalid definition if the user skips the MR link
let(:expected_errors) { a_hash_including('data_pointer' => '/introduced_by_url', 'data' => 'TODO') }
it "for #{directory}", :aggregate_failures do
Dir[Rails.root.join('spec', 'fixtures', 'scripts', 'internal_events', directory, '*.yml')].each do |filepath|
attributes = YAML.safe_load(File.read(filepath))
errors = schema.validate(attributes).to_a
error_message = <<~TEXT
Unexpected validation errors in: #{filepath}
#{errors.map { |e| JSONSchemer::Errors.pretty(e) }.join("\n")}
TEXT
expect(errors).to contain_exactly(expected_errors), error_message
end
end
end
it_behaves_like 'definition fixtures are valid', 'events', 'config/events/schema.json'
it_behaves_like 'definition fixtures are valid', 'metrics', 'config/metrics/schema/base.json'
context 'when creating new events' do
YAML.safe_load(File.read('spec/fixtures/scripts/internal_events/new_events.yml')).each do |test_case|
it_behaves_like 'creates the right defintion files', test_case['description'], test_case

View File

@ -312,6 +312,10 @@ RSpec.describe AddressableUrlValidator do
let(:options) { { attributes: [:link_url] } }
let(:validator) { described_class.new(**options) }
before do
allow(ApplicationSetting).to receive(:current).and_return(ApplicationSetting.new)
end
context 'true' do
let(:options) { super().merge(deny_all_requests_except_allowed: true) }
@ -322,6 +326,20 @@ RSpec.describe AddressableUrlValidator do
expect(badge.errors).to be_present
end
context 'when allowlisted in application setting' do
before do
stub_application_setting(outbound_local_requests_whitelist: ['example.com'])
end
it 'allows the url' do
badge.link_url = url
subject
expect(badge.errors).to be_empty
end
end
end
context 'false' do
@ -338,7 +356,6 @@ RSpec.describe AddressableUrlValidator do
context 'not given' do
before do
allow(ApplicationSetting).to receive(:current).and_return(ApplicationSetting.new)
stub_application_setting(deny_all_requests_except_allowed: app_setting)
end
@ -352,6 +369,20 @@ RSpec.describe AddressableUrlValidator do
expect(badge.errors).to be_present
end
context 'when allowlisted in application setting' do
before do
stub_application_setting(outbound_local_requests_whitelist: ['example.com'])
end
it 'allows the url' do
badge.link_url = url
subject
expect(badge.errors).to be_empty
end
end
end
context 'when app setting is false' do

View File

@ -31,6 +31,20 @@ RSpec.describe 'shared/projects/_list' do
expect(rendered).not_to have_css('a.issues')
expect(rendered).not_to have_css('a.merge-requests')
end
it 'renders list in list view' do
expect(rendered).not_to have_css('.gl-new-card')
end
end
context 'with projects in card mode' do
let(:projects) { build_stubbed_list(:project, 1) }
it 'renders card mode when set to true' do
render template: 'shared/projects/_list', locals: { card_mode: true }
expect(rendered).to have_css('.gl-new-card')
end
end
context 'without projects' do

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'shared/projects/_project_card.html.haml', feature_category: :shared do
let(:project) { build(:project) }
before do
allow(view)
.to receive(:current_application_settings)
.and_return(Gitlab::CurrentSettings.current_application_settings)
allow(view).to receive(:can?).and_return(true)
end
it 'renders as a card component' do
render 'shared/projects/project_card', use_creator_avatar: true, project: project
expect(rendered).to have_selector('.gl-new-card')
end
it 'renders creator avatar if project has a creator' do
render 'shared/projects/project_card', use_creator_avatar: true, project: project
expect(rendered).to have_selector('img.gl-avatar')
end
it 'renders a generic avatar if project does not have a creator' do
project.creator = nil
render 'shared/projects/project_card', use_creator_avatar: true, project: project
expect(rendered).to have_selector('.gl-avatar-identicon')
end
end