Commit 738bf2c2 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into feature/gb/manual-actions-protected-branches-permissions

* master: (103 commits)
  Include missing project attributes to Import/Export
  Create the rest of the wiki docs
  Fill in information about creating the wiki Home page
  Move wiki doc to its own index page
  Create initial file for Wiki documentation
  Default to null user when asignee is unselected
  Re-enable ref operations with gitaly after not-found fix
  #31560 Add repo parameter to gitaly:install and workhorse:install
  Remove N+1 queries when checking nodes visible to user
  Don't validate reserved words if the format doesn't match
  Revert "Shorten and improve some job names"
  Remove unused initializer
  DRY the `<<: *except-docs` a bit in `.gitlab-ci.yml`
  Make the static-analysis job be run for docs branches too
  Add download_snippet_path helper
  Refresh the markdown cache if it was `nil`
  Add some documentation for the new migration helpers
  Update comments
  Display comments for personal snippets
  Update docs on creating a project
  ...
parents eb45582f 40cc917a
......@@ -264,11 +264,25 @@ spinach mysql 9 10: *spinach-knapsack-mysql
static-analysis:
<<: *ruby-static-analysis
<<: *dedicated-runner
<<: *except-docs
stage: test
script:
- scripts/static-analysis
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
downtime_check:
<<: *rake-exec
except:
......@@ -300,39 +314,38 @@ ee_compat_check:
.db-migrate-reset: &db-migrate-reset
stage: test
<<: *dedicated-runner
<<: *except-docs
script:
- bundle exec rake db:migrate:reset
db:migrate:reset pg:
rake pg db:migrate:reset:
<<: *db-migrate-reset
<<: *use-pg
<<: *except-docs
db:migrate:reset mysql:
rake mysql db:migrate:reset:
<<: *db-migrate-reset
<<: *use-mysql
<<: *except-docs
.db-rollback: &db-rollback
stage: test
<<: *dedicated-runner
<<: *except-docs
script:
- bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate
db:rollback pg:
rake pg db:rollback:
<<: *db-rollback
<<: *use-pg
<<: *except-docs
db:rollback mysql:
rake mysql db:rollback:
<<: *db-rollback
<<: *use-mysql
<<: *except-docs
.db-seed_fu: &db-seed_fu
stage: test
<<: *dedicated-runner
<<: *except-docs
variables:
SIZE: "1"
SETUP_DB: "false"
......@@ -347,17 +360,15 @@ db:rollback mysql:
paths:
- log/development.log
db:seed_fu pg:
rake pg db:seed_fu:
<<: *db-seed_fu
<<: *use-pg
<<: *except-docs
db:seed_fu mysql:
rake mysql db:seed_fu:
<<: *db-seed_fu
<<: *use-mysql
<<: *except-docs
gitlab:assets:compile:
rake gitlab:assets:compile:
stage: test
<<: *dedicated-runner
<<: *except-docs
......@@ -377,7 +388,7 @@ gitlab:assets:compile:
paths:
- webpack-report/
karma:
rake karma:
cache:
paths:
- vendor/ruby
......@@ -396,21 +407,6 @@ karma:
paths:
- coverage-javascript/
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
bundler:audit:
stage: test
<<: *ruby-static-analysis
......@@ -443,11 +439,11 @@ bundler:audit:
- . scripts/prepare_build.sh
- bundle exec rake db:migrate
migration path pg:
migration pg paths:
<<: *migration-paths
<<: *use-pg
migration path mysql:
migration mysql paths:
<<: *migration-paths
<<: *use-mysql
......@@ -502,30 +498,14 @@ trigger_docs:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
# Notify slack in the end
notify:slack:
stage: post-test
<<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- tags@gitlab-org/gitlab-ee
pages:
before_script: []
stage: pages
<<: *dedicated-runner
dependencies:
- coverage
- karma
- gitlab:assets:compile
- rake karma
- rake gitlab:assets:compile
- lint:javascript:report
script:
- mv public/ .public/
......
......@@ -961,7 +961,7 @@ RSpec/DescribeSymbol:
# Checks that the second argument to top level describe is the tested method
# name.
RSpec/DescribedClass:
Enabled: false
Enabled: true
# Checks for long example.
RSpec/ExampleLength:
......
app/assets/images/ci_favicons/favicon_status_canceled.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_canceled.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_created.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_created.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_failed.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_failed.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_manual.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_manual.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_not_found.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_not_found.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_pending.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_pending.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_running.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_running.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_skipped.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_skipped.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_success.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_success.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_warning.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_warning.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
  • 2-up
  • Swipe
  • Onion skin
......@@ -27,6 +27,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({
computed: {
showSidebar () {
return Object.keys(this.issue).length;
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
}
},
watch: {
......
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %d, %Y');
export const timeFormat = d3.time.format('%H:%M%p');
/* global Flash */
import d3 from 'd3';
import {
dateFormat,
timeFormat,
} from './constants';
export default class Deployments {
constructor(width, height) {
this.width = width;
this.height = height;
this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint;
this.createGradientDef();
}
init(chartData) {
this.chartData = chartData;
this.x = d3.time.scale().range([0, this.width]);
this.x.domain(d3.extent(this.chartData, d => d.time));
this.charts = d3.selectAll('.prometheus-graph');
this.getData();
}
getData() {
$.ajax({
url: this.endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error getting deployment information.'))
.done((data) => {
this.data = data.deployments.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.x(time));
time.setSeconds(this.chartData[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
});
}
return deploymentDataArray;
}, []);
this.plotData();
});
}
plotData() {
this.charts.each((d, i) => {
const svg = d3.select(this.charts[0][i]);
const chart = svg.select('.graph-container');
const key = svg.node().getAttribute('graph-type');
this.createLine(chart, key);
this.createDeployInfoBox(chart, key);
});
}
createGradientDef() {
const defs = d3.select('body')
.append('svg')
.attr({
height: 0,
width: 0,
})
.append('defs');
defs.append('linearGradient')
.attr({
id: 'shadow-gradient',
})
.append('stop')
.attr({
offset: '0%',
'stop-color': '#000',
'stop-opacity': 0.4,
})
.select(this.selectParentNode)
.append('stop')
.attr({
offset: '100%',
'stop-color': '#000',
'stop-opacity': 0,
});
}
createLine(chart, key) {
chart.append('g')
.attr({
class: 'deploy-info',
})
.selectAll('.deploy-info')
.data(this.data)
.enter()
.append('g')
.attr({
class: d => `deploy-info-${d.id}-${key}`,
transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`,
})
.append('rect')
.attr({
x: 1,
y: 0,
height: this.height + 1,
width: 3,
fill: 'url(#shadow-gradient)',
})
.select(this.selectParentNode)
.append('line')
.attr({
class: 'deployment-line',
x1: 0,
x2: 0,
y1: 0,
y2: this.height + 1,
});
}
createDeployInfoBox(chart, key) {
chart.selectAll('.deploy-info')
.selectAll('.js-deploy-info-box')
.data(this.data)
.enter()
.select(d => document.querySelector(`.deploy-info-${d.id}-${key}`))
.append('svg')
.attr({
class: 'js-deploy-info-box hidden',
x: 3,
y: 0,
width: 92,
height: 60,
})
.append('rect')
.attr({
class: 'rect-text-metric deploy-info-rect rect-metric',
x: 1,
y: 1,
rx: 2,
width: 90,
height: 58,
})
.select(this.selectParentNode)
.append('g')
.attr({
transform: 'translate(5, 2)',
})
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
})
.text(Deployments.refText)
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text',
y: 18,
})
.text(d => dateFormat(d.time))
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
y: 38,
})
.text(d => timeFormat(d.time));
}
static toggleDeployTextbox(deploy, key, showInfoBox) {
d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`)
.classed('hidden', !showInfoBox);
}
mouseOverDeployInfo(mouseXPos, key) {
if (!this.data) return false;
let dataFound = false;
this.data.forEach((d) => {
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
Deployments.toggleDeployTextbox(d, key, true);
} else {
Deployments.toggleDeployTextbox(d, key, false);
}
});
return dataFound;
}
/* `this` is bound to the D3 node */
selectParentNode() {
return this.parentNode;
}
static refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
}
}
......@@ -3,16 +3,20 @@
import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils';
import Deployments from './deployments';
import '../lib/utils/common_utils';
import { formatRelevantDigits } from '../lib/utils/number_utils';
import '../flash';
import {
dateFormat,
timeFormat,
} from './constants';
const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a');
const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100;
......@@ -36,6 +40,7 @@ class PrometheusGraph {
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
this.deployments = new Deployments(this.width, this.height);
this.configureGraph();
this.init();
} else {
......@@ -74,6 +79,12 @@ class PrometheusGraph {
$(prometheusParentGraphContainer).show();
this.transformData(metricsResponse);
this.createGraph();
const firstMetricData = this.graphSpecificProperties[
Object.keys(this.graphSpecificProperties)[0]
].data;
this.deployments.init(firstMetricData);
}
});
}
......@@ -96,6 +107,7 @@ class PrometheusGraph {
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.bottom + this.margin.top)
.append('g')
.attr('class', 'graph-container')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer)
......@@ -116,6 +128,7 @@ class PrometheusGraph {
.scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width)
.outerTickSize(0)
.orient('left');
this.createAxisLabelContainers(axisLabelContainer, key);
......@@ -248,7 +261,8 @@ class PrometheusGraph {
const d1 = currentGraphProps.data[overlayIndex];
const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
const currentData = evalTime ? d1 : d0;
const currentTimeCoordinate = currentGraphProps.xScale(currentData.time);
const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time));
const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key);
const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
......@@ -256,13 +270,12 @@ class PrometheusGraph {
// Clear up all the pieces of the flag
d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove();
const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line')
.attr('class', 'selected-metric-line')
.attr({
class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`,
x1: currentTimeCoordinate,
y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate,
......@@ -272,33 +285,45 @@ class PrometheusGraph {
currentChart.append('circle')
.attr('class', 'circle-metric')
.attr('fill', currentGraphProps.line_color)
.attr('cx', currentTimeCoordinate)
.attr('cx', currentDeployXPos || currentTimeCoordinate)
.attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric);
if (currentDeployXPos) return;
// The little box with text
const rectTextMetric = currentChart.append('g')
.attr('class', 'rect-text-metric')
.attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`);
const rectTextMetric = currentChart.append('svg')
.attr({
class: 'rect-text-metric',
x: currentTimeCoordinate,
y: 0,
});
rectTextMetric.append('rect')
.attr('class', 'rect-metric')
.attr('x', currentTimeCoordinate + 10)
.attr('y', maxMetricValue)
.attr('width', this.commonGraphProperties.rect_text_width)
.attr('height', this.commonGraphProperties.rect_text_height);
.attr({
class: 'rect-metric',
x: 4,
y: 1,
rx: 2,
width: this.commonGraphProperties.rect_text_width,
height: this.commonGraphProperties.rect_text_height,
});
rectTextMetric.append('text')
.attr('class', 'text-metric')
.attr('x', currentTimeCoordinate + 35)
.attr('y', maxMetricValue + 35)
.attr({
class: 'text-metric text-metric-bold',
x: 8,
y: 35,
})
.text(timeFormat(currentData.time));
rectTextMetric.append('text')
.attr('class', 'text-metric-date')
.attr('x', currentTimeCoordinate + 15)
.attr('y', maxMetricValue + 15)
.text(dayFormat(currentData.time));
.attr({
class: 'text-metric-date',
x: 8,
y: 15,
})
.text(dateFormat(currentData.time));
let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') {
......
......@@ -30,7 +30,7 @@
$els.each((function(_this) {
return function(i, dropdown) {
var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id');
options.groupId = $dropdown.data('group-id');
......@@ -38,11 +38,11 @@
options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter');
showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id');
selectedId = $dropdown.data('selected');
defaultLabel = $dropdown.data('default-label');
issueURL = $dropdown.data('issueUpdate');
$selectbox = $dropdown.closest('.selectbox');
......@@ -51,6 +51,8 @@
$value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut();
selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null;
selectedId = $dropdown.data('selected') || selectedIdDefault;
var updateIssueBoardsIssue = function () {
$loading.removeClass('hidden').fadeIn();
......@@ -186,12 +188,14 @@
fieldName: $dropdown.data('field-name'),
toggleLabel: function(selected, el) {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
$dropdown.find('.dropdown-toggle-text').removeClass('is-default');
if (selected.text) {
return selected.text;
} else {
return selected.name;
}
} else {
$dropdown.find('.dropdown-toggle-text').addClass('is-default');
return defaultLabel;
}
},
......@@ -204,13 +208,14 @@
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected;
var isIssueIndex, isMRIndex, page, selected, isSelecting;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (user.id !== selectedId);
selectedId = isSelecting ? user.id : selectedIdDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
selectedId = user.id;
if (selectedId === gon.current_user_id) {
$('.assign-to-me-link').hide();
} else {
......@@ -221,12 +226,11 @@
if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedId = user.id;
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (user.id) {
if (user.id && isSelecting) {
gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: user.id,
username: user.username,
......@@ -248,6 +252,9 @@
},
opened: function(e) {
const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault;
}
$el.find('.is-active').removeClass('is-active');
$el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active');
},
......
......@@ -390,7 +390,8 @@
&::before {
position: absolute;
left: 6px;
top: 6px;
top: 50%;
transform: translateY(-50%);
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
......
......@@ -157,7 +157,8 @@
.prometheus-graph {
text {
fill: $stat-graph-axis-fill;
fill: $gl-text-color;
stroke-width: 0;
}
.label-axis-text,
......@@ -210,27 +211,33 @@
.rect-text-metric {
fill: $white-light;
stroke-width: 1;
stroke: $black;
stroke: $gray-darkest;
}
.rect-axis-text {
fill: $white-light;
}
.text-metric,
.text-median-metric,
.text-metric-usage,
.text-metric-date {
fill: $black;
.text-metric {
font-weight: 600;
}
.text-metric-date {
font-weight: 200;
.selected-metric-line {
stroke: $gl-gray-dark;
stroke-width: 1;
}
.selected-metric-line {
.deployment-line {
stroke: $black;
stroke-width: 1;
stroke-width: 2;
}
.deploy-info-text {
dominant-baseline: text-before-edge;
}
.text-metric-bold {
font-weight: 600;
}
.prometheus-state {
......
class Admin::HooksController < Admin::ApplicationController
before_action :hook, only: :edit
def index
@hooks = SystemHook.all
@hook = SystemHook.new
......@@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController
end
end
def edit
end
def update
if hook.update_attributes(hook_params)
flash[:notice] = 'System hook was successfully updated.'
redirect_to admin_hooks_path
else
render 'edit'
end
end
def destroy
@hook = SystemHook.find(params[:id])
@hook.destroy
hook.destroy
redirect_to admin_hooks_path
end
def test
@hook = SystemHook.find(params[:hook_id])
data = {
event_name: "project_create",
name: "Ruby",
......@@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
@hook.execute(data, 'system_hooks')
hook.execute(data, 'system_hooks')
redirect_back_or_default
end
private
def hook
@hook ||= SystemHook.find(params[:id])
end
def hook_params
params.require(:hook).permit(
:enable_ssl_verification,
......
module NotesActions
include RendersNotes
extend ActiveSupport::Concern
included do
before_action :authorize_admin_note!, only: [:update, :destroy]
end
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_relations_for_view
@notes = prepare_notes_for_rendering(@notes)
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
end
private
def note_json(note)
attrs = {
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end
end
......@@ -10,6 +10,8 @@ module RendersNotes
private
def preload_max_access_for_authors(notes, project)
return nil unless project
user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids)
end
......
......@@ -5,10 +5,12 @@ module SnippetsActions
end
def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
disposition: 'inline',
disposition: disposition,
filename: @snippet.sanitized_file_name
)
end
......
......@@ -22,7 +22,8 @@ module ToggleAwardEmoji
def to_todoable(awardable)
case awardable
when Note
awardable.noteable
# we don't create todos for personal snippet comments for now
awardable.for_personal_snippet? ? nil : awardable.noteable
when MergeRequest, Issue
awardable
when Snippet
......
class Projects::DeploymentsController < Projects::ApplicationController
before_action :authorize_read_environment!
before_action :authorize_read_deployment!
def index
deployments = environment.deployments.reorder(created_at: :desc)
deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time
render json: { deployments: DeploymentSerializer.new(user: @current_user, project: project)
.represent_concise(deployments) }
end
private
def environment
@environment ||= project.environments.find(params[:environment_id])
end
end
class Projects::HooksController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :hook, only: :edit
respond_to :html
......@@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
def edit
end
def update
if hook.update_attributes(hook_params)
flash[:notice] = 'Hook was successfully updated.'
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
else
render 'edit'
end
end
def test
if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user)
......
class Projects::NotesController < Projects::ApplicationController
include RendersNotes
include NotesActions
include ToggleAwardEmoji
# Authorize
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_relations_for_view
@notes = prepare_notes_for_rendering(@notes)
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
#
# This is a fix to make spinach feature tests passing:
# Controller actions are returned from AbstractController::Base and methods of parent classes are
# excluded in order to return only specific controller related methods.
# That is ok for the app (no :create method in ancestors)
# but fails for tests because there is a :create method on FactoryGirl (one of the ancestors)
#
# see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
#
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
super
end
def delete_attachment
......@@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController
def note_html(note)
render_to_string(
"projects/notes/_note",
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
......@@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController
)
end
def note_json(note)
attrs = {
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
def finder_params
params.merge(last_fetched_at: last_fetched_at)
end
def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note)
end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
end
class Snippets::NotesController < ApplicationController
include NotesActions
include ToggleAwardEmoji
skip_before_action :authenticate_user!, only: [:index]
before_action :snippet
before_action :authorize_read_snippet!, only: [:show, :index, :create]
private
def note
@note ||= snippet.notes.find(params[:id])
end
alias_method :awardable, :note
def note_html(note)
render_to_string(
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
)
end
def project
nil
end
def snippet
PersonalSnippet.find_by(id: params[:snippet_id])
end
def note_params
super.merge(noteable_id: params[:snippet_id])
end
def finder_params
params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet')
end
def authorize_read_snippet!
return render_404 unless can?(current_user, :read_personal_snippet, snippet)
end
end
class SnippetsController < ApplicationController
include RendersNotes
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
include MarkdownPreview
include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
before_action :authorize_read_snippet!, only: [:show, :raw, :download]
before_action :authorize_read_snippet!, only: [:show, :raw]
# Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
......@@ -16,7 +17,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download]
skip_before_action :authenticate_user!, only: [:index, :show, :raw]
layout 'snippets'
respond_to :html
......@@ -64,6 +65,11 @@ class SnippetsController < ApplicationController
blob = @snippet.blob
override_max_blob_size(blob)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
respond_to do |format|
format.html do
render 'show'
......@@ -83,14 +89,6 @@ class SnippetsController < ApplicationController
redirect_to snippets_path
end
def download
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
end
def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true)
end
......
......@@ -68,6 +68,8 @@ class NotesFinder
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet"
SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
when "personal_snippet"
PersonalSnippet.all
else
raise 'invalid target_type'
end
......
module AwardEmojiHelper
def toggle_award_url(awardable)
return url_for([:toggle_award_emoji, awardable]) unless @project
return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note)
if awardable.is_a?(Note)
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id)
if awardable.for_personal_snippet?
toggle_award_emoji_snippet_note_path(awardable.noteable, awardable)
else
toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id)
end
else
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end
......
module MergeRequestsHelper
def new_mr_path_from_push_event(event)
target_project = event.project.forked_from_project || event.project
target_project = event.project.default_merge_request_target
new_namespace_project_merge_request_path(
event.project.namespace,
event.project,
......@@ -127,6 +127,10 @@ module MergeRequestsHelper
end
end
def target_projects(project)
[project, project.default_merge_request_target].uniq
end
def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
......
......@@ -8,6 +8,14 @@ module SnippetsHelper
end
end
def download_snippet_path(snippet)
if snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false)
else
raw_snippet_path(snippet, inline: false)
end
end
# Return the path of a snippets index for a user or for a project
#
# @returns String, path to snippet index
......
......@@ -78,6 +78,9 @@ module CacheMarkdownField
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
cached = !cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil?
return false unless cached
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
......
......@@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base
validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
validate :validate_target_project, on: :create
scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
......@@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base
end
end
def validate_target_project
return true if target_project.merge_requests_enabled?
errors.add :base, 'Target project has disabled merge requests'
end
def validate_fork
return true unless target_project && source_project
return true if target_project == source_project
......
......@@ -33,7 +33,7 @@ class Namespace < ActiveRecord::Base
validates :path,
presence: true,
length: { maximum: 255 },
namespace: true
dynamic_path: true
validate :nesting_level_allowed
......@@ -220,6 +220,10 @@ class Namespace < ActiveRecord::Base
Project.inside_path(full_path)
end
def has_parent?
parent.present?
end
private
def repository_storage_paths
......
......@@ -196,13 +196,14 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
project_path: true,
dynamic_path: true,
length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
message: Gitlab::Regex.project_path_regex_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import?
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
......@@ -1270,6 +1271,9 @@ class Project < ActiveRecord::Base
else
update_attribute(name, value)
end
rescue ActiveRecord::RecordNotSaved => e
handle_update_attribute_error(e, value)
end
def pushes_since_gc
......@@ -1314,6 +1318,14 @@ class Project < ActiveRecord::Base
namespace_id_changed?
end
def default_merge_request_target
if forked_from_project&.merge_requests_enabled?
forked_from_project
else
self
end
end
alias_method :name_with_namespace, :full_name
alias_method :human_name, :full_name
alias_method :path_with_namespace, :full_path
......@@ -1383,4 +1395,16 @@ class Project < ActiveRecord::Base
ContainerRepository.build_root_repository(self).has_tags?
end
def handle_update_attribute_error(ex, value)
if ex.message.start_with?('Failed to replace')
if value.respond_to?(:each)
invalid = value.detect(&:invalid?)
raise ex, ([ex.message] + invalid.errors.full_messages).join(' ') if invalid
end
end
raise ex
end
end
......@@ -505,14 +505,8 @@ class Repository
delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: []
def branch_count
branches.size
end
delegate :branch_count, :tag_count, to: :raw_repository
cache_method :branch_count, fallback: 0
def tag_count
raw_repository.rugged.tags.count
end
cache_method :tag_count, fallback: 0
def avatar
......
......@@ -118,7 +118,7 @@ class User < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
namespace: true,
dynamic_path: true,
presence: true,
uniqueness: { case_sensitive: false }
......
......@@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity
end
end
expose :created_at
expose :tag
expose :last?
expose :user, using: UserEntity
expose :commit, using: CommitEntity
expose :deployable, using: BuildEntity
......
class DeploymentSerializer < BaseSerializer
entity DeploymentEntity
def represent_concise(resource, opts = {})
opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]]
represent(resource, opts)
end
end
......@@ -7,6 +7,9 @@ class StatusEntity < Grape::Entity
expose :details_path
expose :favicon do |status|
ActionController::Base.helpers.image_path(File.join('ci_favicons', "#{status.favicon}.ico"))
dir = 'ci_favicons'
dir = File.join(dir, 'dev') if Rails.env.development?
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end
end
......@@ -28,7 +28,7 @@ module MergeRequests
def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project)
project.forked_from_project || project
project.default_merge_request_target
end
def find_target_branch
......
......@@ -281,7 +281,7 @@ class TodoService
def attributes_for_target(target)
attributes = {
project_id: target.project.id,
project_id: target&.project&.id,
target_id: target.id,
target_type: target.class.name,
commit_id: nil
......
# DynamicPathValidator
#
# Custom validator for GitLab path values.
# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
# All routes that appear on the top level must be listed here.
# This will make sure that groups cannot be created with these names
# as these routes would be masked by the paths already in place.
#
# Example:
# /api/api-project
#
# the path `api` shouldn't be allowed because it would be masked by `api/*`
#
TOP_LEVEL_ROUTES = %w[
-
.well-known
abuse_reports
admin
all
api
assets
autocomplete
ci
dashboard
explore
files
groups
health_check
help
hooks
import
invites
issues
jwt
koding
member
merge_requests
new
notes
notification_settings
oauth
profile
projects
public
repository
robots.txt
s
search
sent_notifications
services
snippets
teams
u
unicorn_test
unsubscribes
uploads
users
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
# routes that contain a second wildcard.
#
# Example:
# /*namespace_id/:project_id/badges/*ref/build
#
# If `badges` was allowed as a project/group name, we would not be able to access the
# `badges` route for those projects:
#
# Consider a namespace with path `foo/bar` and a project called `badges`.
# The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg`
#
# When accessing this path the route would be matched to the `badges` path
# with the following params:
# - namespace_id: `foo`
# - project_id: `bar`
# - ref: `badges/master`
#
# Failing to find the project, this would result in a 404.
#
# By rejecting `badges` the router can _count_ on the fact that `badges` will
# be preceded by the `namespace/project`.
WILDCARD_ROUTES = %w[
badges
blame
blob
builds
commits
create
create_dir
edit
environments/folders
files
find_file
gitlab-lfs/objects
info/lfs/objects
new
preview
raw
refs
tree
update
wikis
].freeze
# These are all the paths that follow `/groups/*id/ or `/groups/*group_id`
# We need to reject these because we have a `/groups/*id` page that is the same
# as the `/*id`.
#
# If we would allow a subgroup to be created with the name `activity` then
# this group would not be accessible through `/groups/parent/activity` since
# this would map to the activity-page of it's parent.
GROUP_ROUTES = %w[
activity
avatar
edit
group_members
issues
labels
merge_requests
milestones
projects
subgroups
].freeze
CHILD_ROUTES = (WILDCARD_ROUTES | GROUP_ROUTES).freeze
def self.without_reserved_wildcard_paths_regex
@without_reserved_wildcard_paths_regex ||= regex_excluding_child_paths(WILDCARD_ROUTES)
end
def self.without_reserved_child_paths_regex
@without_reserved_child_paths_regex ||= regex_excluding_child_paths(CHILD_ROUTES)
end
# This is used to validate a full path.
# It doesn't match paths
# - Starting with one of the top level words
# - Containing one of the child level words in the middle of a path
def self.regex_excluding_child_paths(child_routes)
reserved_top_level_words = Regexp.union(TOP_LEVEL_ROUTES)
not_starting_in_reserved_word = %r{\A/?(?!(#{reserved_top_level_words})(/|\z))}
reserved_child_level_words = Regexp.union(child_routes)
not_containing_reserved_child = %r{(?!\S+/(#{reserved_child_level_words})(/|\z))}
%r{#{not_starting_in_reserved_word}
#{not_containing_reserved_child}
#{Gitlab::Regex.full_namespace_regex}}x
end
def self.valid?(path)
path =~ Gitlab::Regex.full_namespace_regex && !full_path_reserved?(path)
end
def self.full_path_reserved?(path)
path = path.to_s.downcase
_project_part, namespace_parts = path.reverse.split('/', 2).map(&:reverse)
wildcard_reserved?(path) || child_reserved?(namespace_parts)
end
def self.child_reserved?(path)
return false unless path
path !~ without_reserved_child_paths_regex
end
def self.wildcard_reserved?(path)
return false unless path
path !~ without_reserved_wildcard_paths_regex
end
delegate :full_path_reserved?,
:child_reserved?,
to: :class
def path_reserved_for_record?(record, value)
full_path = record.respond_to?(:full_path) ? record.full_path : value
# For group paths the entire path cannot contain a reserved child word
# The path doesn't contain the last `_project_part` so we need to validate
# if the entire path.
# Example:
# A *group* with full path `parent/activity` is reserved.
# A *project* with full path `parent/activity` is allowed.
if record.is_a? Group
child_reserved?(full_path)
else
full_path_reserved?(full_path)
end
end
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_regex
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
return
end
if path_reserved_for_record?(record, value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
# NamespaceValidator
#
# Custom validator for GitLab namespace values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class NamespaceValidator < ActiveModel::EachValidator
RESERVED = %w[
.well-known
admin
all
assets
ci
dashboard
files
groups
help
hooks
issues
merge_requests
new
notes
profile
projects
public
repository
robots.txt
s
search
services
snippets
teams
u
unsubscribes
users
].freeze
WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file
artifacts graphs refs badges].freeze
STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
def self.valid?(value)
!reserved?(value) && follow_format?(value)
end
def self.reserved?(value, strict: false)
if strict
STRICT_RESERVED.include?(value)
else
RESERVED.include?(value)
end
end
def self.follow_format?(value)
value =~ Gitlab::Regex.namespace_regex
end
delegate :reserved?, :follow_format?, to: :class
def validate_each(record, attribute, value)
unless follow_format?(value)
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
strict = record.is_a?(Group) && record.parent_id
if reserved?(value, strict: strict)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
# ProjectPathValidator
#
# Custom validator for GitLab project path values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class ProjectPathValidator < ActiveModel::EachValidator
# All project routes with wildcard argument must be listed here.
# Otherwise it can lead to routing issues when route considered as project name.
#
# Example:
# /group/project/tree/deploy_keys
#
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
RESERVED = (NamespaceValidator::STRICT_RESERVED -
%w[dashboard help ci admin search notes services assets profile public]).freeze
def self.valid?(value)
!reserved?(value)
end
def self.reserved?(value)
RESERVED.include?(value)
end
delegate :reserved?, to: :class
def validate_each(record, attribute, value)
if reserved?(value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
......@@ -73,6 +73,12 @@
= container_reg
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
- gitlab_pages = 'GitLab Pages'
- gitlab_pages_enabled = Gitlab.config.pages.enabled
%p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") }
= gitlab_pages
%span.light.pull-right
= boolean_to_icon gitlab_pages_enabled
.col-md-4
%h4
......
= form_errors(hook)
.form-group
= form.label :url, 'URL', class: 'control-label'
.col-sm-10
= form.text_field :url, class: 'form-control'
.form-group
= form.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= form.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group
= form.label :url, 'Trigger', class: 'control-label'
.col-sm-10.prepend-top-10
%div
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
= form.check_box :push_events, class: 'pull-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%div
= form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox'
.col-sm-10
.checkbox
= form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification
%strong Enable SSL verification
- page_title 'Edit System Hook'
%h3.page-title
Edit System Hook
%p.light
#{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project.
%hr
= form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Save changes', class: 'btn btn-create'
- page_title "System Hooks"
- page_title 'System Hooks'
%h3.page-title
System hooks
%p.light
#{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be
#{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project.
%hr
= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
= form_errors(@hook)
.form-group
= f.label :url, 'URL', class: 'control-label'
.col-sm-10
= f.text_field :url, class: 'form-control'
.form-group
= f.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= f.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group
= f.label :url, "Trigger", class: 'control-label'
.col-sm-10.prepend-top-10
%div
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%div
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
.col-sm-10
.checkbox
= f.label :enable_ssl_verification do
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit "Add system hook", class: "btn btn-create"
= f.submit 'Add system hook', class: 'btn btn-create'
%hr
- if @hooks.any?
......@@ -62,11 +22,12 @@
- @hooks.each do |hook|
%li
.controls
= link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm"
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
= link_to 'Test hook', test_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url
%div
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
.discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "projects/notes/note", collection: discussion.notes, as: :note
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
- if current_user
.discussion-reply-holder
......
......@@ -28,8 +28,9 @@
":value" => "issue.assignee.id",
"v-if" => "issue.assignee" }
.dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" },
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", null_user_default: "true" },
":data-issuable-id" => "issue.id",
":data-selected" => "assigneeId",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Select assignee
= icon("chevron-down")
......
......@@ -60,7 +60,7 @@
git init
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add .
git commit
git commit -m "Initial commit"
git push -u origin master
%fieldset
......
......@@ -5,7 +5,7 @@
= page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head"
.prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" }
#js-metrics.prometheus-container{ class: container_class, data: { has_metrics: "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } }
.top-area
.row
.col-sm-6
......
= render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project]
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
= page_title
%p
#{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
= f.submit 'Add webhook', class: 'btn btn-create'
%hr
%h5.prepend-top-default
Webhooks (#{@hooks.count})
- if @hooks.any?
%ul.well-list
- @hooks.each do |hook|
= render 'project_hook', hook: hook
- else
%p.settings-message.text-center.append-bottom-0
No webhooks found, add one in the form above.
= render 'projects/settings/head'
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
= page_title
%p
#{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hook_path do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
= f.submit 'Save changes', class: 'btn btn-create'
......@@ -38,7 +38,7 @@
.panel-heading
Target branch
.panel-body.clearfix
- projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project]
- projects = target_projects(@project)
.merge-request-select.dropdown
= f.hidden_field :target_project_id
= dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
......
- access = note_max_access_for_user(note)
- if access
%span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => project_path(note.project),
"discussion-id" => note.discussion_id(@noteable),
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
":author-name" => "'#{j(note.author.name)}'",
"author-avatar" => note.author.avatar_url,
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
%button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }",
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
":ref" => "'button'" }
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
= render "shared/notes/notes"
= render 'projects/notes/edit_form'
......
......@@ -14,7 +14,7 @@
%span
Members
- if can_edit
= nav_link(controller: [:integrations, :services]) do
= nav_link(controller: [:integrations, :services, :hooks]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
Integrations
......
......@@ -9,6 +9,7 @@
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
%span.append-right-10.inline
SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
= link_to "Edit", edit_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm"
= link_to "Test", test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm"
= link_to namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent" do
%span.sr-only Remove
......
......@@ -117,7 +117,7 @@
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
......@@ -125,13 +125,13 @@
%a{ href: "#", data: { id: "close" } } Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
......
......@@ -48,7 +48,7 @@
.selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } })
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, null_user_default: true, selected: issuable.assignee_id } })
.block.milestone
.sidebar-collapsed-icon
......
......@@ -29,58 +29,19 @@
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
.note-actions
- access = note_max_access_for_user(note)
- if access
%span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => project_path(note.project),
"discussion-id" => note.discussion_id(@noteable),
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
":author-name" => "'#{j(note.author.name)}'",
"author-avatar" => note.author.avatar_url,
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
%button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }",
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
":ref" => "'button'" }
= icon("spin spinner", "v-show" => "loading", class: 'loading')
%div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg"
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
- if note.for_personal_snippet?
= render 'snippets/notes/actions', note: note, note_editable: note_editable
- else
= render 'projects/notes/actions', note: note, note_editable: note_editable
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
= note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note
- if note.for_personal_snippet?
= render 'snippets/notes/edit', note: note
- else
= render 'projects/notes/edit', note: note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
......
- if defined?(@discussions)
- @discussions.each do |discussion|
- if discussion.individual_note?
= render partial: "projects/notes/note", collection: discussion.notes, as: :note
= render partial: "shared/notes/note", collection: discussion.notes, as: :note
- else
= render 'discussions/discussion', discussion: discussion
- else
= render partial: "projects/notes/note", collection: @notes, as: :note
= render partial: "shared/notes/note", collection: @notes, as: :note
......@@ -18,7 +18,6 @@
= copy_blob_source_button(blob)
= open_raw_blob_button(blob)
- if defined?(download_path) && download_path
= link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
= link_to icon('download'), download_snippet_path(@snippet), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
= render 'projects/blob/content', blob: blob
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
= page_title
%p
#{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
= form_errors(hook)
= form_errors(hook)
.form-group
= f.label :url, "URL", class: 'label-light'
= f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
.form-group
= f.label :token, "Secret Token", class: 'label-light'
= f.text_field :token, class: "form-control", placeholder: ''
%p.help-block
Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
.form-group
= f.label :url, "Trigger", class: 'label-light'
%ul.list-unstyled
%li
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This URL will be triggered by a push to the repository
%li
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This URL will be triggered when a new tag is pushed to the repository
%li
= f.check_box :note_events, class: 'pull-left'
.prepend-left-20
= f.label :note_events, class: 'list-label' do
%strong Comments
%p.light
This URL will be triggered when someone adds a comment
%li
= f.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= f.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
This URL will be triggered when an issue is created/updated/merged
%li
= f.check_box :confidential_issues_events, class: 'pull-left'
.prepend-left-20
= f.label :confidential_issues_events, class: 'list-label' do
%strong Confidential Issues events
%p.light
This URL will be triggered when a confidential issue is created/updated/merged
%li
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
This URL will be triggered when a merge request is created/updated/merged
%li
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
= f.label :build_events, class: 'list-label' do
%strong Jobs events
%p.light
This URL will be triggered when the job status changes
%li
= f.check_box :pipeline_events, class: 'pull-left'
.prepend-left-20
= f.label :pipeline_events, class: 'list-label' do
%strong Pipeline events
%p.light
This URL will be triggered when the pipeline status changes
%li
= f.check_box :wiki_page_events, class: 'pull-left'
.prepend-left-20
= f.label :wiki_page_events, class: 'list-label' do
%strong Wiki Page events
%p.light
This URL will be triggered when a wiki page is created/updated
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
.checkbox
= f.label :enable_ssl_verification do
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
= f.submit "Add webhook", class: "btn btn-create"
%hr
%h5.prepend-top-default
Webhooks (#{hooks.count})
- if hooks.any?
%ul.well-list
- hooks.each do |hook|
= render "project_hook", hook: hook
- else
%p.settings-message.text-center.append-bottom-0
No webhooks found, add one in the form above.
.form-group
= form.label :url, 'URL', class: 'label-light'
= form.text_field :url, class: 'form-control', placeholder: 'http://example.com/trigger-ci.json'
.form-group
= form.label :token, 'Secret Token', class: 'label-light'
= form.text_field :token, class: 'form-control', placeholder: ''
%p.help-block
Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
.form-group
= form.label :url, 'Trigger', class: 'label-light'
%ul.list-unstyled
%li
= form.check_box :push_events, class: 'pull-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This URL will be triggered by a push to the repository
%li
= form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This URL will be triggered when a new tag is pushed to the repository
%li
= form.check_box :note_events, class: 'pull-left'
.prepend-left-20
= form.label :note_events, class: 'list-label' do
%strong Comments
%p.light
This URL will be triggered when someone adds a comment
%li
= form.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= form.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
This URL will be triggered when an issue is created/updated/merged
%li
= form.check_box :confidential_issues_events, class: 'pull-left'
.prepend-left-20
= form.label :confidential_issues_events, class: 'list-label' do
%strong Confidential Issues events
%p.light
This URL will be triggered when a confidential issue is created/updated/merged
%li
= form.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= form.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
This URL will be triggered when a merge request is created/updated/merged
%li
= form.check_box :build_events, class: 'pull-left'
.prepend-left-20
= form.label :build_events, class: 'list-label' do
%strong Jobs events
%p.light
This URL will be triggered when the job status changes
%li
= form.check_box :pipeline_events, class: 'pull-left'
.prepend-left-20
= form.label :pipeline_events, class: 'list-label' do
%strong Pipeline events
%p.light
This URL will be triggered when the pipeline status changes
%li
= form.check_box :wiki_page_events, class: 'pull-left'
.prepend-left-20
= form.label :wiki_page_events, class: 'list-label' do
%strong Wiki Page events
%p.light
This URL will be triggered when a wiki page is created/updated
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
.checkbox
= form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification
%strong Enable SSL verification
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to snippet_note_path(note.noteable, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
......@@ -3,7 +3,10 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
= render 'shared/snippets/blob', download_path: download_snippet_path(@snippet)
= render 'shared/snippets/blob'
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
%ul#notes-list.notes.main-notes-list.timeline
#notes= render 'shared/notes/notes'
---
title: Display comments for personal snippets
merge_request:
author:
---
title: Implement ability to edit hooks
merge_request: 10816
author: Alexander Randa
---
title: Disallow merge requests from fork when source project have disabled merge requests
merge_request:
author: mhasbini
---
title: Improve validation of namespace & project paths
merge_request: 10413
author:
---
title: Display GitLab Pages status in Admin Dashboard
merge_request:
author:
---
title: Show checkmark on current assignee in assignee dropdown
merge_request: 10767
author:
---
title: Change Git commit command in Existing folder to git commit -m
merge_request: 10900
author: TM Lee
---
title: rickettm Add repo parameter to gitaly:install and workhorse:install rake tasks
merge_request: 10979
author: M. Ricketts
---
title: Updated CI status favicons to include the tanuki
merge_request: 10923
author:
---
title: Add download button to project snippets
merge_request:
author:
---
title: Add missing project attributes to Import/Export
merge_request:
author:
---
title: Remove N+1 queries in processing MR references
merge_request:
author:
if ENV['ENABLE_QUERY_TRACE']
require 'active_record_query_trace'
ActiveRecordQueryTrace.enabled = 'true'
end
......@@ -50,8 +50,10 @@ namespace :admin do
resources :deploy_keys, only: [:index, :new, :create, :destroy]
resources :hooks, only: [:index, :create, :destroy] do
get :test
resources :hooks, only: [:index, :create, :edit, :update, :destroy] do
member do
get :test
end
end
resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
......
......@@ -44,7 +44,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
get :raw
post :mark_as_spam
end
end
......@@ -138,6 +138,8 @@ constraints(ProjectUrlConstrainer.new) do
collection do
get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
end
resources :deployments, only: [:index]
end
resource :cycle_analytics, only: [:show]
......@@ -185,7 +187,7 @@ constraints(ProjectUrlConstrainer.new) do
end
end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
member do
get :test
end
......
resources :snippets, concerns: :awardable do
member do
get 'raw'
get 'download'
get :raw
post :mark_as_spam
post :preview_markdown
end
scope module: :snippets do
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
delete :delete_attachment
end
end
end
end
get '/s/:username', to: redirect('/u/%{username}/snippets'),
......
class AddCreatedAtIndexToDeployments < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :deployments, :created_at
end
def down
remove_concurrent_index :deployments, :created_at
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment