Commit 86b1628f authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce_upstream' into 'master'

CE upstream

See merge request !1045
parents 87312502 7b6dd7b3
......@@ -14,7 +14,7 @@ entry.
## 8.15.3 (2017-01-06)
- Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !0 (8425)
- Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !8425
- Rename projects wth reserved names. !8234
- Cache project authorizations even when user has access to zero projects. !8327
- Fix a minor grammar error in merge request widget. !8337
......
......@@ -111,7 +111,7 @@ gem 'gitlab-elasticsearch-git', '~> 1.0.1', require: "elasticsearch/git"
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.0'
gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
......@@ -231,8 +231,7 @@ gem 'chronic_duration', '~> 0.10.6'
gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0'
gem 'jquery-turbolinks', '~> 2.1.0'
gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
......
......@@ -285,7 +285,9 @@ GEM
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab-license (1.0.0)
gitlab-markup (1.5.0)
gitlab-markup (1.5.1)
gitlab-turbolinks-classic (2.5.6)
coffee-rails
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
......@@ -394,9 +396,6 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jquery-turbolinks (2.1.0)
railties (>= 3.1.0)
turbolinks
jquery-ui-rails (5.0.5)
railties (>= 3.2.16)
json (1.8.3)
......@@ -806,8 +805,6 @@ GEM
truncato (0.7.8)
htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1)
turbolinks (2.5.3)
coffee-rails
tzinfo (1.2.2)
thread_safe (~> 0.1)
u2f (0.2.1)
......@@ -923,7 +920,8 @@ DEPENDENCIES
gitlab-elasticsearch-git (~> 1.0.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
gitlab-markup (~> 1.5.0)
gitlab-markup (~> 1.5.1)
gitlab-turbolinks-classic (~> 2.5, >= 2.5.6)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
......@@ -943,7 +941,6 @@ DEPENDENCIES
jira-ruby (~> 1.1.2)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 5.0.0)
json-schema (~> 2.6.2)
jwt
......@@ -1007,7 +1004,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
rubocop (~> 0.43.0)
rubocop (~> 0.46.0)
rubocop-rspec (~> 1.9.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
......@@ -1043,7 +1040,6 @@ DEPENDENCIES
thin (~> 1.7.0)
timecop (~> 0.8.0)
truncato (~> 0.7.8)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
......
(() => {
window.gl = window.gl || {};
class CILintEditor {
constructor() {
this.editor = window.ace.edit('ci-editor');
this.textarea = document.querySelector('#content');
this.editor.getSession().setMode('ace/mode/yaml');
this.editor.on('input', () => {
const content = this.editor.getSession().getValue();
this.textarea.value = content;
});
}
}
gl.CILintEditor = CILintEditor;
})();
......@@ -187,11 +187,6 @@
new TreeView();
}
break;
case 'projects:pipelines:index':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
......@@ -283,6 +278,10 @@
case 'projects:variables:index':
new gl.ProjectVariables();
break;
case 'ci:lints:create':
case 'ci:lints:show':
new gl.CILintEditor();
break;
}
switch (path.first()) {
case 'admin':
......
......@@ -139,6 +139,21 @@
}, 200);
};
/**
this will take in the `name` of the param you want to parse in the url
if the name does not exist this function will return `null`
otherwise it will return the value of the param key provided
*/
w.gl.utils.getParameterByName = (name) => {
const url = window.location.href;
name = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
})(window);
}).call(this);
/* global Vue, gl */
/* eslint-disable no-param-reassign, no-plusplus */
((gl) => {
const PAGINATION_UI_BUTTON_LIMIT = 4;
const UI_LIMIT = 6;
const SPREAD = '...';
const PREV = 'Prev';
const NEXT = 'Next';
const FIRST = '<< First';
const LAST = 'Last >>';
gl.VueGlPagination = Vue.extend({
props: {
/**
This function will take the information given by the pagination component
And make a new Turbolinks call
Here is an example `change` method:
change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
},
*/
change: {
type: Function,
required: true,
},
/**
pageInfo will come from the headers of the API call
in the `.then` clause of the VueResource API call
there should be a function that contructs the pageInfo for this component
This is an example:
const pageInfo = headers => ({
perPage: +headers['X-Per-Page'],
page: +headers['X-Page'],
total: +headers['X-Total'],
totalPages: +headers['X-Total-Pages'],
nextPage: +headers['X-Next-Page'],
previousPage: +headers['X-Prev-Page'],
});
*/
pageInfo: {
type: Object,
required: true,
},
},
methods: {
changePage(e) {
let apiScope = gl.utils.getParameterByName('scope');
if (!apiScope) apiScope = 'all';
const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo;
switch (text) {
case SPREAD:
break;
case LAST:
this.change(totalPages, apiScope);
break;
case NEXT:
this.change(nextPage, apiScope);
break;
case PREV:
this.change(previousPage, apiScope);
break;
case FIRST:
this.change(1, apiScope);
break;
default:
this.change(+text, apiScope);
break;
}
},
},
computed: {
prev() {
return this.pageInfo.previousPage;
},
next() {
return this.pageInfo.nextPage;
},
getItems() {
const total = this.pageInfo.totalPages;
const page = this.pageInfo.page;
const items = [];
if (page > 1) items.push({ title: FIRST });
if (page > 1) {
items.push({ title: PREV, prev: true });
} else {
items.push({ title: PREV, disabled: true, prev: true });
}
if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
for (let i = start; i <= end; i++) {
const isActive = i === page;
items.push({ title: i, active: isActive, page: true });
}
if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
items.push({ title: SPREAD, separator: true, page: true });
}
if (page === total) {
items.push({ title: NEXT, disabled: true, next: true });
} else if (total - page >= 1) {
items.push({ title: NEXT, next: true });
}
if (total - page >= 1) items.push({ title: LAST, last: true });
return items;
},
},
template: `
<div class="gl-pagination">
<ul class="pagination clearfix">
<li v-for='item in getItems'
:class='{
page: item.page,
prev: item.prev,
next: item.next,
separator: item.separator,
active: item.active,
disabled: item.disabled
}'
>
<a @click="changePage($event)">{{item.title}}</a>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, VueResource, gl */
/*= require vue_common_component/commit */
/*= require vue-resource
/*= require boards/vue_resource_interceptor */
/*= require ./status.js.es6 */
/*= require ./store.js.es6 */
/*= require ./pipeline_url.js.es6 */
/*= require ./stage.js.es6 */
/*= require ./stages.js.es6 */
/*= require ./pipeline_actions.js.es6 */
/*= require ./time_ago.js.es6 */
/*= require ./pipelines.js.es6 */
(() => {
const project = document.querySelector('.pipelines');
const entry = document.querySelector('.vue-pipelines-index');
const svgs = document.querySelector('.pipeline-svgs');
Vue.use(VueResource);
if (!entry) return null;
return new Vue({
el: entry,
data: {
scope: project.dataset.url,
store: new gl.PipelineStore(),
svgs: svgs.dataset,
},
components: {
'vue-pipelines': gl.VuePipelines,
},
template: `
<vue-pipelines
:scope='scope'
:store='store'
:svgs='svgs'
>
</vue-pipelines>
`,
});
})();
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelineActions = Vue.extend({
props: ['pipeline', 'svgs'],
computed: {
actions() {
return this.pipeline.details.manual_actions.length > 0;
},
artifacts() {
return this.pipeline.details.artifacts.length > 0;
},
},
methods: {
download(name) {
return `Download ${name} artifacts`;
},
},
template: `
<td class="pipeline-actions hidden-xs">
<div class="controls pull-right">
<div class="btn-group inline">
<div class="btn-group">
<a
v-if='actions'
class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual build"
alt="Manual Build"
>
<span v-html='svgs.iconPlay'></span>
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href='action.path'
title="Manual build"
>
<span v-html='svgs.iconPlay'></span>
<span title="Manual build">{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group">
<a
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
data-toggle="dropdown"
type="button"
>
<i class="fa fa-download"></i>
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href='artifact.path'
>
<i class="fa fa-download"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
</ul>
</div>
</div>
<div class="cancel-retry-btns inline">
<a
v-if='pipeline.flags.retryable'
class="btn has-tooltip"
title="Retry"
rel="nofollow"
data-method="post"
:href='pipeline.retry_path'
>
<i class="fa fa-repeat"></i>
</a>
<a
v-if='pipeline.flags.cancelable'
class="btn btn-remove has-tooltip"
title="Cancel"
rel="nofollow"
data-method="post"
:href='pipeline.cancel_path'
data-original-title="Cancel"
>
<i class="fa fa-remove"></i>
</a>
</div>
</div>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelineUrl = Vue.extend({
props: [
'pipeline',
],
computed: {
user() {
return !!this.pipeline.user;
},
},
template: `
<td>
<a :href='pipeline.path'>
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<a
v-if='user'
:href='pipeline.user.web_url'
>
<img
v-if='user'
class="avatar has-tooltip s20 "
:title='pipeline.user.name'
data-container="body"
:src='pipeline.user.avatar_url'
>
</a>
<span
v-if='!user'
class="api monospace"
>
API
</span>
<span
v-if='pipeline.flags.latest'
class="label label-success has-tooltip"
title="Latest pipeline for this branch"
data-original-title="Latest pipeline for this branch"
>
latest
</span>
<span
v-if='pipeline.flags.yaml_errors'
class="label label-danger has-tooltip"
:title='pipeline.yaml_errors'
:data-original-title='pipeline.yaml_errors'
>
yaml invalid
</span>
<span
v-if='pipeline.flags.stuck'
class="label label-warning"
>
stuck
</span>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, Turbolinks, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelines = Vue.extend({
components: {
runningPipeline: gl.VueRunningPipeline,
pipelineActions: gl.VuePipelineActions,
stages: gl.VueStages,
commit: gl.CommitComponent,
pipelineUrl: gl.VuePipelineUrl,
pipelineHead: gl.VuePipelineHead,
glPagination: gl.VueGlPagination,
statusScope: gl.VueStatusScope,
timeAgo: gl.VueTimeAgo,
},
data() {
return {
pipelines: [],
timeLoopInterval: '',
intervalId: '',
apiScope: 'all',
pageInfo: {},
pagenum: 1,
count: { all: 0, running_or_pending: 0 },
pageRequest: false,
};
},
props: ['scope', 'store', 'svgs'],
created() {
const pagenum = gl.utils.getParameterByName('p');
const scope = gl.utils.getParameterByName('scope');
if (pagenum) this.pagenum = pagenum;
if (scope) this.apiScope = scope;
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
},
methods: {
change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
},
author(pipeline) {
if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' };
if (pipeline.commit.author) return pipeline.commit.author;
return {
avatar_url: pipeline.commit.author_gravatar_url,
web_url: `mailto:${pipeline.commit.author_email}`,
username: pipeline.commit.author_name,
};
},
ref(pipeline) {
const { ref } = pipeline;
return { name: ref.name, tag: ref.tag, ref_url: ref.path };
},
commitTitle(pipeline) {
return pipeline.commit ? pipeline.commit.title : '';
},
commitSha(pipeline) {
return pipeline.commit ? pipeline.commit.short_id : '';
},
commitUrl(pipeline) {
return pipeline.commit ? pipeline.commit.commit_path : '';
},
match(string) {
return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
},
},
template: `
<div>
<div class="pipelines realtime-loading" v-if='pipelines.length < 1'>
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="table-holder" v-if='pipelines.length'>
<table class="table ci-table">
<thead>
<tr>
<th>Status</th>
<th>Pipeline</th>
<th>Commit</th>
<th>Stages</th>
<th></th>
<th class="hidden-xs"></th>
</tr>
</thead>
<tbody>
<tr class="commit" v-for='pipeline in pipelines'>
<status-scope
:pipeline='pipeline'
:match='match'
:svgs='svgs'
>
</status-scope>
<pipeline-url :pipeline='pipeline'></pipeline-url>
<td>
<commit
:commit-icon-svg='svgs.commitIconSvg'
:author='author(pipeline)'
:tag="pipeline.ref.tag"
:title='commitTitle(pipeline)'
:commit-ref='ref(pipeline)'
:short-sha='commitSha(pipeline)'
:commit-url='commitUrl(pipeline)'
>
</commit>
</td>
<stages
:pipeline='pipeline'
:svgs='svgs'
:match='match'
>
</stages>
<time-ago :pipeline='pipeline' :svgs='svgs'></time-ago>
<pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions>
</tr>
</tbody>
</table>
</div>
<div class="pipelines realtime-loading" v-if='pageRequest'>
<i class="fa fa-spinner fa-spin"></i>
</div>
<gl-pagination
v-if='pageInfo.total > pageInfo.perPage'
:pagenum='pagenum'
:change='change'
:count='count.all'
:pageInfo='pageInfo'
>
</gl-pagination>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStage = Vue.extend({
data() {
return {
request: false,
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: ['stage', 'svgs', 'match'],
methods: {
fetchBuilds() {
if (this.request) return this.clearBuilds();
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.request = true;
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
this.request = false;
return flash;
});
},
clearBuilds() {
this.builds = '';
this.request = false;
},
},
computed: {
buildsOrSpinner() {
return this.request ? this.builds : this.spinner;
},
dropdownClass() {
if (this.request) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
svg() {
const icon = this.stage.status.icon;
const stageIcon = icon.replace(/icon/i, 'stage_icon');
return this.svgs[this.match(stageIcon)];
},
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
},
template: `
<div>
<button
@click='fetchBuilds'
@blur='fetchBuilds'
:class="triggerButtonClass"
:title='stage.title'
data-placement="top"
data-toggle="dropdown"
type="button">
<span v-html="svg"></span>
<i class="fa fa-caret-down "></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up"></div>
<div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner"></div>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStages = Vue.extend({
components: {
'vue-stage': gl.VueStage,
},
props: ['pipeline', 'svgs', 'match'],
template: `
<td class="stage-cell">
<div
class="stage-container dropdown js-mini-pipeline-graph"
v-for='stage in pipeline.details.stages'
>
<vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage>
</div>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStatusScope = Vue.extend({
props: [
'pipeline', 'svgs', 'match',
],
computed: {
cssClasses() {
const cssObject = { 'ci-status': true };
cssObject[`ci-${this.pipeline.details.status.group}`] = true;
return cssObject;
},
svg() {
return this.svgs[this.match(this.pipeline.details.status.icon)];
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
},
template: `
<td class="commit-link">
<a
:class='cssClasses'
:href='detailsPath'
v-html='svg + pipeline.details.status.text'
>
</a>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global gl, Flash */
/* eslint-disable no-param-reassign, no-underscore-dangle */
/*= require vue_realtime_listener/index.js */
((gl) => {
const pageValues = headers => ({
perPage: +headers['X-Per-Page'],
page: +headers['X-Page'],
total: +headers['X-Total'],
totalPages: +headers['X-Total-Pages'],
nextPage: +headers['X-Next-Page'],
previousPage: +headers['X-Prev-Page'],
});
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
const updatePipelineNums = (count) => {
const { all } = count;
const running = count.running_or_pending;
document.querySelector('.js-totalbuilds-count').innerHTML = all;
document.querySelector('.js-running-count').innerHTML = running;
};
const goFetch = () =>
this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
updatePipelineNums(this.count);
this.pageRequest = false;
}, () => {
this.pageRequest = false;
return new Flash('Something went wrong on our end.');
});
goFetch();
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children
.filter(e => e.$options._componentTag === 'time-ago')
.forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals);
}
};
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueTimeAgo = Vue.extend({
data() {
return {
currentTime: new Date(),
};
},
props: ['pipeline', 'svgs'],
computed: {
timeAgo() {
return gl.utils.getTimeago();
},
localTimeFinished() {
return gl.utils.formatDate(this.pipeline.details.finished_at);
},
timeStopped() {
const changeTime = this.currentTime;
const options = {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
};
options.timeZoneName = 'short';
const finished = this.pipeline.details.finished_at;
if (!finished && changeTime) return false;
return ({ words: this.timeAgo.format(finished) });
},
duration() {
const { duration } = this.pipeline.details;
const date = new Date(duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
if (hh < 10) hh = `0${hh}`;
if (mm < 10) mm = `0${mm}`;
if (ss < 10) ss = `0${ss}`;
if (duration !== null) return `${hh}:${mm}:${ss}`;
return false;
},
},
methods: {
changeTime() {
this.currentTime = new Date();
},
},
template: `
<td>
<p class="duration" v-if='duration'>
<span v-html='svgs.iconTimer'></span>
{{duration}}
</p>
<p class="finished-at" v-if='timeStopped'>
<i class="fa fa-calendar"></i>
<time
data-toggle="tooltip"
data-placement="top"
data-container="body"
:data-original-title='localTimeFinished'
>
{{timeStopped.words}}
</time>
</p>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
const removeAll = () => {
removeIntervals();
window.removeEventListener('beforeunload', removeIntervals);
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
document.removeEventListener('page:fetch', removeAll);
};
window.addEventListener('beforeunload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
document.addEventListener('page:fetch', removeAll);
};
})(window.gl || (window.gl = {}));
......@@ -37,10 +37,6 @@
display: none;
}
.project-avatar {
display: none;
}
.project-home-panel {
padding-left: 0 !important;
......
......@@ -183,7 +183,9 @@
&.right-sidebar-expanded {
.line-resolve-all-container {
display: none;
@media (min-width: $sidebar-breakpoint) {
display: none;
}
}
}
}
......
......@@ -9,3 +9,13 @@
color: $lint-correct-color;
}
}
.ci-linter {
.ci-editor {
height: 400px;
}
.ci-template pre {
white-space: pre-wrap;
}
}
......@@ -526,8 +526,9 @@ ul.notes {
}
.line-resolve-all {
vertical-align: middle;
display: inline-block;
padding: 5px 10px;
padding: 6px 10px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
......@@ -535,18 +536,14 @@ ul.notes {
&.has-next-btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 0;
}
.line-resolve-btn {
vertical-align: middle;
margin-right: 5px;
}
}
.line-resolve-text {
vertical-align: middle;
}
.line-resolve-btn {
display: inline-block;
position: relative;
......
.pipelines {
.realtime-loading {
font-size: 40px;
text-align: center;
}
.stage {
max-width: 90px;
width: 90px;
......@@ -24,6 +29,10 @@
min-width: 1200px;
table-layout: fixed;
.label {
margin-bottom: 3px;
}
.pipeline-id {
color: $black;
}
......@@ -177,6 +186,7 @@
.stage-cell {
font-size: 0;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg {
height: 22px;
width: 22px;
......
......@@ -587,11 +587,21 @@ pre.light-well {
.project-full-name {
@include str-truncated;
@media (max-width: $screen-xs-max) {
max-width: 50%;
}
}
.controls {
line-height: $list-text-height;
.badge {
@media (max-width: $screen-xs-max) {
display: none;
}
}
a:hover {
text-decoration: none;
}
......@@ -605,6 +615,12 @@ pre.light-well {
top: 2px;
}
}
.description p {
@media (max-width: $screen-xs-max) {
max-width: 50%;
}
}
}
.bottom {
......
......@@ -7,11 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
@pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
@pipelines = @pipelines.includes(project: :namespace)
@pipelines = PipelinesFinder
.new(project)
.execute(scope: @scope)
.page(params[:page])
.per(30)
@running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
@pipelines_count = PipelinesFinder.new(project).execute.count
@running_or_pending_count = PipelinesFinder
.new(project).execute(scope: 'running').count
@pipelines_count = PipelinesFinder
.new(project).execute.count
respond_to do |format|
format.html
format.json do
render json: {
pipelines: PipelineSerializer
.new(project: @project, user: @current_user)
.with_pagination(request, response)
.represent(@pipelines),
count: {
all: @pipelines_count,
running_or_pending: @running_or_pending_count
}
}
end
end
end
def new
......
......@@ -142,7 +142,7 @@ module Ci
end
def artifacts
builds.latest.with_artifacts_not_expired
builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
def project_id
......@@ -191,7 +191,11 @@ module Ci
end
def manual_actions
builds.latest.manual_actions
builds.latest.manual_actions.includes(project: [:namespace])
end
def stuck?
builds.pending.any?(&:stuck?)
end
def retryable?
......@@ -283,6 +287,10 @@ module Ci
end
end
def has_yaml_errors?
yaml_errors.present?
end
def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
......
......@@ -87,7 +87,7 @@ class Environment < ActiveRecord::Base
end
def update_merge_request_metrics?
self.name == "production"
(environment_type || name) == "production"
end
def first_deployment_for(commit)
......
......@@ -26,6 +26,7 @@ class Label < ActiveRecord::Base
# Don't allow ',' for label titles
validates :title, presence: true, format: { with: /\A[^,]+\z/ }
validates :title, uniqueness: { scope: [:group_id, :project_id] }
validates :title, length: { maximum: 255 }
default_scope { order(title: :asc) }
......
......@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline
]
EXCLUDED_WATCHER_EVENTS = [
:success_pipeline
]
store :events, accessors: EMAIL_EVENTS, coder: JSON
before_create :set_events
......
......@@ -127,7 +127,7 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
has_many :project_authorizations, dependent: :destroy
has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members
......
......@@ -74,7 +74,7 @@ class User < ActiveRecord::Base
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
has_many :project_authorizations, dependent: :destroy
has_many :project_authorizations
has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id
......@@ -466,7 +466,7 @@ class User < ActiveRecord::Base
end
def remove_project_authorizations(project_ids)
project_authorizations.where(id: project_ids).delete_all
project_authorizations.where(project_id: project_ids).delete_all
end
def set_authorized_projects_column
......
class BuildActionEntity < Grape::Entity
include RequestAwareEntity
expose :name do |build|
build.name.humanize
end
expose :path do |build|
play_namespace_project_build_path(
build.project.namespace,
build.project,
build)
end
end
class BuildArtifactEntity < Grape::Entity
include RequestAwareEntity
expose :name do |build|
build.name
end
expose :path do |build|
download_namespace_project_build_artifacts_path(
build.project.namespace,
build.project,
build)
end
end
......@@ -3,6 +3,10 @@ class CommitEntity < API::Entities::RepoCommit
expose :author, using: UserEntity
expose :author_gravatar_url do |commit|
GravatarService.new.execute(commit.author_email)
end
expose :commit_url do |commit|
namespace_project_tree_url(
request.project.namespace,
......
class PipelineEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :user, using: UserEntity
expose :path do |pipeline|
namespace_project_pipeline_path(
pipeline.project.namespace,
pipeline.project,
pipeline)
end
expose :details do
expose :status do |pipeline, options|
StatusEntity.represent(
pipeline.detailed_status(request.user),
options)
end
expose :duration
expose :finished_at
expose :stages, using: StageEntity
expose :artifacts, using: BuildArtifactEntity
expose :manual_actions, using: BuildActionEntity
end
expose :flags do
expose :latest?, as: :latest
expose :triggered?, as: :triggered
expose :stuck?, as: :stuck
expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
end
expose :ref do
expose :name do |pipeline|
pipeline.ref
end
expose :path do |pipeline|
namespace_project_tree_path(
pipeline.project.namespace,
pipeline.project,
id: pipeline.ref)
end
expose :tag?, as: :tag
expose :branch?, as: :branch
end
expose :commit, using: CommitEntity
expose :yaml_errors, if: ->(pipeline, _) { pipeline.has_yaml_errors? }
expose :retry_path, if: proc { can_retry? } do |pipeline|
retry_namespace_project_pipeline_path(pipeline.project.namespace,
pipeline.project,
pipeline.id)
end
expose :cancel_path, if: proc { can_cancel? } do |pipeline|
cancel_namespace_project_pipeline_path(pipeline.project.namespace,
pipeline.project,
pipeline.id)
end
expose :created_at, :updated_at
private
alias_method :pipeline, :object
def can_retry?
pipeline.retryable? &&
can?(request.user, :update_pipeline, pipeline)
end
def can_cancel?
pipeline.cancelable? &&
can?(request.user, :update_pipeline, pipeline)
end
end
class PipelineSerializer < BaseSerializer
entity PipelineEntity
class InvalidResourceError < StandardError; end
include API::Helpers::Pagination
Struct.new('Pagination', :request, :response)
def represent(resource, opts = {})
if paginated?
raise InvalidResourceError unless resource.respond_to?(:page)
super(paginate(resource.includes(project: :namespace)), opts)
else
super(resource, opts)
end
end
def paginated?
defined?(@pagination)
end
def with_pagination(request, response)
tap { @pagination = Struct::Pagination.new(request, response) }
end
private
# Methods needed by `API::Helpers::Pagination`
#
def params
@pagination.request.query_parameters
end
def request
@pagination.request
end
def header(header, value)
@pagination.response.headers[header] = value
end
end
......@@ -2,14 +2,11 @@ module RequestAwareEntity
extend ActiveSupport::Concern
included do
include Gitlab::Routing.url_helpers
include Gitlab::Routing
include Gitlab::Allowable
end
def request
@options.fetch(:request)
end
def can?(object, action, subject)
Ability.allowed?(object, action, subject)
options.fetch(:request)
end
end
class StageEntity < Grape::Entity
include RequestAwareEntity
expose :name
expose :title do |stage|
"#{stage.name}: #{detailed_status.label}"
end
expose :detailed_status,
as: :status,
with: StatusEntity
expose :path do |stage|
namespace_project_pipeline_path(
stage.pipeline.project.namespace,
stage.pipeline.project,
stage.pipeline,
anchor: stage.name)
end
expose :dropdown_path do |stage|
stage_namespace_project_pipeline_path(
stage.pipeline.project.namespace,
stage.pipeline.project,
stage.pipeline,
stage: stage.name,
format: :json)
end
private
alias_method :stage, :object
def detailed_status
stage.detailed_status(request.user)
end
end
class StatusEntity < Grape::Entity
include RequestAwareEntity
expose :icon, :text, :label, :group
expose :has_details?, as: :has_details
expose :details_path
end
......@@ -620,7 +620,10 @@ class NotificationService
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
recipients = add_project_watchers(recipients, project)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
recipients = add_project_watchers(recipients, project)
end
recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project)
......
......@@ -35,7 +35,7 @@ module Users
# rows not in the new list or with a different access level should be
# removed.
if !fresh[project_id] || fresh[project_id] != row.access_level
array << row.id
array << row.project_id
end
end
......@@ -100,7 +100,7 @@ module Users
end
def current_authorizations
user.project_authorizations.select(:id, :project_id, :access_level)
user.project_authorizations.select(:project_id, :access_level)
end
def fresh_authorizations
......
- page_title "CI Lint"
- page_description "Validate your GitLab CI configuration file"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
%h2 Check your .gitlab-ci.yml
%hr
.row
= form_tag ci_lint_path, method: :post do
.form-group
= label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap')
.ci-linter
.row
= form_tag ci_lint_path, method: :post do
.form-group
.col-sm-12
.file-holder
.file-title.clearfix
Content of .gitlab-ci.yml
#ci-editor.ci-editor #{@content}
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
= text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true)
.col-sm-12
.pull-left.prepend-top-10
= submit_tag('Validate', class: 'btn btn-success submit-yml')
.pull-left.prepend-top-10
= submit_tag('Validate', class: 'btn btn-success submit-yml')
.row.prepend-top-20
.col-sm-12
.results
= render partial: 'create' if defined?(@status)
.row.prepend-top-20
.col-sm-12
.results.ci-template
= render partial: 'create' if defined?(@status)
......@@ -21,5 +21,5 @@
= render 'shared/group_tips'
.form-actions
= f.submit 'Create group', class: "btn btn-create", tabindex: 3
= f.submit 'Create group', class: "btn btn-create"
= link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
......@@ -5,7 +5,8 @@
%div{ class: container_class }
.top-area.adjust
.nav-text
Protected branches can be managed in project settings
Protected branches can be managed in
= link_to 'project settings', namespace_project_protected_branches_path(@project.namespace, @project)
.nav-controls
= form_tag(filter_branches_path, method: :get) do
......
......@@ -78,9 +78,9 @@
.btn-group.inline
- if actions.any?
.btn-group
%a.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
%button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
= icon('caret-down')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
......@@ -89,7 +89,7 @@
%span= build.name.humanize
- if artifacts.present?
.btn-group
%a.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
%button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
......
......@@ -35,21 +35,34 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
.content-list.pipelines
.content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
- if @pipelines.blank?
%div
.nothing-here-block No pipelines to show
- else
.table-holder
%table.table.ci-table.js-pipeline-table
%thead
%th.pipeline-status Status
%th.pipeline-info Pipeline
%th.pipeline-commit Commit
%th.pipeline-stages Stages
%th.pipeline-date
%th.pipeline-actions.hidden-xs
= render @pipelines, commit_sha: true, stage: true, allow_retry: true
= paginate @pipelines, theme: 'gitlab'
.pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
"icon_status_canceled" => custom_icon("icon_status_canceled"),
"icon_status_running" => custom_icon("icon_status_running"),
"icon_status_skipped" => custom_icon("icon_status_skipped"),
"icon_status_created" => custom_icon("icon_status_created"),
"icon_status_pending" => custom_icon("icon_status_pending"),
"icon_status_success" => custom_icon("icon_status_success"),
"icon_status_failed" => custom_icon("icon_status_failed"),
"icon_status_warning" => custom_icon("icon_status_warning"),
"stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
"stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
"stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
"stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
"stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
"stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
"stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
"stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
"icon_play" => custom_icon("icon_play"),
"icon_timer" => custom_icon("icon_timer"),
"icon_status_manual" => custom_icon("icon_status_manual"),
} }
.vue-pipelines-index
= page_specific_javascript_tag('vue_pagination/index.js')
= page_specific_javascript_tag('vue_pipelines_index/index.js')
%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button
%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button
%i.fa.fa-paperclip
%span Choose File ...
&nbsp;
......
......@@ -9,6 +9,7 @@
- if show_counter
.right
= issuables.size
.pull-right= number_with_delimiter(issuables.size)
- class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id }
......
---
title: Fix double spaced CI log
merge_request: 8349
author: Jared Deckard <jared.deckard@gmail.com>
---
title: Treat environments matching `production/*` as Production
merge_request: 8500
author:
---
title: Added number_with_delimiter to counter on milestone panels
merge_request:
author: Ryan Harris
---
title: Don't instrument 405 Grape calls
merge_request: 8445
author:
---
title: Convert project setting text into protected branch path link
merge_request: 8377
author: Ken Ding
---
title: Fixes buttons not being accessible via the keyboard when creating new group
merge_request: 8469
author:
---
title: Display project avatars on Admin Area and Projects pages for mobile views
merge_request:
author: Ryan Harris
---
title: Make play button on Pipelines page accessible via keyboard
merge_request:
author: Ryan Harris
---
title: Made download artifacts button accessible via keyboard by changing it from
an anchor tag to an actual button
merge_request:
author: Ryan Harris
---
title: 26504 Fix styling of MR jump to discussion button
merge_request:
author:
---
title: Change CI template linter textarea with Ace Editor
merge_request: 8452
author: Didem Acet
---
title: Log LDAP blocking/unblocking events to application log
merge_request: 8042
author: Markus Koller
---
title: Remove the project_authorizations.id column
merge_request:
author:
---
title: Make successful pipeline emails off for watchers
merge_request: 8176
author:
---
title: Speed up group milestone index by passing group_id to IssuesFinder
merge_request: 8363
author:
---
title: Update the gitlab-markup gem to the version 1.5.1
merge_request: 8509
author:
---
title: "Validate label's title length"
merge_request: 5767
author: Tomáš Kukrál
......@@ -114,6 +114,8 @@ module Gitlab
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
config.assets.precompile << "vue_pipelines_index/index.js"
config.assets.precompile << "vue_pagination/index.js"
config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveProjectAuthorizationsIdColumn < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
remove_column :project_authorizations, :id, :primary_key
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161227192806) do
ActiveRecord::Schema.define(version: 20170106172224) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -989,7 +989,7 @@ ActiveRecord::Schema.define(version: 20161227192806) do
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "project_authorizations", force: :cascade do |t|
create_table "project_authorizations", id: false, force: :cascade do |t|
t.integer "user_id"
t.integer "project_id"
t.integer "access_level"
......@@ -1511,4 +1511,4 @@ ActiveRecord::Schema.define(version: 20161227192806) do
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
\ No newline at end of file
end
......@@ -338,8 +338,11 @@ LDAP server please double-check the LDAP `port` and `method` settings used by
GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
`method: 'ssl'` and `port: 636`.
### Login with valid credentials rejected
### Troubleshooting
If there is an unexpected error while authenticating the user with the LDAP
backend, the login is rejected and details about the error are logged to
If a user account is blocked or unblocked due to the LDAP configuration, a
message will be logged to `application.log`.
If there is an unexpected error during an LDAP lookup (configuration error,
timeout), the login is rejected and a message will be logged to
`production.log`.
......@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-16-stable gitlab
**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
**Note:** You can change `8-16-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
......
# From 8.15 to 8.16
Make sure you view this update guide from the tag (version) of GitLab you would
like to install. In most cases this should be the highest numbered production
tag (without rc in it). You can select the tag in the version dropdown at the
top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear please check the
[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
guide links by version.
### 1. Stop server
```bash
sudo service gitlab stop
```
### 2. Backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
### 3. Update Ruby
We will continue supporting Ruby < 2.3 for the time being but we recommend you
upgrade to Ruby 2.3 if you're running a source installation, as this is the same
version that ships with our Omnibus package.
You can check which version you are running with `ruby -v`.
Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
cd ruby-2.3.3
./configure --disable-install-rdoc
make
sudo make install
```
Install Bundler:
```bash
sudo gem install bundler --no-ri --no-rdoc
```
### 4. Get latest code
```bash
cd /home/git/gitlab
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
For GitLab Community Edition:
```bash
cd /home/git/gitlab
sudo -u git -H git checkout 8-16-stable
```
OR
For GitLab Enterprise Edition:
```bash
cd /home/git/gitlab
sudo -u git -H git checkout 8-16-stable-ee
```
### 5. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
# MySQL installations (note: the line below states '--without postgres')
sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 6. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from
GitLab 8.1.
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
```
### 7. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v4.1.1
```
### 8. Update configuration files
#### New configuration options for `gitlab.yml`
There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
cd /home/git/gitlab
git diff origin/8-15-stable:config/gitlab.yml.example origin/8-16-stable:config/gitlab.yml.example
```
#### Git configuration
Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
the GitLab server during `git gc`.
```sh
cd /home/git/gitlab
sudo -u git -H git config --global repack.writeBitmaps true
```
#### Nginx configuration
Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh
cd /home/git/gitlab
# For HTTPS configurations
git diff origin/8-15-stable:lib/support/nginx/gitlab-ssl origin/8-16-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
git diff origin/8-15-stable:lib/support/nginx/gitlab origin/8-16-stable:lib/support/nginx/gitlab
```
If you are using Apache instead of NGINX please see the updated [Apache templates].
Also note that because Apache does not support upstreams behind Unix sockets you
will need to let gitlab-workhorse listen on a TCP port. You can do this
via [/etc/default/gitlab].
[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/lib/support/init.d/gitlab.default.example#L38
#### SMTP configuration
If you're installing from source and use SMTP to deliver mail, you will need to add the following line
to config/initializers/smtp_settings.rb:
```ruby
ActionMailer::Base.delivery_method = :smtp
```
See [smtp_settings.rb.sample] as an example.
[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
Ensure you're still up-to-date with the latest init script changes:
```bash
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
For Ubuntu 16.04.1 LTS:
```bash
sudo systemctl daemon-reload
```
### 9. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
### 10. Check application status
Check if GitLab and its environment are configured correctly:
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
To make sure you didn't miss anything run a more thorough check:
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
```
If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (8.15)
### 1. Revert the code to the previous version
Follow the [upgrade guide from 8.14 to 8.15](8.14-to-8.15.md), except for the
database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
......@@ -50,7 +50,7 @@ exception of the staging and production stages, where only data deployed to
production are measured.
Specifically, if your CI is not set up and you have not defined a `production`
[environment], then you will not have any data for those stages.
or `production/*` [environment], then you will not have any data for those stages.
Below you can see in more detail what the various stages of Cycle Analytics mean.
......@@ -61,7 +61,7 @@ Below you can see in more detail what the various stages of Cycle Analytics mean
| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. |
| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. |
| Review | Measures the median time taken to review the merge request, between its creation and until it's merged. |
| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a production environment, this is not tracked. |
| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. |
---
......@@ -79,10 +79,13 @@ Here's a little explanation of how this works behind the scenes:
etc.
To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all.
So, if a merge request doesn't close an issue or an issue is not labeled with a
label present in the Issue Board or assigned a milestone or a project has no
`production` environment (for staging and production stages), the Cycle Analytics
dashboard won't present any data at all.
So, the Cycle Analytics dashboard won't present any data:
- For merge requests that do not close an issue.
- For issues not labeled with a label present in the Issue Board.
- For issues not assigned a milestone.
- For staging and production stages, if the project has no `production` or `production/*`
environment.
## Example workflow
......
......@@ -5,6 +5,9 @@ GitLab support is enabled on your GitLab instance.
You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html)
To get to the importer page you need to go to "New project" page.
>**Note:**
If you are interested in importing Wiki and Merge Request data to your new instance, you'll need to follow the instructions for [project export](../../user/project/settings/import_export.md)
![New project page](gitlab_importer/new_project_page.png)
Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com
......
......@@ -73,7 +73,7 @@ In all of the below cases, the notification will be sent to:
...with notification level "Participating" or higher
- Watchers: users with notification level "Watch"
- Watchers: users with notification level "Watch" (however successful pipeline would be off for watchers)
- Subscribers: anyone who manually subscribed to the issue/merge request
- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
......
......@@ -14,7 +14,11 @@ module API
end
# Retain 405 error rather than a 500 error for Grape 0.15.0+.
# See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de
# https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
rescue_from Grape::Exceptions::MethodNotAllowed do |e|
error! e.message, e.status, e.headers
end
rescue_from Grape::Exceptions::Base do |e|
error! e.message, e.status, e.headers
end
......
module API
module Helpers
include Gitlab::Utils
include Helpers::Pagination
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
......@@ -85,12 +86,6 @@ module API
IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end
def paginate(relation)
relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
add_pagination_headers(data)
end
end
def authenticate!
unauthorized! unless current_user
end
......@@ -368,38 +363,6 @@ module API
@sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
end
def add_pagination_headers(paginated_data)
header 'X-Total', paginated_data.total_count.to_s
header 'X-Total-Pages', paginated_data.total_pages.to_s
header 'X-Per-Page', paginated_data.limit_value.to_s
header 'X-Page', paginated_data.current_page.to_s
header 'X-Next-Page', paginated_data.next_page.to_s
header 'X-Prev-Page', paginated_data.prev_page.to_s
header 'Link', pagination_links(paginated_data)
end
def pagination_links(paginated_data)
request_url = request.url.split('?').first
request_params = params.clone
request_params[:per_page] = paginated_data.limit_value
links = []
request_params[:page] = paginated_data.current_page - 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
request_params[:page] = paginated_data.current_page + 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
request_params[:page] = 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
request_params[:page] = paginated_data.total_pages
links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
links.join(', ')
end
def secret_token
Gitlab::Shell.secret_token
end
......
module API
module Helpers
module Pagination
def paginate(relation)
relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
add_pagination_headers(data)
end
end
private
def add_pagination_headers(paginated_data)
header 'X-Total', paginated_data.total_count.to_s
header 'X-Total-Pages', paginated_data.total_pages.to_s
header 'X-Per-Page', paginated_data.limit_value.to_s
header 'X-Page', paginated_data.current_page.to_s
header 'X-Next-Page', paginated_data.next_page.to_s
header 'X-Prev-Page', paginated_data.prev_page.to_s
header 'Link', pagination_links(paginated_data)
end
def pagination_links(paginated_data)
request_url = request.url.split('?').first
request_params = params.clone
request_params[:per_page] = paginated_data.limit_value
links = []
request_params[:page] = paginated_data.current_page - 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
request_params[:page] = paginated_data.current_page + 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
request_params[:page] = 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
request_params[:page] = paginated_data.total_pages
links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
links.join(', ')
end
end
end
end
......@@ -105,7 +105,7 @@ module Ci
break
elsif s.scan(/</)
@out << '&lt;'
elsif s.scan(/\n/)
elsif s.scan(/\r?\n/)
@out << '<br>'
else
@out << s.scan(/./m)
......
......@@ -8,6 +8,16 @@ module Ci
rack_response({ 'message' => '404 Not found' }.to_json, 404)
end
# Retain 405 error rather than a 500 error for Grape 0.15.0+.
# https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
rescue_from Grape::Exceptions::MethodNotAllowed do |e|
error! e.message, e.status, e.headers
end
rescue_from Grape::Exceptions::Base do |e|
error! e.message, e.status, e.headers
end
rescue_from :all do |exception|
handle_api_exception(exception)
end
......
......@@ -5,8 +5,8 @@ class EmailTemplateInterceptor
def self.delivering_email(message)
# Remove HTML part if HTML emails are disabled.
unless current_application_settings.html_emails_enabled
message.part.delete_if do |part|
part.content_type.try(:start_with?, 'text/html')
message.parts.delete_if do |part|
part.content_type.start_with?('text/html')
end
end
end
......
......@@ -38,21 +38,21 @@ module Gitlab
def allowed?
if ldap_user
unless ldap_config.active_directory
user.activate if user.ldap_blocked?
unblock_user(user, 'is available again') if user.ldap_blocked?
return true
end
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
user.ldap_block
block_user(user, 'is disabled in Active Directory')
false
else
user.activate if user.ldap_blocked?
unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
true
end
else
# Block the user if they no longer exist in LDAP/AD
user.ldap_block
block_user(user, 'does not exist anymore')
false
end
end
......@@ -69,6 +69,24 @@ module Gitlab
@ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
end
def block_user(user, reason)
user.ldap_block
Gitlab::AppLogger.info(
"LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
"blocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
def unblock_user(user, reason)
user.activate
Gitlab::AppLogger.info(
"LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
"unblocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
def update_user
update_email
update_ssh_keys if sync_ssh_keys?
......
......@@ -70,8 +70,12 @@ module Gitlab
def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY]
path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
trans.action = "Grape##{endpoint.route.request_method} #{path}"
# endpoint.route is nil in the case of a 405 response
if endpoint.route
path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
trans.action = "Grape##{endpoint.route.request_method} #{path}"
end
end
private
......
......@@ -5,13 +5,33 @@ describe Projects::PipelinesController do
let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) }
let(:pipeline) { create(:ci_pipeline, project: project) }
before do
sign_in(user)
end
describe 'GET index.json' do
before do
create_list(:ci_empty_pipeline, 2, project: project)
get :index, namespace_id: project.namespace.path,
project_id: project.path,
format: :json
end
it 'returns JSON with serialized pipelines' do
expect(response).to have_http_status(:ok)
expect(json_response).to include('pipelines')
expect(json_response['pipelines'].count).to eq 2
expect(json_response['count']['all']).to eq 2
expect(json_response['count']['running_or_pending']).to eq 2
end
end
describe 'GET stages.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when accessing existing stage' do
before do
create(:ci_build, pipeline: pipeline, stage: 'build')
......
......@@ -31,6 +31,14 @@ FactoryGirl.define do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
end
# Populates pipeline with errors
#
pipeline.config_processor if evaluator.config
end
trait :invalid do
config(rspec: nil)
end
end
end
......
......@@ -8,6 +8,10 @@ FactoryGirl.define do
is_shared false
active true
trait :online do
contacted_at Time.now
end
trait :shared do
is_shared true
end
......
require 'spec_helper'
describe 'CI Lint' do
describe 'CI Lint', js: true do
before do
login_as :user
end
......@@ -8,7 +8,10 @@ describe 'CI Lint' do
describe 'YAML parsing' do
before do
visit ci_lint_path
fill_in 'content', with: yaml_content
# Ace editor updates a hidden textarea and it happens asynchronously
# `sleep 0.1` is actually needed here because of this
execute_script("ace.edit('ci-editor').setValue(" + yaml_content.to_json + ");")
sleep 0.1
click_on 'Validate'
end
......@@ -40,7 +43,7 @@ describe 'CI Lint' do
let(:yaml_content) { 'my yaml content' }
it 'loads previous YAML content after validation' do
expect(page).to have_field('content', with: 'my yaml content', type: 'textarea')
expect(page).to have_field('content', with: 'my yaml content', visible: false, type: 'textarea')
end
end
end
......
//= require vue
//= require lib/utils/common_utils
//= require vue_pagination/index
/* global fixture, gl */
describe('Pagination component', () => {
let component;
const changeChanges = {
one: '',
two: '',
};
const change = (one, two) => {
changeChanges.one = one;
changeChanges.two = two;
};
it('should render and start at page 1', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 2,
previousPage: '',
},
change,
},
});
expect(component.$el.classList).toContain('gl-pagination');
component.changePage({ target: { innerText: '1' } });
expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all');
});
it('should go to the previous page', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 3,
previousPage: 1,
},
change,
},
});
component.changePage({ target: { innerText: 'Prev' } });
expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all');
});
it('should go to the next page', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 5,
previousPage: 3,
},
change,
},
});
component.changePage({ target: { innerText: 'Next' } });
expect(changeChanges.one).toEqual(5);
expect(changeChanges.two).toEqual('all');
});
it('should go to the last page', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 5,
previousPage: 3,
},
change,
},
});
component.changePage({ target: { innerText: 'Last >>' } });
expect(changeChanges.one).toEqual(10);
expect(changeChanges.two).toEqual('all');
});
it('should go to the first page', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 5,
previousPage: 3,
},
change,
},
});
component.changePage({ target: { innerText: '<< First' } });
expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all');
});
it('should do nothing', () => {
fixture.set('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
propsData: {
pageInfo: {
totalPages: 10,
nextPage: 2,
previousPage: '',
},
change,
},
});
component.changePage({ target: { innerText: '...' } });
expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all');
});
});
describe('paramHelper', () => {
it('can parse url parameters correctly', () => {
window.history.pushState({}, null, '?scope=all&p=2');
const scope = gl.utils.getParameterByName('scope');
const p = gl.utils.getParameterByName('p');
expect(scope).toEqual('all');
expect(p).toEqual('2');
});
it('returns null if param not in url', () => {
window.history.pushState({}, null, '?p=2');
const scope = gl.utils.getParameterByName('scope');
const p = gl.utils.getParameterByName('p');
expect(scope).toEqual(null);
expect(p).toEqual('2');
});
});
require 'spec_helper'
describe API::Helpers::Pagination do
let(:resource) { Project.all }
subject do
Class.new.include(described_class).new
end
describe '#paginate' do
let(:value) { spy('return value') }
before do
allow(value).to receive(:to_query).and_return(value)
allow(subject).to receive(:header).and_return(value)
allow(subject).to receive(:params).and_return(value)
allow(subject).to receive(:request).and_return(value)
end
describe 'required instance methods' do
let(:return_spy) { spy }
it 'requires some instance methods' do
expect_message(:header)
expect_message(:params)
expect_message(:request)
subject.paginate(resource)
end
end
context 'when resource can be paginated' do
before do
create_list(:empty_project, 3)
end
describe 'first page' do
before do
allow(subject).to receive(:params)
.and_return({ page: 1, per_page: 2 })
end
it 'returns appropriate amount of resources' do
expect(subject.paginate(resource).count).to eq 2
end
it 'adds appropriate headers' do
expect_header('X-Total', '3')
expect_header('X-Total-Pages', '2')
expect_header('X-Per-Page', '2')
expect_header('X-Page', '1')
expect_header('X-Next-Page', '2')
expect_header('X-Prev-Page', '')
expect_header('Link', any_args)
subject.paginate(resource)
end
end
describe 'second page' do
before do
allow(subject).to receive(:params)
.and_return({ page: 2, per_page: 2 })
end
it 'returns appropriate amount of resources' do
expect(subject.paginate(resource).count).to eq 1
end
it 'adds appropriate headers' do
expect_header('X-Total', '3')
expect_header('X-Total-Pages', '2')
expect_header('X-Per-Page', '2')
expect_header('X-Page', '2')
expect_header('X-Next-Page', '')
expect_header('X-Prev-Page', '1')
expect_header('Link', any_args)
subject.paginate(resource)
end
end
end
def expect_header(name, value)
expect(subject).to receive(:header).with(name, value)
end
def expect_message(method)
expect(subject).to receive(method)
.at_least(:once).and_return(value)
end
end
end
......@@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do
expect(subject.convert("<")[:html]).to eq('&lt;')
end
it "replaces newlines with line break tags" do
expect(subject.convert("\n")[:html]).to eq('<br>')
end
it "groups carriage returns with newlines" do
expect(subject.convert("\r\n")[:html]).to eq('<br>')
end
describe "incremental update" do
shared_examples 'stateable converter' do
let(:pass1) { subject.convert(pre_text) }
......
......@@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
end
......@@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory')
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
end
......@@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'does not unblock user in GitLab' do
expect(access).not_to receive(:unblock_user)
access.allowed?
expect(user).to be_blocked
expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
end
......@@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'unblocks user in GitLab' do
expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore')
access.allowed?
expect(user).not_to be_blocked
end
end
end
......@@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
end
......@@ -99,14 +103,57 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'unblocks the user if it exists' do
expect(access).to receive(:unblock_user).with(user, 'is available again')
access.allowed?
expect(user).not_to be_blocked
end
end
end
end
end
describe '#block_user' do
before do
user.activate
allow(Gitlab::AppLogger).to receive(:info)
access.block_user user, 'reason'
end
it 'blocks the user' do
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
it 'logs the reason' do
expect(Gitlab::AppLogger).to have_received(:info).with(
"LDAP account \"123456\" reason, " \
"blocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
end
describe '#unblock_user' do
before do
user.ldap_block
allow(Gitlab::AppLogger).to receive(:info)
access.unblock_user user, 'reason'
end
it 'activates the user' do
expect(user).not_to be_blocked
expect(user).not_to be_ldap_blocked
end
it 'logs the reason' do
Gitlab::AppLogger.info(
"LDAP account \"123456\" reason, " \
"unblocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
end
describe '#update_user' do
subject { access.update_user }
let(:entry) do
......
......@@ -888,6 +888,48 @@ describe Ci::Pipeline, models: true do
end
end
describe '#stuck?' do
before do
create(:ci_build, :pending, pipeline: pipeline)
end
context 'when pipeline is stuck' do
it 'is stuck' do
expect(pipeline).to be_stuck
end
end
context 'when pipeline is not stuck' do
before { create(:ci_runner, :shared, :online) }
it 'is not stuck' do
expect(pipeline).not_to be_stuck
end
end
end
describe '#has_yaml_errors?' do
context 'when pipeline has errors' do
let(:pipeline) do
create(:ci_pipeline, config: { rspec: nil })
end
it 'contains yaml errors' do
expect(pipeline).to have_yaml_errors
end
end
context 'when pipeline does not have errors' do
let(:pipeline) do
create(:ci_pipeline, config: { rspec: { script: 'rake test' } })
end
it 'does not containyaml errors' do
expect(pipeline).not_to have_yaml_errors
end
end
end
describe 'notifications when pipeline success or failed' do
let(:project) { create(:project) }
......
......@@ -63,6 +63,23 @@ describe Environment, models: true do
end
end
describe '#update_merge_request_metrics?' do
{ 'production' => true,
'production/eu' => true,
'production/www.gitlab.com' => true,
'productioneu' => false,
'Production' => false,
'Production/eu' => false,
'test-production' => false
}.each do |name, expected_value|
it "returns #{expected_value} for #{name}" do
env = create(:environment, name: name)
expect(env.update_merge_request_metrics?).to eq(expected_value)
end
end
end
describe '#first_deployment_for' do
let(:project) { create(:project) }
let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) }
......
......@@ -31,12 +31,14 @@ describe Label, models: true do
it 'validates title' do
is_expected.not_to allow_value('G,ITLAB').for(:title)
is_expected.not_to allow_value('').for(:title)
is_expected.not_to allow_value('s' * 256).for(:title)
is_expected.to allow_value('GITLAB').for(:title)
is_expected.to allow_value('gitlab').for(:title)
is_expected.to allow_value('G?ITLAB').for(:title)
is_expected.to allow_value('G&ITLAB').for(:title)
is_expected.to allow_value("customer's request").for(:title)
is_expected.to allow_value('s' * 255).for(:title)
end
end
......
require 'spec_helper'
describe BuildActionEntity do
let(:build) { create(:ci_build, name: 'test_build') }
let(:entity) do
described_class.new(build, request: double)
end
describe '#as_json' do
subject { entity.as_json }
it 'contains humanized build name' do
expect(subject[:name]).to eq 'Test build'
end
it 'contains path to the action play' do
expect(subject[:path]).to include "builds/#{build.id}/play"
end
end
end
require 'spec_helper'
describe BuildArtifactEntity do
let(:build) { create(:ci_build, name: 'test:build') }
let(:entity) do
described_class.new(build, request: double)
end
describe '#as_json' do
subject { entity.as_json }
it 'contains build name' do
expect(subject[:name]).to eq 'test:build'
end
it 'contains path to the artifacts' do
expect(subject[:path])
.to include "builds/#{build.id}/artifacts/download"
end
end
end
......@@ -45,4 +45,8 @@ describe CommitEntity do
subject
end
it 'exposes gravatar url that belongs to author' do
expect(subject.fetch(:author_gravatar_url)).to match /gravatar/
end
end
require 'spec_helper'
describe PipelineEntity do
let(:user) { create(:user) }
let(:request) { double('request') }
before do
allow(request).to receive(:user).and_return(user)
end
let(:entity) do
described_class.represent(pipeline, request: request)
end
describe '#as_json' do
subject { entity.as_json }
context 'when pipeline is empty' do
let(:pipeline) { create(:ci_empty_pipeline) }
it 'contains required fields' do
expect(subject).to include :id, :user, :path
expect(subject).to include :ref, :commit
expect(subject).to include :updated_at, :created_at
end
it 'contains details' do
expect(subject).to include :details
expect(subject[:details])
.to include :duration, :finished_at
expect(subject[:details])
.to include :stages, :artifacts, :manual_actions
expect(subject[:details][:status]).to include :icon, :text, :label
end
it 'contains flags' do
expect(subject).to include :flags
expect(subject[:flags])
.to include :latest, :triggered, :stuck,
:yaml_errors, :retryable, :cancelable
end
end
context 'when pipeline is retryable' do
let(:project) { create(:empty_project) }
let(:pipeline) do
create(:ci_pipeline, status: :success, project: project)
end
before do
create(:ci_build, :failed, pipeline: pipeline)
end
context 'user has ability to retry pipeline' do
before { project.team << [user, :developer] }
it 'retryable flag is true' do
expect(subject[:flags][:retryable]).to eq true
end
it 'contains retry path' do
expect(subject[:retry_path]).to be_present
end
end
context 'user does not have ability to retry pipeline' do
it 'retryable flag is false' do
expect(subject[:flags][:retryable]).to eq false
end
it 'does not contain retry path' do
expect(subject).not_to have_key(:retry_path)
end
end
end
context 'when pipeline is cancelable' do
let(:project) { create(:empty_project) }
let(:pipeline) do
create(:ci_pipeline, status: :running, project: project)
end
before do
create(:ci_build, :pending, pipeline: pipeline)
end
context 'user has ability to cancel pipeline' do
before { project.team << [user, :developer] }
it 'cancelable flag is true' do
expect(subject[:flags][:cancelable]).to eq true
end
it 'contains cancel path' do
expect(subject[:cancel_path]).to be_present
end
end
context 'user does not have ability to cancel pipeline' do
it 'cancelable flag is false' do
expect(subject[:flags][:cancelable]).to eq false
end
it 'does not contain cancel path' do
expect(subject).not_to have_key(:cancel_path)
end
end
end
context 'when pipeline has YAML errors' do
let(:pipeline) do
create(:ci_pipeline, config: { rspec: { invalid: :value } })
end
it 'contains flag that indicates there are errors' do
expect(subject[:flags][:yaml_errors]).to be true
end
it 'contains information about error' do
expect(subject[:yaml_errors]).to be_present
end
end
context 'when pipeline does not have YAML errors' do
let(:pipeline) { create(:ci_empty_pipeline) }
it 'contains flag that indicates there are no errors' do
expect(subject[:flags][:yaml_errors]).to be false
end
it 'does not contain field that normally holds an error' do
expect(subject).not_to have_key(:yaml_errors)
end
end
end
end
require 'spec_helper'
describe PipelineSerializer do
let(:user) { create(:user) }
let(:serializer) do
described_class.new(user: user)
end
let(:entity) do
serializer.represent(resource)
end
subject { entity.as_json }
describe '#represent' do
context 'when used without pagination' do
it 'created a not paginated serializer' do
expect(serializer).not_to be_paginated
end
context 'when a single object is being serialized' do
let(:resource) { create(:ci_empty_pipeline) }
it 'serializers the pipeline object' do
expect(subject[:id]).to eq resource.id
end
end
context 'when multiple objects are being serialized' do
let(:resource) { create_list(:ci_pipeline, 2) }
it 'serializers the array of pipelines' do
expect(subject).not_to be_empty
end
end
end
context 'when used with pagination' do
let(:request) { spy('request') }
let(:response) { spy('response') }
let(:pagination) { {} }
before do
allow(request)
.to receive(:query_parameters)
.and_return(pagination)
end
let(:serializer) do
described_class.new(user: user)
.with_pagination(request, response)
end
it 'created a paginated serializer' do
expect(serializer).to be_paginated
end
context 'when resource does is not paginatable' do
context 'when a single pipeline object is being serialized' do
let(:resource) { create(:ci_empty_pipeline) }
let(:pagination) { { page: 1, per_page: 1 } }
it 'raises error' do
expect { subject }
.to raise_error(PipelineSerializer::InvalidResourceError)
end
end
end
context 'when resource is paginatable relation' do
let(:resource) { Ci::Pipeline.all }
let(:pagination) { { page: 1, per_page: 2 } }
context 'when a single pipeline object is present in relation' do
before { create(:ci_empty_pipeline) }
it 'serializes pipeline relation' do
expect(subject.first).to have_key :id
end
end
context 'when a multiple pipeline objects are being serialized' do
before { create_list(:ci_empty_pipeline, 3) }
it 'serializes appropriate number of objects' do
expect(subject.count).to be 2
end
it 'appends relevant headers' do
expect(response).to receive(:[]=).with('X-Total', '3')
expect(response).to receive(:[]=).with('X-Total-Pages', '2')
expect(response).to receive(:[]=).with('X-Per-Page', '2')
subject
end
end
end
end
end
end
require 'spec_helper'
describe RequestAwareEntity do
subject do
Class.new.include(described_class).new
end
it 'includes URL helpers' do
expect(subject).to respond_to(:namespace_project_path)
end
it 'includes method for checking abilities' do
expect(subject).to respond_to(:can?)
end
it 'fetches request from options' do
expect(subject).to receive(:options)
.and_return({ request: 'some value' })
expect(subject.request).to eq 'some value'
end
end
require 'spec_helper'
describe StageEntity do
let(:pipeline) { create(:ci_pipeline) }
let(:request) { double('request') }
let(:user) { create(:user) }
let(:entity) do
described_class.new(stage, request: request)
end
let(:stage) do
build(:ci_stage, pipeline: pipeline, name: 'test')
end
before do
allow(request).to receive(:user).and_return(user)
create(:ci_build, :success, pipeline: pipeline)
end
describe '#as_json' do
subject { entity.as_json }
it 'contains relevant fields' do
expect(subject).to include :name, :status, :path
end
it 'contains detailed status' do
expect(subject[:status]).to include :text, :label, :group, :icon
expect(subject[:status][:label]).to eq 'passed'
end
it 'contains valid name' do
expect(subject[:name]).to eq 'test'
end
it 'contains path to the stage' do
expect(subject[:path])
.to include "pipelines/#{pipeline.id}##{stage.name}"
end
it 'contains path to the stage dropdown' do
expect(subject[:dropdown_path])
.to include "pipelines/#{pipeline.id}/stage.json?stage=test"
end
it 'contains stage title' do
expect(subject[:title]).to eq 'test: passed'
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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