Commit dd66c7b7 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'rc/ce-to-ee-wednesday' into 'master'

CE Upstream - Wednesday

Closes gitlab-ci-multi-runner#2284

See merge request !1577
parents 9cfdb603 ec3044ac
......@@ -281,8 +281,6 @@ docs:check:apilint:
image: "phusion/baseimage"
stage: test
<<: *dedicated-runner
variables:
GIT_DEPTH: "3"
cache: {}
dependencies: []
before_script: []
......@@ -293,8 +291,6 @@ docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
variables:
GIT_DEPTH: "3"
cache: {}
dependencies: []
before_script: []
......
......@@ -476,10 +476,10 @@ AwardsHandler.prototype.setupSearch = function setupSearch() {
this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
const term = $(e.target).val().trim();
// Clean previous search results
$('ul.emoji-menu-search, h5.emoji-search').remove();
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
if (term.length > 0) {
// Generate a search result block
const h5 = $('<h5 class="emoji-search" />').text('Search results');
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.searchEmojis(term).show();
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
......
/* eslint-disable no-new */
import Vue from 'vue';
import PDFLab from 'vendor/pdflab';
import workerSrc from 'vendor/pdf.worker';
Vue.use(PDFLab, {
workerSrc,
});
export default () => {
const el = document.getElementById('js-pdf-viewer');
new Vue({
el,
data() {
return {
error: false,
loadError: false,
loading: true,
pdf: el.dataset.endpoint,
};
},
methods: {
onLoad() {
this.loading = false;
},
onError(error) {
this.loading = false;
this.loadError = true;
this.error = error;
},
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="PDF loading">
</i>
</div>
<pdf-lab
v-if="!loadError"
:pdf="pdf"
@pdflabload="onLoad"
@pdflaberror="onError" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occured whilst loading the file. Please try again later.
</span>
<span v-else>
An error occured whilst decoding the file.
</span>
</p>
</div>
`,
});
};
import renderPDF from './pdf';
document.addEventListener('DOMContentLoaded', renderPDF);
import JSZip from 'jszip';
import JSZipUtils from 'jszip-utils';
export default class SketchLoader {
constructor(container) {
this.container = container;
this.loadingIcon = this.container.querySelector('.js-loading-icon');
this.load();
}
load() {
return this.getZipFile()
.then(data => JSZip.loadAsync(data))
.then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array'))
.then((content) => {
const url = window.URL || window.webkitURL;
const blob = new Blob([new Uint8Array(content)], {
type: 'image/png',
});
const previewUrl = url.createObjectURL(blob);
this.render(previewUrl);
})
.catch(this.error.bind(this));
}
getZipFile() {
return new JSZip.external.Promise((resolve, reject) => {
JSZipUtils.getBinaryContent(this.container.dataset.endpoint, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
render(previewUrl) {
const previewLink = document.createElement('a');
const previewImage = document.createElement('img');
previewLink.href = previewUrl;
previewLink.target = '_blank';
previewImage.src = previewUrl;
previewImage.className = 'img-responsive';
previewLink.appendChild(previewImage);
this.container.appendChild(previewLink);
this.removeLoadingIcon();
}
error() {
const errorMsg = document.createElement('p');
errorMsg.className = 'prepend-top-default append-bottom-default text-center';
errorMsg.textContent = `
Cannot show preview. For previews on sketch files, they must have the file format
introduced by Sketch version 43 and above.
`;
this.container.appendChild(errorMsg);
this.removeLoadingIcon();
}
removeLoadingIcon() {
if (this.loadingIcon) {
this.loadingIcon.remove();
}
}
}
/* eslint-disable no-new */
import SketchLoader from './sketch';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('js-sketch-viewer');
new SketchLoader(el);
});
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */
import Vue from 'vue';
import boardList from './board_list';
import boardBlankState from './board_blank_state';
require('./board_delete');
......@@ -16,7 +16,7 @@ require('./board_list');
gl.issueBoards.Board = Vue.extend({
template: '#js-board-template',
components: {
'board-list': gl.issueBoards.BoardList,
boardList,
'board-delete': gl.issueBoards.BoardDelete,
boardBlankState,
},
......
/* eslint-disable comma-dangle, space-before-function-paren, max-len */
/* global Sortable */
import Vue from 'vue';
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
import eventHub from '../eventhub';
(() => {
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
const Store = gl.issueBoards.BoardsStore;
gl.issueBoards.BoardList = Vue.extend({
template: '#js-board-list-template',
components: {
boardCard,
boardNewIssue,
},
export default {
name: 'BoardList',
props: {
disabled: Boolean,
list: Object,
issues: Array,
loading: Boolean,
issueLinkBase: String,
rootPath: String,
},
data () {
disabled: {
type: Boolean,
required: true,
},
list: {
type: Object,
required: true,
},
issues: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
},
data() {
return {
scrollOffset: 250,
filters: Store.state.filters,
showCount: false,
showIssueForm: false
showIssueForm: false,
};
},
watch: {
filters: {
handler () {
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
deep: true
},
issues () {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
this.list.page += 1;
this.list.getIssues(false);
}
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
});
}
components: {
boardCard,
boardNewIssue,
},
methods: {
listHeight () {
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight () {
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop () {
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
loadNextPage () {
loadNextPage() {
const getIssues = this.list.nextPage();
if (getIssues) {
......@@ -79,11 +68,40 @@ import boardCard from './board_card';
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
},
},
watch: {
filters: {
handler() {
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
deep: true,
},
issues() {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() &&
this.list.issuesSize > this.list.issues.length) {
this.list.page += 1;
this.list.getIssues(false);
}
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
});
},
},
created() {
gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
},
mounted () {
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues',
......@@ -100,7 +118,8 @@ import boardCard from './board_card';
gl.issueBoards.onStart();
},
onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
gl.issueBoards.BoardsStore
.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
this.$nextTick(() => {
e.item.remove();
......@@ -108,24 +127,71 @@ import boardCard from './board_card';
},
onUpdate: (e) => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
gl.issueBoards.BoardsStore
.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
},
onMove(e) {
return !e.related.classList.contains('board-list-count');
}
},
});
this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more
this.$refs.list.onscroll = () => {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
};
this.$refs.list.addEventListener('scroll', this.onScroll);
},
beforeDestroy() {
gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
},
});
})();
eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
this.$refs.list.removeEventListener('scroll', this.onScroll);
},
template: `
<div class="board-list-component">
<div
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
</div>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
ref="issue"
:index="index"
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
<li
class="board-list-count text-center"
v-if="showCount"
data-id="-1">
<i
class="fa fa-spinner fa-spin"
aria-label="Loading more issues"
aria-hidden="true"
v-show="list.loadingMore">
</i>
<span v-if="list.issues.length === list.issuesSize">
Showing all issues
</span>
<span v-else>
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
</span>
</li>
</ul>
</div>
`,
};
/* global ListIssue */
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
......@@ -53,7 +55,7 @@ export default {
},
cancel() {
this.title = '';
gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`);
eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
},
mounted() {
......
/* eslint-disable import/prefer-default-export */
/**
* Function that allows a number with an X amount of decimals
* to be formatted in the following fashion:
* * For 1 digit to the left of the decimal point and X digits to the right of it
* * * Show 3 digits to the right
* * For 2 digits to the left of the decimal point and X digits to the right of it
* * * Show 2 digits to the right
*/
export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
let formattedNumber = '';
if (!isNaN(Number(number))) {
digitsLeft = number.split('.')[0];
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
break;
case 2:
relevantDigits = 2;
break;
case 3:
relevantDigits = 1;
break;
default:
relevantDigits = 4;
break;
}
formattedNumber = Number(number).toFixed(relevantDigits);
}
return formattedNumber;
}
/* eslint-disable no-new*/
/* eslint-disable no-new */
/* global Flash */
import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status';
import '../lib/utils/common_utils';
import { formatRelevantDigits } from '~/lib/utils/number_utils';
import '../flash';
const prometheusGraphsContainer = '.prometheus-graph';
......@@ -21,19 +21,19 @@ class PrometheusGraph {
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
extraAddedWidthParent;
this.originalWidth = parentContainerWidth;
this.originalHeight = 400;
this.originalHeight = 330;
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = 400 - this.margin.top - this.margin.bottom;
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
this.configureGraph();
this.init();
}
createGraph() {
Object.keys(this.data).forEach((key) => {
const value = this.data[key];
if (value.length > 0) {
this.plotValues(value, key);
Object.keys(this.graphSpecificProperties).forEach((key) => {
const value = this.graphSpecificProperties[key];
if (value.data.length > 0) {
this.plotValues(key);
}
});
}
......@@ -49,16 +49,19 @@ class PrometheusGraph {
});
}
plotValues(valuesToPlot, key) {
plotValues(key) {
const graphSpecifics = this.graphSpecificProperties[key];
const x = d3.time.scale()
.range([0, this.width]);
const y = d3.scale.linear()
.range([this.height, 0]);
const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
graphSpecifics.xScale = x;
graphSpecifics.yScale = y;
const graphSpecifics = this.graphSpecificProperties[key];
const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const chart = d3.select(prometheusGraphContainer)
.attr('width', this.width + this.margin.left + this.margin.right)
......@@ -67,13 +70,13 @@ class PrometheusGraph {
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer)
.attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right)
.attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top)
.attr('width', this.originalWidth)
.attr('height', this.originalHeight)
.append('g')
.attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`);
x.domain(d3.extent(valuesToPlot, d => d.time));
y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]);
x.domain(d3.extent(graphSpecifics.data, d => d.time));
y.domain([0, d3.max(graphSpecifics.data.map(metricValue => metricValue.value))]);
const xAxis = d3.svg.axis()
.scale(x)
......@@ -108,13 +111,13 @@ class PrometheusGraph {
.y(d => y(d.value));
chart.append('path')
.datum(valuesToPlot)
.datum(graphSpecifics.data)
.attr('d', area)
.attr('class', 'metric-area')
.attr('fill', graphSpecifics.area_fill_color);
chart.append('path')
.datum(valuesToPlot)
.datum(graphSpecifics.data)
.attr('class', 'metric-line')
.attr('stroke', graphSpecifics.line_color)
.attr('fill', 'none')
......@@ -126,7 +129,7 @@ class PrometheusGraph {
.attr('class', 'prometheus-graph-overlay')
.attr('width', this.width)
.attr('height', this.height)
.on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key));
.on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer));
}
// The legends from the metric
......@@ -138,10 +141,10 @@ class PrometheusGraph {
.attr('stroke', '#000000')
.attr('stroke-width', '1')
.attr({
x1: 0,
y1: this.originalHeight - this.marginLabelContainer.top,
x2: this.originalWidth - this.margin.right,
y2: this.originalHeight - this.marginLabelContainer.top,
x1: 10,
y1: this.originalHeight - this.margin.top,
x2: (this.originalWidth - this.margin.right) + 10,
y2: this.originalHeight - this.margin.top,
});
axisLabelContainer.append('line')
......@@ -149,29 +152,36 @@ class PrometheusGraph {
.attr('stroke', '#000000')
.attr('stroke-width', '1')
.attr({
x1: 0,
x1: 10,
y1: 0,
x2: 0,
y2: this.originalHeight - this.marginLabelContainer.top,
x2: 10,
y2: this.originalHeight - this.margin.top,
});
axisLabelContainer.append('rect')
.attr('class', 'rect-axis-text')
.attr('x', 0)
.attr('y', 50)
.attr('width', 30)
.attr('height', 150);
axisLabelContainer.append('text')
.attr('class', 'label-axis-text')
.attr('text-anchor', 'middle')
.attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`)
.attr('transform', `translate(15, ${(this.originalHeight - this.margin.top) / 2}) rotate(-90)`)
.text(graphSpecifics.graph_legend_title);
axisLabelContainer.append('rect')
.attr('class', 'rect-axis-text')
.attr('x', (this.originalWidth / 2) - this.margin.right)
.attr('y', this.originalHeight - this.marginLabelContainer.top - 20)
.attr('y', this.originalHeight - 100)
.attr('width', 30)
.attr('height', 80);
axisLabelContainer.append('text')
.attr('class', 'label-axis-text')
.attr('x', (this.originalWidth / 2) - this.margin.right)
.attr('y', this.originalHeight - this.marginLabelContainer.top)
.attr('y', this.originalHeight - this.margin.top)
.attr('dy', '.35em')
.text('Time');
......@@ -186,7 +196,7 @@ class PrometheusGraph {
.attr('height', 35);
axisLabelContainer.append('text')
.attr('class', 'label-axis-text')
.attr('class', 'text-metric-title')
.attr('x', this.originalWidth - 140)
.attr('y', (this.originalHeight / 2) - 50)
.text('Average');
......@@ -197,65 +207,80 @@ class PrometheusGraph {
.attr('y', (this.originalHeight / 2) - 25);
}
handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) {
handleMouseOverGraph(prometheusGraphContainer) {
const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`);
const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]);
const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1);
const d0 = valuesToPlot[timeValueIndex - 1];
const d1 = valuesToPlot[timeValueIndex];
const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0;
const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value)));
const currentTimeCoordinate = x(currentData.time);
const graphSpecifics = this.graphSpecificProperties[key];
// Remove the current selectors
d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove();
d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove();
d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove();
d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove();
chart.append('line')
const currentXCoordinate = d3.mouse(rectOverlay)[0];
Object.keys(this.graphSpecificProperties).forEach((key) => {
const currentGraphProps = this.graphSpecificProperties[key];
const timeValueOverlay = currentGraphProps.xScale.invert(currentXCoordinate);
const overlayIndex = bisectDate(currentGraphProps.data, timeValueOverlay, 1);
const d0 = currentGraphProps.data[overlayIndex - 1];
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 currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
// 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();
const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line')
.attr('class', 'selected-metric-line')
.attr({
x1: currentTimeCoordinate,
y1: y(0),
y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate,
y2: maxValueMetric,
y2: maxMetricValue,
});
chart.append('circle')
currentChart.append('circle')
.attr('class', 'circle-metric')
.attr('fill', graphSpecifics.line_color)
.attr('fill', currentGraphProps.line_color)
.attr('cx', currentTimeCoordinate)
.attr('cy', y(currentData.value))
.attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric);
// The little box with text
const rectTextMetric = chart.append('g')
const rectTextMetric = currentChart.append('g')
.attr('class', 'rect-text-metric')
.attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`);
.attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`);
rectTextMetric.append('rect')
.attr('class', 'rect-metric')
.attr('x', currentTimeCoordinate + 10)
.attr('y', maxValueMetric)
.attr('y', maxMetricValue)
.attr('width', this.commonGraphProperties.rect_text_width)
.attr('height', this.commonGraphProperties.rect_text_height);
rectTextMetric.append('text')
.attr('class', 'text-metric')
.attr('x', currentTimeCoordinate + 35)
.attr('y', maxValueMetric + 35)
.attr('y', maxMetricValue + 35)
.text(timeFormat(currentData.time));
rectTextMetric.append('text')
.attr('class', 'text-metric-date')
.attr('x', currentTimeCoordinate + 15)
.attr('y', maxValueMetric + 15)
.attr('y', maxMetricValue + 15)
.text(dayFormat(currentData.time));
// Update the text
d3.select(`${prometheusGraphContainer} .text-metric-usage`)
.text(currentData.value.substring(0, 8));
let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') {
currentMetricValue = `${currentMetricValue}%`;
} else {
currentMetricValue = `${currentMetricValue} MB`;
}
d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`)
.text(currentMetricValue);
});
}
configureGraph() {
......@@ -263,12 +288,18 @@ class PrometheusGraph {
cpu_values: {
area_fill_color: '#edf3fc',
line_color: '#5b99f7',
graph_legend_title: 'CPU utilization (%)',
graph_legend_title: 'CPU Usage (Cores)',
data: [],
xScale: {},
yScale: {},
},
memory_values: {
area_fill_color: '#fca326',
line_color: '#fc6d26',
graph_legend_title: 'Memory usage (MB)',
graph_legend_title: 'Memory Usage (MB)',
data: [],
xScale: {},
yScale: {},
},
};
......@@ -318,17 +349,17 @@ class PrometheusGraph {
}
transformData(metricsResponse) {
const metricTypes = {};
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0];
metricTypes[key] = metricValues.values.map(metric => ({
if (typeof metricValues !== 'undefined') {
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
}));
}
}
});
this.data = metricTypes;
}
}
......
......@@ -292,6 +292,10 @@
}
@media(min-width: $screen-xs-max) {
&.merge-requests .text-content {
margin-top: 40px;
}
&.labels .text-content {
margin-top: 70px;
}
......
......@@ -46,7 +46,7 @@
}
.issue-boards-page {
.page-with-sidebar {
.content-wrapper {
padding-bottom: 0;
}
......@@ -101,8 +101,8 @@
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px);
min-height: 409px;
height: calc(100vh - 222px);
min-height: 475px;
transition: width .2s;
&.is-compact {
......
......@@ -309,6 +309,16 @@
text {
fill: $stat-graph-axis-fill;
}
.label-axis-text,
.text-metric-usage {
fill: $black;
font-weight: 500;
}
.legend-axis-text {
fill: $black;
}
}
.x-axis path,
......
......@@ -459,20 +459,13 @@ a.deploy-project-label {
flex-wrap: wrap;
.btn {
margin: 0 10px 10px 0;
padding: 8px;
margin-left: 10px;
}
> div {
margin-bottom: 10px;
padding-left: 0;
&:last-child {
margin-bottom: 0;
.btn {
margin-right: 0;
}
}
}
}
}
......
......@@ -10,6 +10,7 @@ class Groups::ApplicationController < ApplicationController
unless @group
id = params[:group_id] || params[:id]
@group = Group.find_by_full_path(id)
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
unless @group && can?(current_user, :read_group, @group)
@group = nil
......
class Import::BaseController < ApplicationController
private
def find_or_create_namespace(name, owner)
return current_user.namespace if name == owner
def find_or_create_namespace(names, owner)
return current_user.namespace if names == owner
return current_user.namespace unless current_user.can_create_group?
names = params[:target_namespace].presence || names
full_path_namespace = Namespace.find_by_full_path(names)
return full_path_namespace if full_path_namespace
names.split('/').inject(nil) do |parent, name|
begin
name = params[:target_namespace].presence || name
namespace = Group.create!(name: name, path: name, owner: current_user)
namespace = Group.create!(name: name,
path: name,
owner: current_user,
parent: parent)
namespace.add_owner(current_user)
namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
Namespace.find_by_full_path(name)
Namespace.where(parent: parent).find_by_path_or_name(name)
end
end
end
end
......@@ -9,7 +9,7 @@
# state: 'open' or 'closed' or 'all'
# group_id: integer
# project_id: integer
# milestone_id: integer
# milestone_title: string
# assignee_id: integer
# search: string
# label_name: string
......
......@@ -9,7 +9,7 @@
# state: 'open' or 'closed' or 'all'
# group_id: integer
# project_id: integer
# milestone_id: integer
# milestone_title: string
# assignee_id: integer
# search: string
# label_name: string
......
......@@ -46,10 +46,18 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG'
end
def pdf?
name && File.extname(name) == '.pdf'
end
def ipython_notebook?
text? && language&.name == 'Jupyter Notebook'
end
def sketch?
binary? && extname.downcase.delete('.') == 'sketch'
end
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
......@@ -67,8 +75,12 @@ class Blob < SimpleDelegator
end
elsif image? || svg?
'image'
elsif pdf?
'pdf'
elsif ipython_notebook?
'notebook'
elsif sketch?
'sketch'
elsif text?
'text'
else
......
......@@ -541,6 +541,8 @@ module Ci
end
def dependencies
return [] if empty_dependencies?
depended_jobs = depends_on_builds
return depended_jobs unless options[:dependencies].present?
......@@ -550,6 +552,10 @@ module Ci
end
end
def empty_dependencies?
options[:dependencies]&.empty?
end
private
def update_artifacts_size
......
......@@ -177,6 +177,16 @@ class MergeRequestDiff < ActiveRecord::Base
st_commits.count
end
def utf8_st_diffs
return [] if st_diffs.blank?
st_diffs.map do |diff|
diff.each do |k, v|
diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
end
end
end
private
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
......@@ -270,14 +280,6 @@ class MergeRequestDiff < ActiveRecord::Base
project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha)
end
def utf8_st_diffs
st_diffs.map do |diff|
diff.each do |k, v|
diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
end
end
end
#
# #save or #update_attributes providing changes on serialized attributes do a lot of
# serialization and deserialization calls resulting in bad performance.
......
......@@ -62,6 +62,7 @@ module Users
:email,
:external,
:force_random_password,
:password_automatically_set,
:hide_no_password,
:hide_no_ssh_key,
:key_id,
......@@ -85,6 +86,7 @@ module Users
[
:email,
:email_confirmation,
:password_automatically_set,
:name,
:password,
:username
......
- page_title "Merge Requests"
.top-area
- if @group_merge_requests.empty?
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- if current_user
.nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request"
= render 'shared/issuable/filter', type: :merge_requests
= render 'shared/issuable/filter', type: :merge_requests
.row-content-block.second-block
.row-content-block.second-block
Only merge requests from
%strong= @group.name
group are listed here.
- if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
= render 'shared/merge_requests'
.prepend-top-default
= render 'shared/merge_requests'
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pdf_viewer')
.file-content#js-pdf-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sketch_viewer')
.file-content#js-sketch-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
.js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
= icon('spinner spin 2x', 'aria-hidden' => 'true');
......@@ -9,7 +9,6 @@
= page_specific_javascript_bundle_tag('boards')
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head"
......
.board-list-component
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ ":list" => "list",
"v-if" => 'list.type !== "closed" && showIssueForm' }
%ul.board-list{ "ref" => "list",
"v-show" => "!loading",
":data-board" => "list.id",
":class" => '{ "is-smaller": showIssueForm }' }
%board-card{ "v-for" => "(issue, index) in issues",
"ref" => "issue",
":index" => "index",
":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
":disabled" => "disabled",
":key" => "issue.id" }
%li.board-list-count.text-center{ "v-if" => "showCount",
"data-issue-id" => "-1" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
%span{ "v-else" => true }
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
- if environment.external_url && can?(current_user, :read_environment, environment)
= link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
= icon('external-link')
View deployment
......@@ -4,3 +4,4 @@
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
= icon('area-chart')
Monitoring
......@@ -11,7 +11,7 @@
.col-sm-6
%h3.page-title
Environment:
= @environment.name
= link_to @environment.name, environment_path(@environment)
.col-sm-6
.nav-controls
......
......@@ -4,9 +4,9 @@
%div{ class: container_class }
.top-area.adjust
.col-md-9
.col-md-7
%h3.page-title= @environment.name
.col-md-3
.col-md-5
.nav-controls
= render 'projects/environments/metrics_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
......
%ul.content-list.mr-list.issuable-list
- if @merge_requests.exists?
= render @merge_requests
- if @merge_requests.blank?
%li
.nothing-here-block No merge requests to show
- else
= render 'shared/empty_states/merge_requests'
- if @merge_requests.present?
= paginate @merge_requests, theme: "gitlab"
......@@ -7,16 +7,19 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('filtered_search')
%div{ class: container_class }
- if @project.merge_requests.exists?
%div{ class: container_class }
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
New Merge Request
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do
New merge request
= render 'shared/issuable/search_bar', type: :merge_requests
.merge-requests-holder
= render 'merge_requests'
- else
= render 'shared/empty_states/merge_requests', button_path: new_namespace_project_merge_request_path(@project.namespace, @project)
......@@ -78,7 +78,7 @@
- if git_import_enabled?
%button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL')
.import_gitlab_project
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
......@@ -109,6 +109,9 @@
%p Please wait a moment, this page will automatically refresh when ready.
:javascript
var importBtnTooltip = "Please enter a valid project name.";
var $importBtnWrapper = $('.import_gitlab_project');
$('.how_to_import_link').bind('click', function (e) {
e.preventDefault();
var import_modal = $(this).next(".modal").show();
......@@ -123,15 +126,8 @@
$(".btn_import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
});
$('.btn_import_gitlab_project').attr('disabled',true)
$('.import_gitlab_project').attr('title', 'Project path and name required.');
$('.import_gitlab_project').click(function( event ) {
if($('.btn_import_gitlab_project').attr('disabled')) {
event.preventDefault();
new Flash("Please enter path and name for the project to be imported to.");
}
});
$('.btn_import_gitlab_project').attr('disabled', $('#project_path').val().trim().length === 0);
$importBtnWrapper.attr('title', importBtnTooltip);
$('#new_project').submit(function(){
var $path = $('#project_path');
......@@ -139,13 +135,13 @@
});
$('#project_path').keyup(function(){
if($(this).val().length !=0) {
if($(this).val().trim().length !== 0) {
$('.btn_import_gitlab_project').attr('disabled', false);
$('.import_gitlab_project').attr('title','');
$(".flash-container").html("")
$importBtnWrapper.attr('title','');
$importBtnWrapper.removeClass('has-tooltip');
} else {
$('.btn_import_gitlab_project').attr('disabled',true);
$('.import_gitlab_project').attr('title', 'Project path and name required.');
$importBtnWrapper.addClass('has-tooltip');
}
});
......
- commit_message = @page.persisted? ? "Update #{@page.title}" : "Create #{@page.title}"
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page)
......@@ -28,7 +30,7 @@
.form-group
= f.label :commit_message, class: 'control-label'
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18, value: commit_message
.form-actions
- if @page && @page.persisted?
......
......@@ -6,4 +6,4 @@
= paginate @merge_requests, theme: "gitlab"
- else
.nothing-here-block No merge requests to show
= render 'shared/empty_states/merge_requests'
- button_path = local_assigns.fetch(:button_path, false)
- project_select_button = local_assigns.fetch(:project_select_button, false)
- has_button = button_path || project_select_button
.row.empty-state.merge-requests
.col-xs-12{ class: "#{'col-sm-6 pull-right' if has_button}" }
.svg-content
= render 'shared/empty_states/icons/merge_requests.svg'
.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.text-content
- if has_button
%h4
Merge requests are a place to propose changes you've made to a project and discuss those changes with others.
%p
Interested parties can even contribute by pushing commits if they want to.
- if project_select_button
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: 'New merge request'
- else
= link_to 'New merge request', button_path, class: 'btn btn-new', title: 'New merge request', id: 'new_merge_request_link'
- else
%h4.text-center
There are no merge requests to show.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="755 221 385 225" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="278" height="179" rx="10"/><mask id="d" width="278" height="179" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="e" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="f" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#F9F9F9" transform="translate(752 227)"><rect width="120" height="22" x="30" rx="11"/><rect width="132" height="22" y="44" rx="11"/><rect width="190" height="22" x="208" y="66" rx="11"/><rect width="158" height="22" x="129" y="197" rx="11"/><rect width="158" height="22" x="66" y="154" rx="11"/><rect width="350" height="22" x="31" y="110" rx="11"/><path d="M153 22H21h21.5c6 0 11 5 11 11s-5 11-11 11H21h132-36.5c-6 0-11-5-11-11s5-11 11-11H153zm252 66H288h36.5c6 0 11 5 11 11s-5 11-11 11H288h117-36.5c-6 0-11-5-11-11s5-11 11-11H405zm-244 44H44h36.5c6 0 11 5 11 11s-5 11-11 11H44h117-36.5c-6 0-11-5-11-11s5-11 11-11H161zm75 44H119h21.5c6 0 11 5 11 11s-5 11-11 11H119h117-51.5c-6 0-11-5-11-11s5-11 11-11H236z"/></g><g transform="translate(812 240)"><use fill="#FFF" stroke="#EEE" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#EEE" d="M4 29h271v4H4z"/><g transform="translate(34 60)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 93)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#FC6D26" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#FC6D26" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 126)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(157 59)"><rect width="6" height="2" y="1" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="23" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="34" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="33" fill="#EEE" rx="2"/><rect width="15" height="4" x="58" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="55" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="29" y="44" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" y="33" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="15" y="55" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" y="33" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="48" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="62" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="77" y="22" fill="#EEE" rx="2"/><rect width="6" height="2" y="45" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="56" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="67" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="66" fill="#6B4FBB" rx="2"/><rect width="15" height="4" x="39" y="88" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="77" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="88" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="77" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="34" y="66" fill="#EEE" rx="2"/><rect width="10" height="4" x="72" y="77" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="77" fill="#EEE" rx="2"/><rect width="6" height="2" y="78" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="89" fill="#FDE5D8" rx="1"/></g></g><g transform="translate(1057 221)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="8" mask="url(#e)" xlink:href="#b"/><rect width="29" height="3" x="14" y="14" fill="#FDB692" rx="1.5"/><rect width="39" height="3" x="14" y="23" fill="#FDB692" rx="1.5"/><rect width="29" height="3" x="14" y="32" fill="#FDB692" rx="1.5"/></g><g transform="translate(1046 285)"><circle cx="16" cy="15" r="15" fill="#FFF7F4" stroke="#FC6D26" stroke-width="3"/><path stroke="#FC6D26" stroke-width="2" d="M0 14h1c5 0 9.2-2.7 11.4-6.7M14 1V0"/><path stroke="#FC6D26" stroke-width="2" d="M7.8 3c3 4.3 7.8 7 13.2 7 3.3 0 6.3-1 9-2.7"/><circle cx="10.5" cy="17.5" r="1.5" fill="#FC6D26"/><circle cx="21.5" cy="17.5" r="1.5" fill="#FC6D26"/></g><g transform="translate(825 370)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M25 7h2.7C25 2.8 20.4 0 15 0 9.6 0 5 2.8 2.3 7H5l2.5-3L10 7l2.5-3L15 7l2.5-3L20 7l2.5-3L25 7z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g><g transform="matrix(-1 0 0 1 840 306)"><use fill="#FFF" stroke="#E2DCF2" stroke-width="8" mask="url(#f)" xlink:href="#c"/><rect width="29" height="3" x="24" y="14" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="23" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="32" fill="#6B4FBB" opacity=".5" rx="1.5"/></g></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>path-1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="_activity" fill="#7E7D7D">
<g id="Page-1">
<g id="path-1">
<path d="M5,0 C4.448,0 4,0.448 4,1 L4,3 L1,3 C0.448,3 0,3.448 0,4 L0,9 C0,9.552 0.448,10 1,10 L5,10 L5,8 L11,8 L11,10 L15,10 C15.552,10 16,9.552 16,9 L16,4 C16,3.448 15.552,3 15,3 L12,3 L12,1 C12,0.448 11.552,0 11,0 L5,0 L5,0 L5,0 L5,0 Z M6,2.5 C6,2.224 6.224,2 6.5,2 L9.5,2 C9.776,2 10,2.224 10,2.5 C10,2.776 9.776,3 9.5,3 L6.5,3 C6.224,3 6,2.776 6,2.5 L6,2.5 L6,2.5 L6,2.5 Z M6,11 L10.001,11 L10.001,9 L6,9 L6,11 L6,11 L6,11 L6,11 Z M11,11 L11,12 L5,12 L5,11 L1,11 C0.448,11 0,11.448 0,12 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,12 C16,11.448 15.552,11 15,11 L11,11 L11,11 L11,11 L11,11 Z"></path>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 240</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M3,8 C3,5.951 4.236,4.194 6,3.422 L6,0 L1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L6,16 L6,12.578 C4.236,11.806 3,10.049 3,8 M7,12.899 L7,16 L9,16 L9,12.899 C8.677,12.965 8.343,13 8,13 C7.657,13 7.323,12.965 7,12.899 M15,0 L10,0 L10,3.422 C11.764,4.194 13,5.951 13,8 C13,10.049 11.764,11.806 10,12.578 L10,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 M10,8 C10,9.105 9.105,10 8,10 C6.895,10 6,9.105 6,8 C6,6.895 6.895,6 8,6 C9.105,6 10,6.895 10,8 M4,8 C4,10.209 5.791,12 8,12 C10.209,12 12,10.209 12,8 C12,5.791 10.209,4 8,4 C5.791,4 4,5.791 4,8 M9,3.101 L9,0 L7,0 L7,3.101 C7.323,3.035 7.657,3 8,3 C8.343,3 8.677,3.035 9,3.101" id="Pasted-Image-240" fill="#7E7D7D"></path>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group">
<path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1" fill="#7E7C7C"></path>
<polygon id="Stroke-6" fill="#7E7C7C" points="2.0197351 9.86809696 6.4567351 6.52409696 5.79233671 6.46815759 9.53233671 10.4271576 9.87070552 10.78534 10.2338016 10.4522494 15.0258016 6.05624938 14.3497984 5.31935062 9.55779844 9.71535062 10.2592633 9.74044241 6.51926329 5.78144241 6.21208651 5.45627854 5.8548649 5.72550304 1.4178649 9.06950304"></polygon>
<path d="M7.0313,6.3928 C7.0313,6.9448 6.5833,7.3928 6.0313,7.3928 C5.4793,7.3928 5.0313,6.9448 5.0313,6.3928 C5.0313,5.8408 5.4793,5.3928 6.0313,5.3928 C6.5833,5.3928 7.0313,5.8408 7.0313,6.3928" id="Fill-8" fill="#FEFEFE"></path>
<path d="M6.5313,6.3928 C6.5313,6.66865763 6.30715763,6.8928 6.0313,6.8928 C5.75544237,6.8928 5.5313,6.66865763 5.5313,6.3928 C5.5313,6.11694237 5.75544237,5.8928 6.0313,5.8928 C6.30715763,5.8928 6.5313,6.11694237 6.5313,6.3928 L6.5313,6.3928 Z M7.5313,6.3928 C7.5313,5.56465763 6.85944237,4.8928 6.0313,4.8928 C5.20315763,4.8928 4.5313,5.56465763 4.5313,6.3928 C4.5313,7.22094237 5.20315763,7.8928 6.0313,7.8928 C6.85944237,7.8928 7.5313,7.22094237 7.5313,6.3928 L7.5313,6.3928 Z" id="Stroke-10" fill="#7E7C7C"></path>
<path d="M10.8854,9.8715 C10.8854,10.4235 10.4374,10.8715 9.8854,10.8715 C9.3334,10.8715 8.8854,10.4235 8.8854,9.8715 C8.8854,9.3195 9.3334,8.8715 9.8854,8.8715 C10.4374,8.8715 10.8854,9.3195 10.8854,9.8715" id="Fill-12" fill="#FEFEFE"></path>
<path d="M10.3854,9.8715 C10.3854,10.1473576 10.1612576,10.3715 9.8854,10.3715 C9.60954237,10.3715 9.3854,10.1473576 9.3854,9.8715 C9.3854,9.59564237 9.60954237,9.3715 9.8854,9.3715 C10.1612576,9.3715 10.3854,9.59564237 10.3854,9.8715 L10.3854,9.8715 Z M11.3854,9.8715 C11.3854,9.04335763 10.7135424,8.3715 9.8854,8.3715 C9.05725763,8.3715 8.3854,9.04335763 8.3854,9.8715 C8.3854,10.6996424 9.05725763,11.3715 9.8854,11.3715 C10.7135424,11.3715 11.3854,10.6996424 11.3854,9.8715 L11.3854,9.8715 Z" id="Stroke-14" fill="#7E7C7C"></path>
</g>
</g>
</svg>
\ No newline at end of file
<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 237</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Pasted-Image-237">
<path d="M15.1111,16 C15.6021,16 16.0001,15.602 16.0001,15.111 L16.0001,4.444 C15.5341,3.983 12.0671,0.378 11.5551,0 L0.8891,0 C0.3981,0 0.0001,0.398 0.0001,0.889 L0.0001,15.111 C0.0001,15.602 0.3981,16 0.8891,16 L15.1111,16 M14.0001,14.111 L1.8891,14.111 L1.8891,2 L10.8131,2 C11.4451,2.42 13.5811,4.555 14.0001,5.187 L14.0001,14.111" id="Fill-1" fill="#7E7D7D"></path>
<path d="M0.889,0 C0.398,0 0,0.398 0,0.889 L0,15.111 C0,15.602 0.398,16 0.889,16 L15.111,16 C15.602,16 16,15.602 16,15.111 L16,4.445 C15.534,3.983 12.068,0.377 11.555,0 L0.889,0 L0.889,0 Z M1.889,2 L10.813,2 C11.446,2.42 13.581,4.554 14,5.187 L14,14.111 L1.889,14.111 L1.889,2 L1.889,2 Z" id="Clip-4"></path>
<polygon id="Fill-6" fill="#7E7D7D" points="9 7 11 7 11 2 9 2"></polygon>
<polygon id="Clip-9" points="9 7 11 7 11 2.001 9 2.001"></polygon>
<polygon id="Fill-11" fill="#7E7D7D" points="10 7 15.444 7 15.444 5 10 5"></polygon>
<polygon id="Clip-14" points="10 7 15.444 7 15.444 5 10 5"></polygon>
</g>
</g>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play">
<path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="22px" height="16px" viewBox="0 0 22 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M6.4357,11.8588 C7.1487,11.2798 7.8797,10.7808 8.5357,10.3708 C8.5837,10.3008 8.6187,10.2338 8.6187,10.1768 L8.6187,8.8088 C8.9197,8.5218 9.0927,8.1248 9.0927,7.7028 L9.0927,5.3748 C9.0927,3.9478 7.9187,2.7858 6.4757,2.7858 L5.9687,2.7858 C4.5247,2.7858 3.3507,3.9478 3.3507,5.3748 L3.3507,7.7028 C3.3507,8.1248 3.5247,8.5218 3.8247,8.8088 L3.8247,10.5838 C3.2537,10.8738 1.8797,11.6198 0.5967,12.6618 C0.2177,12.9698 -0.0003,13.4258 -0.0003,13.9138 L-0.0003,15.5088 C-0.0003,15.5438 0.0857,15.7668 0.3467,15.7778 C1.3257,15.8198 3.8417,15.8328 5.9617,15.9038 C5.8337,15.8148 5.7447,15.6748 5.7447,15.5088 L5.7447,13.5498 C5.7447,12.9848 5.9967,12.2158 6.4357,11.8588" id="Fill-1"></path>
<path d="M21.3092,12.1 C19.6932,10.787 17.9592,9.86 17.3042,9.53 L17.3042,7.235 C17.6722,6.9 17.8862,6.428 17.8862,5.925 L17.8862,3.066 C17.8862,1.376 16.4952,0 14.7852,0 L14.1632,0 C12.4532,0 11.0622,1.376 11.0622,3.066 L11.0622,5.925 C11.0622,6.428 11.2752,6.9 11.6442,7.235 L11.6442,9.53 C10.9892,9.86 9.2542,10.787 7.6392,12.1 C7.2002,12.457 6.9482,12.985 6.9482,13.55 L6.9482,15.509 C6.9482,15.78 7.1702,16 7.4442,16 L14.1172,16 L14.1172,11.704 C12.6812,11.595 11.5652,10.853 11.5652,9.945 C11.5652,9.804 11.5982,9.669 11.6482,9.538 C11.9502,10.326 13.0982,10.913 14.4762,10.913 C15.8532,10.913 17.0012,10.326 17.3032,9.538 C17.3532,9.669 17.3862,9.804 17.3862,9.945 C17.3862,10.793 16.4152,11.5 15.1172,11.679 L15.1172,16 L21.5032,16 C21.7772,16 22.0002,15.78 22.0002,15.509 L22.0002,13.55 C22.0002,12.985 21.7482,12.457 21.3092,12.1" id="Fill-4"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path>
<polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon>
<polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon>
<path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M15.1111,0 L0.8891,0 C0.3981,0 0.0001,0.446 0.0001,0.996 L0.0001,14.945 C0.0001,15.495 0.3981,15.941 0.8891,15.941 L15.1111,15.941 C15.6021,15.941 16.0001,15.495 16.0001,14.945 L16.0001,0.996 C16.0001,0.446 15.6021,0 15.1111,0 L15.1111,0 L15.1111,0 Z M2.0001,13.949 L14.0001,13.949 L14.0001,1.993 L2.0001,1.993 L2.0001,13.949 Z M2,5.0002 L14,5.0002 L14,3.0002 L2,3.0002 L2,5.0002 Z" id="Combined-Shape"></path>
<path d="M8.547,12.0002 L12,12.0002 L12,10.0002 L8.547,10.0002 L8.547,12.0002 Z M5.2029,12 L3.9999,10.867 L5.2029,9.501 L3.9999,8.181 L5.2029,7 L7.4529,9.499 L5.2029,12 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 246</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M12.5,14 C11.672,14 11,13.328 11,12.5 C11,11.672 11.672,11 12.5,11 C13.328,11 14,11.672 14,12.5 C14,13.328 13.328,14 12.5,14 M12.5,9 L3.5,9 C1.567,9 0,10.567 0,12.5 C0,14.433 1.567,16 3.5,16 L12.5,16 C14.433,16 16,14.433 16,12.5 C16,10.567 14.433,9 12.5,9 M3.5,2 C4.328,2 5,2.672 5,3.5 C5,4.328 4.328,5 3.5,5 C2.672,5 2,4.328 2,3.5 C2,2.672 2.672,2 3.5,2 M3.5,7 L12.5,7 C14.433,7 16,5.433 16,3.5 C16,1.567 14.433,0 12.5,0 L3.5,0 C1.567,0 0,1.567 0,3.5 C0,5.433 1.567,7 3.5,7" id="Pasted-Image-246" fill="#303030"></path>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 241</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M2.004,12.9999459 L3.939,12.9999459 L3.939,4.99994585 L2.004,4.99994585 L2.004,12.9999459 Z M7.017,9.99994585 L13.018,9.99994585 L13.018,8.99994585 L7.017,8.99994585 L7.017,9.99994585 Z M7.017,7.99994585 L13.018,7.99994585 L13.018,6.99994585 L7.017,6.99994585 L7.017,7.99994585 Z M7.017,5.99994585 L13.018,5.99994585 L13.018,4.99994585 L7.017,4.99994585 L7.017,5.99994585 Z M14.754,-5.41499267e-05 L4.938,-5.41499267e-05 C4.386,-5.41499267e-05 3.938,0.44794585 3.938,0.99994585 L3.938,2.99994585 L1,2.99994585 C0.448,2.99994585 0,3.44794585 0,3.99994585 L0,12.9999459 C0.037,13.4999459 -0.25,16.0509459 3.938,15.9999459 L12.408,15.9999459 C12.408,15.9999459 15.754,15.9169459 15.754,13.9999459 L15.754,0.99994585 C15.754,0.44794585 15.306,-5.41499267e-05 14.754,-5.41499267e-05 L14.754,-5.41499267e-05 Z" id="Pasted-Image-241" fill="#7E7D7D"></path>
</g>
</svg>
\ No newline at end of file
---
title: Added merge requests empty state
merge_request: 7342
author:
---
title: Set GIT_TERMINAL_PROMPT env variable in initializer
merge_request: 10372
author:
---
title: Fix project title validation, prevent clicking on disabled button
merge_request: 9931
author:
---
title: Fixes milestone/merge_requests endpoint to actually scope the result
merge_request:
author: Joren De Groof
---
title: Removed the duplicated search icon in the award emoji menu
merge_request:
author:
---
title: Improved UX for the environments metrics view
merge_request: 9946
author:
---
title: Fix Import/Export MR diffs not showing and missing forked MRs
merge_request:
author:
---
title: Create subgroups if they don't exist while importing projects
merge_request:
author:
---
title: Fix wiki commit message
merge_request: 10464
author: blackst0ne
---
title: Do not set closed_at to nil when issue is reopened
merge_request:
author:
---
title: Handle SSH keys that have multiple spaces between each marker
merge_request:
author:
......@@ -153,6 +153,7 @@ module Gitlab
# This is needed for gitlab-shell
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
ENV['GIT_TERMINAL_PROMPT'] = '0'
# Gitlab Geo Middleware support
config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadonlyGeo'
......
......@@ -326,3 +326,21 @@
:why: https://github.com/domenic/opener/blob/1.4.3/LICENSE.txt
:versions: []
:when: 2017-02-21 22:33:41.729629000 Z
- - :approve
- jszip
- :who: Phil Hughes
:why: https://github.com/Stuk/jszip/blob/master/LICENSE.markdown
:versions: []
:when: 2017-04-05 10:38:46.275721000 Z
- - :approve
- jszip-utils
- :who: Phil Hughes
:why: https://github.com/Stuk/jszip-utils/blob/master/LICENSE.markdown
:versions: []
:when: 2017-04-05 10:39:32.676232000 Z
- - :approve
- pako
- :who: Phil Hughes
:why: https://github.com/nodeca/pako/blob/master/LICENSE
:versions: []
:when: 2017-04-05 10:43:45.897720000 Z
......@@ -40,6 +40,8 @@ var config = {
monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
sketch_viewer: './blob/sketch_viewer.js',
pdf_viewer: './blob/pdf_viewer.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js',
......@@ -67,7 +69,11 @@ var config = {
{
test: /\.svg$/,
use: 'raw-loader'
}
}, {
test: /\.(worker.js|pdf)$/,
exclude: /node_modules/,
loader: 'file-loader',
},
]
},
......@@ -109,6 +115,7 @@ var config = {
'issuable',
'merge_conflicts',
'notebook_viewer',
'pdf_viewer',
'vue_pipelines',
'mr_widget_ee',
],
......
......@@ -39,6 +39,12 @@ Instead you should use polling mechanism with ETag caching in Redis.
1. If the `If-None-Match` header does not match the current value in Redis
we have to generate a new response, because the resource changed.
Do not use query parameters (for example `?scope=all`) for endpoints where you
want to enable ETag caching. The middleware takes into account only the request
path and ignores query parameters. All parameters should be included in the
request path. By doing this we avoid query parameter ordering problems and make
route matching easier.
For more information see:
- [RFC 7232](https://tools.ietf.org/html/rfc7232)
- [ETag proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/26926)
......@@ -129,6 +129,9 @@ If you want to run the database separately, expect a size of about 1 MB per user
### PostgreSQL Requirements
As of GitLab 9.0, PostgreSQL 9.6 is recommended. Lower versions of PostgreSQL
may work but primary testing and developement takes place using PostgreSQL 9.6.
Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
GitLab database. This extension can be enabled (using a PostgreSQL super user)
by running the following query for every database:
......
......@@ -63,7 +63,8 @@ Feature: Dashboard
@javascript
Scenario: Visiting Project's merge requests after sorting
Given I visit dashboard merge requests page
Given project "Shop" has a "Bugfix MR" merge request open
And I visit dashboard merge requests page
And I sort the list by "Oldest updated"
And I visit project "Shop" merge requests page
Then The list should be sorted by "Oldest updated"
......@@ -56,14 +56,16 @@ Feature: Project Issues
@javascript
Scenario: Visiting Merge Requests after being sorted the list
Given I visit project "Shop" issues page
Given project "Shop" has a "Bugfix MR" merge request open
And I visit project "Shop" issues page
And I sort the list by "Oldest updated"
And I visit project "Shop" merge requests page
Then The list should be sorted by "Oldest updated"
@javascript
Scenario: Visiting Merge Requests from a differente Project after sorting
Given I visit project "Shop" merge requests page
Given project "Shop" has a "Bugfix MR" merge request open
And I visit project "Shop" merge requests page
And I sort the list by "Oldest updated"
And I visit dashboard merge requests page
Then The list should be sorted by "Oldest updated"
......
......@@ -42,8 +42,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I click link "New merge request"' do
expect(page).to have_content(/new merge request/i)
click_link "New Merge Request"
page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
end
step 'I should see the new merge request page for my namespace' do
......
......@@ -16,7 +16,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end
step 'I click link "New Merge Request"' do
click_link "New Merge Request"
page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
end
step 'I should see merge request "Merge Request On Forked Project"' do
......
......@@ -24,7 +24,9 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I click to emoji in the picker' do
page.within '.emoji-menu-content' do
page.first('.js-emoji-btn').click
emoji_button = page.first('.js-emoji-btn')
emoji_button.hover
emoji_button.click
end
end
......
......@@ -14,7 +14,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I click link "New Merge Request"' do
click_link "New Merge Request"
page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
end
step 'I click link "Bug NS-04"' do
......
......@@ -273,6 +273,10 @@ module SharedProject
@project.update(public_builds: false)
end
step 'project "Shop" has a "Bugfix MR" merge request open' do
create(:merge_request, title: "Bugfix MR", target_project: project, source_project: project, author: project.users.first)
end
def user_owns_project(user_name:, project_name:, visibility: :private)
user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
project = Project.find_by(name: project_name)
......
......@@ -139,7 +139,7 @@ module API
finder_params = {
project_id: user_project.id,
milestone_id: milestone.id,
milestone_title: milestone.title,
sort: 'position_asc'
}
......
module Gitlab
module EtagCaching
class Middleware
RESERVED_WORDS = ProjectPathValidator::RESERVED.map { |word| "/#{word}/" }.join('|')
RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|')
ROUTE_REGEXP = Regexp.union(
%r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z)
)
......
module Gitlab
module ImportExport
class HashUtil
def self.deep_symbolize_array!(array)
return if array.blank?
array.map! do |hash|
hash.deep_symbolize_keys!
yield(hash) if block_given?
hash
end
end
def self.deep_symbolize_array_with_date!(array)
self.deep_symbolize_array!(array) do |hash|
hash.select { |k, _v| k.to_s.end_with?('_date') }.each do |key, value|
hash[key] = Time.zone.parse(value)
end
end
end
end
end
end
......@@ -89,3 +89,5 @@ methods:
- :type
merge_request_diff:
- :utf8_st_diffs
merge_requests:
- :diff_head_sha
......@@ -9,7 +9,7 @@ module Gitlab
end
def execute
if import_file && check_version! && [project_tree, avatar_restorer, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore)
if import_file && check_version! && [repo_restorer, wiki_restorer, project_tree, avatar_restorer, uploads_restorer].all?(&:restore)
project_tree.restored_project
else
raise Projects::ImportService::Error.new(@shared.errors.join(', '))
......
module Gitlab
module ImportExport
class MergeRequestParser
FORKED_PROJECT_ID = -1
def initialize(project, diff_head_sha, merge_request, relation_hash)
@project = project
@diff_head_sha = diff_head_sha
@merge_request = merge_request
@relation_hash = relation_hash
end
def parse!
if fork_merge_request? && @diff_head_sha
@merge_request.source_project_id = @relation_hash['project_id']
fetch_ref unless branch_exists?(@merge_request.source_branch)
create_target_branch unless branch_exists?(@merge_request.target_branch)
end
@merge_request
end
def create_target_branch
@project.repository.create_branch(@merge_request.target_branch, @merge_request.target_branch_sha)
end
def fetch_ref
@project.repository.fetch_ref(@project.repository.path, @diff_head_sha, @merge_request.source_branch)
end
def branch_exists?(branch_name)
@project.repository.branch_exists?(branch_name)
end
def fork_merge_request?
@relation_hash['source_project_id'] == FORKED_PROJECT_ID
end
end
end
end
......@@ -119,7 +119,7 @@ module Gitlab
relation_hash: parsed_relation_hash(relation_hash),
members_mapper: members_mapper,
user: @user,
project_id: restored_project.id)
project: restored_project)
end.compact
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
......
......@@ -29,11 +29,12 @@ module Gitlab
new(*args).create
end
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
@relation_hash = relation_hash.except('noteable_id').merge('project_id' => project_id)
@relation_hash = relation_hash.except('noteable_id').merge('project_id' => project.id)
@members_mapper = members_mapper
@user = user
@project = project
@imported_object_retries = 0
end
......@@ -66,7 +67,7 @@ module Gitlab
remove_encrypted_attributes!
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diffs if @relation_name == :merge_request_diff
set_st_diff_commits if @relation_name == :merge_request_diff
end
def update_user_references
......@@ -105,6 +106,8 @@ module Gitlab
imported_object do |object|
object.commit_id = nil
end
elsif @relation_name == :merge_requests
MergeRequestParser.new(@project, @relation_hash.delete('diff_head_sha'), imported_object, @relation_hash).parse!
else
imported_object
end
......@@ -115,7 +118,7 @@ module Gitlab
# If source and target are the same, populate them with the new project ID.
if @relation_hash['source_project_id']
@relation_hash['source_project_id'] = same_source_and_target? ? project_id : -1
@relation_hash['source_project_id'] = same_source_and_target? ? project_id : MergeRequestParser::FORKED_PROJECT_ID
end
# project_id may not be part of the export, but we always need to populate it if required.
......@@ -166,6 +169,7 @@ module Gitlab
def imported_object
yield(existing_or_new_object) if block_given?
existing_or_new_object.importing = true if existing_or_new_object.respond_to?(:importing)
existing_or_new_object
rescue ActiveRecord::RecordNotUnique
# as the operation is not atomic, retry in the unlikely scenario an INSERT is
......@@ -188,8 +192,11 @@ module Gitlab
relation_class: relation_class)
end
def set_st_diffs
def set_st_diff_commits
@relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs')
HashUtil.deep_symbolize_array!(@relation_hash['st_diffs'])
HashUtil.deep_symbolize_array_with_date!(@relation_hash['st_commits'])
end
def existing_or_new_object
......
......@@ -35,7 +35,7 @@ module Gitlab
end
def strip_key(key)
key.split(/ /)[0, 2].join(' ')
key.split(/[ ]+/)[0, 2].join(' ')
end
private
......
......@@ -35,7 +35,8 @@ module Gitlab
feature_enabled = case action.to_s
when 'git_receive_pack'
Gitlab::GitalyClient.feature_enabled?(:post_receive_pack)
# Disabled for now, see https://gitlab.com/gitlab-org/gitaly/issues/172
false
when 'git_upload_pack'
Gitlab::GitalyClient.feature_enabled?(:post_upload_pack)
when 'info_refs'
......@@ -44,7 +45,12 @@ module Gitlab
raise "Unsupported action: #{action}"
end
params[:GitalySocketPath] = URI(address).path if feature_enabled
if feature_enabled
params[:GitalyAddress] = address
# TODO deprecate GitalySocketPath once GITLAB_WORKHORSE_VERSION points
# to a version that supports GitalyAddress.
params[:GitalySocketPath] = URI(address).path
end
end
params
......
......@@ -200,5 +200,72 @@ describe Import::BitbucketController do
end
end
end
context 'user has chosen an existing nested namespace and name for the project' do
let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, test_name, nested_namespace, user, access_params).
and_return(double(execute: true))
post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js }
end
end
context 'user has chosen a non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js }
end
it 'creates the namespaces' do
allow(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } }
.to change { Namespace.count }.by(2)
end
it 'new namespace has the right parent' do
allow(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js }
expect(Namespace.find_by_path_or_name('bar').parent.path).to eq('foo')
end
end
context 'user has chosen existent and non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js }
end
it 'creates the namespaces' do
allow(Gitlab::BitbucketImport::ProjectCreator).
to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } }
.to change { Namespace.count }.by(2)
end
end
end
end
......@@ -174,6 +174,72 @@ describe Import::GitlabController do
end
end
end
context 'user has chosen an existing nested namespace for the project' do
let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::GitlabImport::ProjectCreator).
to receive(:new).with(gitlab_repo, nested_namespace, user, access_params).
and_return(double(execute: true))
post :create, { target_namespace: nested_namespace.full_path, format: :js }
end
end
context 'user has chosen a non-existent nested namespaces for the project' do
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
expect(Gitlab::GitlabImport::ProjectCreator).
to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
post :create, { target_namespace: 'foo/bar', format: :js }
end
it 'creates the namespaces' do
allow(Gitlab::GitlabImport::ProjectCreator).
to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
expect { post :create, { target_namespace: 'foo/bar', format: :js } }
.to change { Namespace.count }.by(2)
end
it 'new namespace has the right parent' do
allow(Gitlab::GitlabImport::ProjectCreator).
to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
post :create, { target_namespace: 'foo/bar', format: :js }
expect(Namespace.find_by_path_or_name('bar').parent.path).to eq('foo')
end
end
context 'user has chosen existent and non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::GitlabImport::ProjectCreator).
to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
post :create, { target_namespace: 'foo/foobar/bar', format: :js }
end
it 'creates the namespaces' do
allow(Gitlab::GitlabImport::ProjectCreator).
to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
and_return(double(execute: true))
expect { post :create, { target_namespace: 'foo/foobar/bar', format: :js } }
.to change { Namespace.count }.by(2)
end
end
end
end
end
......@@ -2,7 +2,6 @@ require 'spec_helper'
feature "Admin Health Check", feature: true do
include StubENV
include WaitForAjax
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
......@@ -24,11 +23,12 @@ feature "Admin Health Check", feature: true do
expect(page).to have_selector('#health-check-token', text: token)
end
describe 'reload access token', js: true do
describe 'reload access token' do
it 'changes the access token' do
orig_token = current_application_settings.health_check_access_token
click_button 'Reset health check access token'
wait_for_ajax
expect(page).to have_content('New health check access token has been generated!')
expect(find('#health-check-token').text).not_to eq orig_token
end
end
......
require 'spec_helper'
describe 'Dashboard Merge Requests' do
let(:current_user) { create :user }
let(:project) do
create(:empty_project) do |project|
project.add_master(current_user)
end
end
before do
login_as(current_user)
end
it 'should show an empty state' do
visit merge_requests_dashboard_path(assignee_id: current_user.id)
expect(page).to have_selector('.empty-state')
end
context 'if there are merge requests' do
before do
create(:merge_request, assignee: current_user, source_project: project)
visit merge_requests_dashboard_path(assignee_id: current_user.id)
end
it 'should not show an empty state' do
expect(page).not_to have_selector('.empty-state')
end
end
end
require 'spec_helper'
feature 'Groups Merge Requests Empty States' do
let(:group) { create(:group) }
let(:user) { create(:group_member, :developer, user: create(:user), group: group ).user }
before do
login_as(user)
end
context 'group has a project' do
let(:project) { create(:empty_project, namespace: group) }
before do
project.add_master(user)
end
context 'the project has a merge request' do
before do
create(:merge_request, source_project: project)
visit merge_requests_group_path(group)
end
it 'should not display an empty state' do
expect(page).not_to have_selector('.empty-state')
end
end
context 'the project has no merge requests', :js do
before do
visit merge_requests_group_path(group)
end
it 'should display an empty state' do
expect(page).to have_selector('.empty-state')
end
it 'should show a new merge request button' do
within '.empty-state' do
expect(page).to have_content('New merge request')
end
end
it 'the new merge request button opens a project dropdown' do
within '.empty-state' do
find('.new-project-item-select-button').click
end
expect(page).to have_selector('.ajax-project-dropdown')
end
end
end
context 'group without a project' do
before do
visit merge_requests_group_path(group)
end
it 'should display an empty state' do
expect(page).to have_selector('.empty-state')
end
it 'should not show a new merge request button' do
within '.empty-state' do
expect(page).not_to have_link('New merge request')
end
end
end
end
require 'spec_helper'
describe 'Filter issues', js: true, feature: true do
include Devise::Test::IntegrationHelpers
include FilteredSearchHelpers
include WaitForAjax
......@@ -42,16 +43,17 @@ describe 'Filter issues', js: true, feature: true do
project.team << [user2, :master]
group.add_developer(user)
group.add_developer(user2)
login_as(user)
create(:issue, project: project)
create(:issue, title: "Bug report 1", project: project)
create(:issue, title: "Bug report 2", project: project)
create(:issue, title: "issue with 'single quotes'", project: project)
create(:issue, title: "issue with \"double quotes\"", project: project)
create(:issue, title: "issue with !@\#{$%^&*()-+", project: project)
create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user)
create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user)
sign_in(user)
create(:issue, project: project)
create(:issue, project: project, title: "Bug report 1")
create(:issue, project: project, title: "Bug report 2")
create(:issue, project: project, title: "issue with 'single quotes'")
create(:issue, project: project, title: "issue with \"double quotes\"")
create(:issue, project: project, title: "issue with !@\#{$%^&*()-+")
create(:issue, project: project, title: "issue by assignee", milestone: milestone, author: user, assignee: user)
create(:issue, project: project, title: "issue by assignee with searchTerm", milestone: milestone, author: user, assignee: user)
issue = create(:issue,
title: "Bug 2",
......
......@@ -15,7 +15,7 @@ feature 'Create New Merge Request', feature: true, js: true do
it 'selects the source branch sha when a tag with the same name exists' do
visit namespace_project_merge_requests_path(project.namespace, project)
click_link 'New Merge Request'
click_link 'New merge request'
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
......@@ -28,7 +28,7 @@ feature 'Create New Merge Request', feature: true, js: true do
it 'selects the target branch sha when a tag with the same name exists' do
visit namespace_project_merge_requests_path(project.namespace, project)
click_link 'New Merge Request'
click_link 'New merge request'
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
......@@ -42,7 +42,7 @@ feature 'Create New Merge Request', feature: true, js: true do
it 'generates a diff for an orphaned branch' do
visit namespace_project_merge_requests_path(project.namespace, project)
click_link 'New Merge Request'
page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
......
......@@ -69,12 +69,8 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
select2(namespace.id, from: '#project_namespace_id')
# click on disabled element
find(:link, 'GitLab export').trigger('click')
page.within('.flash-container') do
expect(page).to have_content('Please enter path and name')
end
# Check for tooltip disabled import button
expect(find('.import_gitlab_project')['title']).to eq('Please enter a valid project name.')
end
end
......
......@@ -17,4 +17,28 @@ feature 'Merge Requests List' do
expect(page).not_to have_selector('.js-new-board-list')
end
it 'should show an empty state' do
visit namespace_project_merge_requests_path(project.namespace, project)
expect(page).to have_selector('.empty-state')
end
it 'empty state should have a create merge request button' do
visit namespace_project_merge_requests_path(project.namespace, project)
expect(page).to have_link 'New merge request', href: new_namespace_project_merge_request_path(project.namespace, project)
end
context 'if there are merge requests' do
before do
create(:merge_request, assignee: user, source_project: project)
visit namespace_project_merge_requests_path(project.namespace, project)
end
it 'should not show an empty state' do
expect(page).not_to have_selector('.empty-state')
end
end
end
......@@ -15,6 +15,10 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
let(:project) { create(:project, namespace: user.namespace) }
context 'when wiki is empty' do
scenario 'commit message field has value "Create home"' do
expect(page).to have_field('wiki[message]', with: 'Create home')
end
scenario 'directly from the wiki home page' do
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
......@@ -37,6 +41,9 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
fill_in :new_wiki_path, with: 'foo'
click_button 'Create Page'
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create foo')
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
......@@ -51,6 +58,9 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
fill_in :new_wiki_path, with: 'Spaces in the name'
click_button 'Create Page'
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create spaces in the name')
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
......@@ -65,6 +75,9 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
fill_in :new_wiki_path, with: 'hyphens-in-the-name'
click_button 'Create Page'
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create hyphens in the name')
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
......@@ -80,6 +93,10 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
let(:project) { create(:project, namespace: create(:group, :public)) }
context 'when wiki is empty' do
scenario 'commit message field has value "Create home"' do
expect(page).to have_field('wiki[message]', with: 'Create home')
end
scenario 'directly from the wiki home page' do
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
......@@ -101,6 +118,9 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
fill_in :new_wiki_path, with: 'foo'
click_button 'Create Page'
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Create foo')
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
......
......@@ -19,6 +19,9 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
scenario 'success when the wiki content is not empty' do
click_link 'Edit'
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home')
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Save changes'
......@@ -48,6 +51,9 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
scenario 'the home page' do
click_link 'Edit'
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home')
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Save changes'
......
......@@ -164,6 +164,8 @@ describe "Search", feature: true do
end
context 'click the links in the category search dropdown', js: true do
let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
before do
page.find('#search').click
end
......
import renderPDF from '~/blob/pdf';
import testPDF from './test.pdf';
describe('PDF renderer', () => {
let viewer;
preloadFixtures('static/pdf_viewer.html.raw');
beforeEach(() => {
loadFixtures('static/pdf_viewer.html.raw');
viewer = document.getElementById('js-pdf-viewer');
viewer.dataset.endpoint = testPDF;
});
it('shows loading icon', () => {
renderPDF();
expect(
document.querySelector('.loading'),
).not.toBeNull();
});
describe('successful response', () => {
beforeEach((done) => {
renderPDF();
setTimeout(() => {
done();
}, 500);
});
it('does not show loading icon', () => {
expect(
document.querySelector('.loading'),
).toBeNull();
});
it('renders the PDF', () => {
expect(
document.querySelector('.pdf-viewer'),
).not.toBeNull();
});
it('renders the PDF page', () => {
expect(
document.querySelector('.pdf-page'),
).not.toBeNull();
});
});
describe('error getting file', () => {
beforeEach((done) => {
viewer.dataset.endpoint = 'invalid/endpoint';
renderPDF();
setTimeout(() => {
done();
}, 500);
});
it('does not show loading icon', () => {
expect(
document.querySelector('.loading'),
).toBeNull();
});
it('shows error message', () => {
expect(
document.querySelector('.md').textContent.trim(),
).toBe('An error occured whilst loading the file. Please try again later.');
});
});
});
/* eslint-disable no-new */
import JSZip from 'jszip';
import SketchLoader from '~/blob/sketch';
describe('Sketch viewer', () => {
const generateZipFileArrayBuffer = (zipFile, resolve, done) => {
zipFile
.generateAsync({ type: 'arrayBuffer' })
.then((content) => {
resolve(content);
setTimeout(() => {
done();
}, 100);
});
};
preloadFixtures('static/sketch_viewer.html.raw');
beforeEach(() => {
loadFixtures('static/sketch_viewer.html.raw');
});
describe('with error message', () => {
beforeEach((done) => {
spyOn(SketchLoader.prototype, 'getZipFile').and.callFake(() => new Promise((resolve, reject) => {
reject();
setTimeout(() => {
done();
});
}));
new SketchLoader(document.getElementById('js-sketch-viewer'));
});
it('renders error message', () => {
expect(
document.querySelector('#js-sketch-viewer p'),
).not.toBeNull();
expect(
document.querySelector('#js-sketch-viewer p').textContent.trim(),
).toContain('Cannot show preview.');
});
it('removes render the loading icon', () => {
expect(
document.querySelector('.js-loading-icon'),
).toBeNull();
});
});
describe('success', () => {
beforeEach((done) => {
spyOn(SketchLoader.prototype, 'getZipFile').and.callFake(() => new Promise((resolve) => {
const zipFile = new JSZip();
zipFile.folder('previews')
.file('preview.png', 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAA1JREFUeNoBAgD9/wAAAAIAAVMrnDAAAAAASUVORK5CYII=', {
base64: true,
});
generateZipFileArrayBuffer(zipFile, resolve, done);
}));
new SketchLoader(document.getElementById('js-sketch-viewer'));
});
it('does not render error message', () => {
expect(
document.querySelector('#js-sketch-viewer p'),
).toBeNull();
});
it('removes render the loading icon', () => {
expect(
document.querySelector('.js-loading-icon'),
).toBeNull();
});
it('renders preview img', () => {
const img = document.querySelector('#js-sketch-viewer img');
expect(img).not.toBeNull();
expect(img.classList.contains('img-responsive')).toBeTruthy();
});
it('renders link to image', () => {
const img = document.querySelector('#js-sketch-viewer img');
const link = document.querySelector('#js-sketch-viewer a');
expect(link.href).toBe(img.src);
expect(link.target).toBe('_blank');
});
});
describe('incorrect file', () => {
beforeEach((done) => {
spyOn(SketchLoader.prototype, 'getZipFile').and.callFake(() => new Promise((resolve) => {
const zipFile = new JSZip();
generateZipFileArrayBuffer(zipFile, resolve, done);
}));
new SketchLoader(document.getElementById('js-sketch-viewer'));
});
it('renders error message', () => {
expect(
document.querySelector('#js-sketch-viewer p'),
).not.toBeNull();
expect(
document.querySelector('#js-sketch-viewer p').textContent.trim(),
).toContain('Cannot show preview.');
});
});
});
/* global BoardService */
/* global boardsMockInterceptor */
/* global List */
/* global listObj */
/* global ListIssue */
import Vue from 'vue';
import _ from 'underscore';
import Sortable from 'vendor/Sortable';
import BoardList from '~/boards/components/board_list';
import eventHub from '~/boards/eventhub';
import '~/boards/mixins/sortable_default_options';
import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/stores/boards_store';
import './mock_data';
window.Sortable = Sortable;
describe('Board list component', () => {
let component;
beforeEach((done) => {
const el = document.createElement('div');
document.body.appendChild(el);
Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
gl.IssueBoardsApp = new Vue();
const BoardListComp = Vue.extend(BoardList);
const list = new List(listObj);
const issue = new ListIssue({
title: 'Testing',
iid: 1,
confidential: false,
labels: [],
});
list.issuesSize = 1;
list.issues.push(issue);
component = new BoardListComp({
el,
propsData: {
disabled: false,
list,
issues: list.issues,
loading: false,
issueLinkBase: '/issues',
rootPath: '/',
},
}).$mount();
Vue.nextTick(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
});
it('renders component', () => {
expect(
component.$el.classList.contains('board-list-component'),
).toBe(true);
});
it('renders loading icon', (done) => {
component.loading = true;
Vue.nextTick(() => {
expect(
component.$el.querySelector('.board-list-loading'),
).not.toBeNull();
done();
});
});
it('renders issues', () => {
expect(
component.$el.querySelectorAll('.card').length,
).toBe(1);
});
it('sets data attribute with issue id', () => {
expect(
component.$el.querySelector('.card').getAttribute('data-issue-id'),
).toBe('1');
});
it('shows new issue form', (done) => {
component.toggleForm();
Vue.nextTick(() => {
expect(
component.$el.querySelector('.board-new-issue-form'),
).not.toBeNull();
expect(
component.$el.querySelector('.is-smaller'),
).not.toBeNull();
done();
});
});
it('shows new issue form after eventhub event', (done) => {
eventHub.$emit(`hide-issue-form-${component.list.id}`);
Vue.nextTick(() => {
expect(
component.$el.querySelector('.board-new-issue-form'),
).not.toBeNull();
expect(
component.$el.querySelector('.is-smaller'),
).not.toBeNull();
done();
});
});
it('does not show new issue form for closed list', (done) => {
component.list.type = 'closed';
component.toggleForm();
Vue.nextTick(() => {
expect(
component.$el.querySelector('.board-new-issue-form'),
).toBeNull();
done();
});
});
it('shows count list item', (done) => {
component.showCount = true;
Vue.nextTick(() => {
expect(
component.$el.querySelector('.board-list-count'),
).not.toBeNull();
expect(
component.$el.querySelector('.board-list-count').textContent.trim(),
).toBe('Showing all issues');
done();
});
});
it('shows how many more issues to load', (done) => {
component.showCount = true;
component.list.issuesSize = 20;
Vue.nextTick(() => {
expect(
component.$el.querySelector('.board-list-count').textContent.trim(),
).toBe('Showing 1 of 20 issues');
done();
});
});
it('loads more issues after scrolling', (done) => {
spyOn(component.list, 'nextPage');
component.$refs.list.style.height = '100px';
component.$refs.list.style.overflow = 'scroll';
for (let i = 0; i < 19; i += 1) {
const issue = component.list.issues[0];
issue.id += 1;
component.list.issues.push(issue);
}
Vue.nextTick(() => {
component.$refs.list.scrollTop = 20000;
setTimeout(() => {
expect(component.list.nextPage).toHaveBeenCalled();
done();
});
});
});
it('shows loading more spinner', (done) => {
component.showCount = true;
component.list.loadingMore = true;
Vue.nextTick(() => {
expect(
component.$el.querySelector('.board-list-count .fa-spinner'),
).not.toBeNull();
done();
});
});
});
......@@ -279,8 +279,9 @@ describe('Environment', () => {
setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all');
done();
Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
done();
});
});
});
......
.file-content#js-pdf-viewer{ data: { endpoint: '/test' } }
.file-content#js-sketch-viewer{ data: { endpoint: '/test_sketch_file.sketch' } }
.js-loading-icon
import { formatRelevantDigits } from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
it('returns an empty string when the number is NaN', () => {
expect(formatRelevantDigits('fail')).toBe('');
});
it('returns 4 decimals when there is 4 plus digits to the left', () => {
const formattedNumber = formatRelevantDigits('1000.1234567');
const rightFromDecimal = formattedNumber.split('.')[1];
const leftFromDecimal = formattedNumber.split('.')[0];
expect(rightFromDecimal.length).toBe(4);
expect(leftFromDecimal.length).toBe(4);
});
it('returns 3 decimals when there is 1 digit to the left', () => {
const formattedNumber = formatRelevantDigits('0.1234567');
const rightFromDecimal = formattedNumber.split('.')[1];
const leftFromDecimal = formattedNumber.split('.')[0];
expect(rightFromDecimal.length).toBe(3);
expect(leftFromDecimal.length).toBe(1);
});
it('returns 2 decimals when there is 2 digits to the left', () => {
const formattedNumber = formatRelevantDigits('10.1234567');
const rightFromDecimal = formattedNumber.split('.')[1];
const leftFromDecimal = formattedNumber.split('.')[0];
expect(rightFromDecimal.length).toBe(2);
expect(leftFromDecimal.length).toBe(2);
});
it('returns 1 decimal when there is 3 digits to the left', () => {
const formattedNumber = formatRelevantDigits('100.1234567');
const rightFromDecimal = formattedNumber.split('.')[1];
const leftFromDecimal = formattedNumber.split('.')[0];
expect(rightFromDecimal.length).toBe(1);
expect(leftFromDecimal.length).toBe(3);
});
});
});
......@@ -37,9 +37,11 @@ describe('PrometheusGraph', () => {
it('transforms the data', () => {
this.prometheusGraph.init(prometheusMockData.metrics);
expect(this.prometheusGraph.data).toBeDefined();
expect(this.prometheusGraph.data.cpu_values.length).toBe(121);
expect(this.prometheusGraph.data.memory_values.length).toBe(121);
Object.keys(this.prometheusGraph.graphSpecificProperties, (key) => {
const graphProps = this.prometheusGraph.graphSpecificProperties[key];
expect(graphProps.data).toBeDefined();
expect(graphProps.data.length).toBe(121);
});
});
it('creates two graphs', () => {
......@@ -68,7 +70,7 @@ describe('PrometheusGraph', () => {
expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined();
expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined();
expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined();
expect($axisLabelContainer.find('rect').length).toBe(2);
expect($axisLabelContainer.find('rect').length).toBe(3);
expect($axisLabelContainer.find('text').length).toBe(4);
});
});
......
......@@ -125,6 +125,15 @@ describe Gitlab::Shell, lib: true do
expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
end
it 'handles multiple spaces in the key' do
io = spy(:io)
adder = described_class.new(io)
adder.add_key('key-42', "ssh-rsa foo")
expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
end
it 'raises an exception if the key contains a tab' do
expect do
described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar")
......
require 'spec_helper'
describe 'forked project import', services: true do
let(:user) { create(:user) }
let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
let!(:project) { create(:empty_project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
let(:forked_from_project) { create(:project) }
let(:fork_link) { create(:forked_project_link, forked_from_project: project_with_repo) }
let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
let(:repo_restorer) do
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, project: project)
end
let!(:merge_request) do
create(:merge_request, source_project: fork_link.forked_to_project, target_project: project_with_repo)
end
let(:saver) do
Gitlab::ImportExport::ProjectTreeSaver.new(project: project_with_repo, current_user: user, shared: shared)
end
let(:restorer) do
Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project)
end
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
saver.save
repo_saver.save
repo_restorer.restore
restorer.restore
end
after do
FileUtils.rm_rf(export_path)
FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
FileUtils.rm_rf(project.repository.path_to_repo)
end
it 'can access the MR' do
expect(project.merge_requests.first.ensure_ref_fetched.first).to include('refs/merge-requests/1/head')
end
end
require 'spec_helper'
describe Gitlab::ImportExport::HashUtil, lib: true do
let(:stringified_array) { [{ 'test' => 1 }] }
let(:stringified_array_with_date) { [{ 'test_date' => '2016-04-06 06:17:44 +0200' }] }
describe '.deep_symbolize_array!' do
it 'symbolizes keys' do
expect { described_class.deep_symbolize_array!(stringified_array) }.to change {
stringified_array.first.keys.first
}.from('test').to(:test)
end
end
describe '.deep_symbolize_array_with_date!' do
it 'symbolizes keys' do
expect { described_class.deep_symbolize_array_with_date!(stringified_array_with_date) }.to change {
stringified_array_with_date.first.keys.first
}.from('test_date').to(:test_date)
end
it 'transforms date strings into Time objects' do
expect { described_class.deep_symbolize_array_with_date!(stringified_array_with_date) }.to change {
stringified_array_with_date.first.values.first.class
}.from(String).to(ActiveSupport::TimeWithZone)
end
end
end
require 'spec_helper'
describe Gitlab::ImportExport::MergeRequestParser do
let(:user) { create(:user) }
let!(:project) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
let(:forked_from_project) { create(:project) }
let(:fork_link) { create(:forked_project_link, forked_from_project: project) }
let!(:merge_request) do
create(:merge_request, source_project: fork_link.forked_to_project, target_project: project)
end
let(:parsed_merge_request) do
described_class.new(project,
merge_request.diff_head_sha,
merge_request,
merge_request.as_json).parse!
end
after do
FileUtils.rm_rf(project.repository.path_to_repo)
end
it 'has a source branch' do
expect(project.repository.branch_exists?(parsed_merge_request.source_branch)).to be true
end
it 'has a target branch' do
expect(project.repository.branch_exists?(parsed_merge_request.target_branch)).to be true
end
end
......@@ -82,6 +82,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9)
end
it 'has the correct time for merge request st_commits' do
st_commits = MergeRequestDiff.where.not(st_commits: nil).first.st_commits
expect(st_commits.first[:committed_date]).to be_kind_of(Time)
end
it 'has labels associated to label links, associated to issues' do
expect(Label.first.label_links.first.target).not_to be_nil
end
......
......@@ -79,6 +79,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty
end
it 'has merge requests diff st_diffs' do
expect(saved_project_json['merge_requests'].first['merge_request_diff']['utf8_st_diffs']).not_to be_nil
end
it 'has merge requests comments' do
expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty
end
......
......@@ -9,7 +9,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
relation_hash: relation_hash,
members_mapper: members_mapper,
user: user,
project_id: project.id)
project: project)
end
context 'hook object' do
......
......@@ -40,6 +40,15 @@ describe Gitlab::OAuth::User, lib: true do
let(:provider) { 'twitter' }
describe 'signup' do
it 'marks user as having password_automatically_set' do
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
oauth_user.save
expect(gl_user).to be_persisted
expect(gl_user).to be_password_automatically_set
end
shared_examples 'to verify compliance with allow_single_sign_on' do
context 'provider is marked as external' do
it 'marks user as external' do
......
......@@ -188,8 +188,10 @@ describe Gitlab::Workhorse, lib: true do
context 'when Gitaly is enabled' do
let(:gitaly_params) do
address = Gitlab::GitalyClient.get_address('default')
{
GitalySocketPath: URI(Gitlab::GitalyClient.get_address('default')).path,
GitalySocketPath: URI(address).path,
GitalyAddress: address,
}
end
......@@ -207,12 +209,9 @@ describe Gitlab::Workhorse, lib: true do
expect(subject).to include(repo_param)
end
{
git_receive_pack: :post_receive_pack,
git_upload_pack: :post_upload_pack
}.each do |action_name, feature_flag|
context "when #{action_name} action is passed" do
let(:action) { action_name }
context "when git_upload_pack action is passed" do
let(:action) { 'git_upload_pack' }
let(:feature_flag) { :post_upload_pack }
context 'when action is enabled by feature flag' do
it 'includes Gitaly params in the returned value' do
......@@ -230,6 +229,11 @@ describe Gitlab::Workhorse, lib: true do
end
end
end
context "when git_receive_pack action is passed" do
let(:action) { 'git_receive_pack' }
it { expect(subject).not_to include(gitaly_params) }
end
context "when info_refs action is passed" do
......
......@@ -53,6 +53,20 @@ describe Blob do
end
end
describe '#pdf?' do
it 'is falsey when file extension is not .pdf' do
git_blob = double(name: 'git_blob.txt')
expect(described_class.decorate(git_blob)).not_to be_pdf
end
it 'is truthy when file extension is .pdf' do
git_blob = double(name: 'git_blob.pdf')
expect(described_class.decorate(git_blob)).to be_pdf
end
end
describe '#ipython_notebook?' do
it 'is falsey when language is not Jupyter Notebook' do
git_blob = double(text?: true, language: double(name: 'JSON'))
......@@ -67,6 +81,20 @@ describe Blob do
end
end
describe '#sketch?' do
it 'is falsey with image extension' do
git_blob = Gitlab::Git::Blob.new(name: "design.png")
expect(described_class.decorate(git_blob)).not_to be_sketch
end
it 'is truthy with sketch extension' do
git_blob = Gitlab::Git::Blob.new(name: "design.sketch")
expect(described_class.decorate(git_blob)).to be_sketch
end
end
describe '#video?' do
it 'is falsey with image extension' do
git_blob = Gitlab::Git::Blob.new(name: 'image.png')
......@@ -88,11 +116,13 @@ describe Blob do
def stubbed_blob(overrides = {})
overrides.reverse_merge!(
name: nil,
image?: false,
language: nil,
lfs_pointer?: false,
svg?: false,
text?: false
text?: false,
binary?: false
)
described_class.decorate(double).tap do |blob|
......@@ -131,10 +161,20 @@ describe Blob do
expect(blob.to_partial_path(project)).to eq 'download'
end
it 'handles PDFs' do
blob = stubbed_blob(name: 'blob.pdf', pdf?: true)
expect(blob.to_partial_path(project)).to eq 'pdf'
end
it 'handles iPython notebooks' do
blob = stubbed_blob(text?: true, ipython_notebook?: true)
expect(blob.to_partial_path(project)).to eq 'notebook'
end
it 'handles Sketch files' do
blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
expect(blob.to_partial_path(project)).to eq 'sketch'
end
end
describe '#size_within_svg_limits?' do
......
......@@ -306,6 +306,8 @@ describe API::Milestones, api: true do
end
it 'returns project merge_requests for a particular milestone' do
# eager-load another_merge_request
another_merge_request
get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
expect(response).to have_http_status(200)
......
......@@ -461,6 +461,29 @@ describe API::Runner do
end
end
context 'when dependencies is an empty array' do
let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
stage: 'deploy', stage_idx: 1,
options: { dependencies: [] })
end
before do
job.success
job2.success
end
it 'returns an empty array' do
request_job
expect(response).to have_http_status(201)
expect(json_response['id']).to eq(empty_dependencies_job.id)
expect(json_response['dependencies'].count).to eq(0)
end
end
context 'when job has no tags' do
before { job.update(tags: []) }
......
......@@ -122,6 +122,32 @@ describe Users::CreateService, services: true do
end
end
context 'when password_automatically_set parameter is true' do
let(:params) do
{
name: 'John Doe',
username: 'jduser',
email: 'jd@example.com',
password: 'mydummypass',
password_automatically_set: true
}
end
it 'persists the given attributes' do
user = service.execute
user.reload
expect(user).to have_attributes(
name: params[:name],
username: params[:username],
email: params[:email],
password: params[:password],
created_by_id: admin_user.id,
password_automatically_set: params[:password_automatically_set]
)
end
end
context 'when skip_confirmation parameter is true' do
let(:params) do
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
......
......@@ -229,7 +229,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
end
context 'user has chosen a nested namespace and name for the project' do
context 'user has chosen an existing nested namespace and name for the project' do
let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
let(:test_name) { 'test_name' }
......@@ -242,5 +242,58 @@ shared_examples 'a GitHub-ish import controller: POST create' do
post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js }
end
end
context 'user has chosen a non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
and_return(double(execute: true))
post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js }
end
it 'creates the namespaces' do
allow(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
and_return(double(execute: true))
expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } }
.to change { Namespace.count }.by(2)
end
it 'new namespace has the right parent' do
allow(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
and_return(double(execute: true))
post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js }
expect(Namespace.find_by_path_or_name('bar').parent.path).to eq('foo')
end
end
context 'user has chosen existent and non-existent nested namespaces and name for the project' do
let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
it 'takes the selected namespace and name' do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
and_return(double(execute: true))
post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js }
end
it 'creates the namespaces' do
allow(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
and_return(double(execute: true))
expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } }
.to change { Namespace.count }.by(2)
end
end
end
end
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -1245,6 +1245,10 @@ core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
core-js@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65"
core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
......@@ -1581,6 +1585,10 @@ es6-map@^0.1.3:
es6-symbol "~3.1.0"
event-emitter "~0.3.4"
es6-promise@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6"
es6-promise@~4.0.3:
version "4.0.5"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.0.5.tgz#7882f30adde5b240ccfa7f7d78c548330951ae42"
......@@ -1934,6 +1942,12 @@ file-entry-cache@^2.0.0:
flat-cache "^1.2.1"
object-assign "^4.0.1"
file-loader@^0.11.1:
version "0.11.1"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-0.11.1.tgz#6b328ee1234a729e4e47d36375dd6d35c0e1db84"
dependencies:
loader-utils "^1.0.2"
filename-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.0.tgz#996e3e80479b98b9897f15a8a58b3d084e926775"
......@@ -2335,6 +2349,10 @@ ignore@^3.2.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410"
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
......@@ -2764,6 +2782,20 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.3.6"
jszip-utils@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/jszip-utils/-/jszip-utils-0.0.2.tgz#457d5cbca60a1c2e0706e9da2b544e8e7bc50bf8"
jszip@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.1.3.tgz#8a920403b2b1651c0fc126be90192d9080957c37"
dependencies:
core-js "~2.3.0"
es6-promise "~3.0.2"
lie "~3.1.0"
pako "~1.0.2"
readable-stream "~2.0.6"
karma-coverage-istanbul-reporter@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-0.2.0.tgz#5766263338adeb0026f7e4ac7a89a5f056c5642c"
......@@ -2868,6 +2900,12 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
lie@~3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
dependencies:
immediate "~3.0.5"
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
......@@ -2891,6 +2929,14 @@ loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
json5 "^0.5.0"
object-assign "^4.0.1"
loader-utils@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
dependencies:
big.js "^3.1.3"
emojis-list "^2.0.0"
json5 "^0.5.0"
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
......@@ -3349,6 +3395,10 @@ pako@~0.2.0:
version "0.2.9"
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
pako@~1.0.2:
version "1.0.5"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.5.tgz#d2205dfe5b9da8af797e7c163db4d1f84e4600bc"
parse-asn1@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.0.0.tgz#35060f6d5015d37628c770f4e091a0b5a278bc23"
......@@ -3661,7 +3711,7 @@ readable-stream@~1.0.2:
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@~2.0.0:
readable-stream@~2.0.0, readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
dependencies:
......
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