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. ...@@ -14,7 +14,7 @@ entry.
## 8.15.3 (2017-01-06) ## 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 - Rename projects wth reserved names. !8234
- Cache project authorizations even when user has access to zero projects. !8327 - Cache project authorizations even when user has access to zero projects. !8327
- Fix a minor grammar error in merge request widget. !8337 - Fix a minor grammar error in merge request widget. !8337
......
...@@ -111,7 +111,7 @@ gem 'gitlab-elasticsearch-git', '~> 1.0.1', require: "elasticsearch/git" ...@@ -111,7 +111,7 @@ gem 'gitlab-elasticsearch-git', '~> 1.0.1', require: "elasticsearch/git"
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' 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 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2' gem 'rdoc', '~> 4.2'
...@@ -231,8 +231,7 @@ gem 'chronic_duration', '~> 0.10.6' ...@@ -231,8 +231,7 @@ gem 'chronic_duration', '~> 0.10.6'
gem 'sass-rails', '~> 5.0.6' gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0' gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2' gem 'uglifier', '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0' gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0' gem 'bootstrap-sass', '~> 3.3.0'
......
...@@ -285,7 +285,9 @@ GEM ...@@ -285,7 +285,9 @@ GEM
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-license (1.0.0) 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) gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9) net-ldap (~> 0.9)
omniauth (~> 1.0) omniauth (~> 1.0)
...@@ -394,9 +396,6 @@ GEM ...@@ -394,9 +396,6 @@ GEM
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
jquery-turbolinks (2.1.0)
railties (>= 3.1.0)
turbolinks
jquery-ui-rails (5.0.5) jquery-ui-rails (5.0.5)
railties (>= 3.2.16) railties (>= 3.2.16)
json (1.8.3) json (1.8.3)
...@@ -806,8 +805,6 @@ GEM ...@@ -806,8 +805,6 @@ GEM
truncato (0.7.8) truncato (0.7.8)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1) nokogiri (~> 1.6.1)
turbolinks (2.5.3)
coffee-rails
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
u2f (0.2.1) u2f (0.2.1)
...@@ -923,7 +920,8 @@ DEPENDENCIES ...@@ -923,7 +920,8 @@ DEPENDENCIES
gitlab-elasticsearch-git (~> 1.0.1) gitlab-elasticsearch-git (~> 1.0.1)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) 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) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2) gollum-rugged_adapter (~> 0.4.2)
...@@ -943,7 +941,6 @@ DEPENDENCIES ...@@ -943,7 +941,6 @@ DEPENDENCIES
jira-ruby (~> 1.1.2) jira-ruby (~> 1.1.2)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0) jquery-rails (~> 4.1.0)
jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 5.0.0) jquery-ui-rails (~> 5.0.0)
json-schema (~> 2.6.2) json-schema (~> 2.6.2)
jwt jwt
...@@ -1007,7 +1004,7 @@ DEPENDENCIES ...@@ -1007,7 +1004,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0) rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rubocop (~> 0.43.0) rubocop (~> 0.46.0)
rubocop-rspec (~> 1.9.1) rubocop-rspec (~> 1.9.1)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
...@@ -1043,7 +1040,6 @@ DEPENDENCIES ...@@ -1043,7 +1040,6 @@ DEPENDENCIES
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0) timecop (~> 0.8.0)
truncato (~> 0.7.8) truncato (~> 0.7.8)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0) 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 @@ ...@@ -187,11 +187,6 @@
new TreeView(); new TreeView();
} }
break; break;
case 'projects:pipelines:index':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
case 'projects:pipelines:show': case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
...@@ -283,6 +278,10 @@ ...@@ -283,6 +278,10 @@
case 'projects:variables:index': case 'projects:variables:index':
new gl.ProjectVariables(); new gl.ProjectVariables();
break; break;
case 'ci:lints:create':
case 'ci:lints:show':
new gl.CILintEditor();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'admin': case 'admin':
......
...@@ -139,6 +139,21 @@ ...@@ -139,6 +139,21 @@
}, 200); }, 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); })(window);
}).call(this); }).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 @@ ...@@ -37,10 +37,6 @@
display: none; display: none;
} }
.project-avatar {
display: none;
}
.project-home-panel { .project-home-panel {
padding-left: 0 !important; padding-left: 0 !important;
......
...@@ -183,7 +183,9 @@ ...@@ -183,7 +183,9 @@
&.right-sidebar-expanded { &.right-sidebar-expanded {
.line-resolve-all-container { .line-resolve-all-container {
display: none; @media (min-width: $sidebar-breakpoint) {
display: none;
}
} }
} }
} }
......
...@@ -9,3 +9,13 @@ ...@@ -9,3 +9,13 @@
color: $lint-correct-color; color: $lint-correct-color;
} }
} }
.ci-linter {
.ci-editor {
height: 400px;
}
.ci-template pre {
white-space: pre-wrap;
}
}
...@@ -526,8 +526,9 @@ ul.notes { ...@@ -526,8 +526,9 @@ ul.notes {
} }
.line-resolve-all { .line-resolve-all {
vertical-align: middle;
display: inline-block; display: inline-block;
padding: 5px 10px; padding: 6px 10px;
background-color: $gray-light; background-color: $gray-light;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
...@@ -535,18 +536,14 @@ ul.notes { ...@@ -535,18 +536,14 @@ ul.notes {
&.has-next-btn { &.has-next-btn {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-right: 0;
} }
.line-resolve-btn { .line-resolve-btn {
vertical-align: middle;
margin-right: 5px; margin-right: 5px;
} }
} }
.line-resolve-text {
vertical-align: middle;
}
.line-resolve-btn { .line-resolve-btn {
display: inline-block; display: inline-block;
position: relative; position: relative;
......
.pipelines { .pipelines {
.realtime-loading {
font-size: 40px;
text-align: center;
}
.stage { .stage {
max-width: 90px; max-width: 90px;
width: 90px; width: 90px;
...@@ -24,6 +29,10 @@ ...@@ -24,6 +29,10 @@
min-width: 1200px; min-width: 1200px;
table-layout: fixed; table-layout: fixed;
.label {
margin-bottom: 3px;
}
.pipeline-id { .pipeline-id {
color: $black; color: $black;
} }
...@@ -177,6 +186,7 @@ ...@@ -177,6 +186,7 @@
.stage-cell { .stage-cell {
font-size: 0; font-size: 0;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg { > .stage-container > button > svg {
height: 22px; height: 22px;
width: 22px; width: 22px;
......
...@@ -587,11 +587,21 @@ pre.light-well { ...@@ -587,11 +587,21 @@ pre.light-well {
.project-full-name { .project-full-name {
@include str-truncated; @include str-truncated;
@media (max-width: $screen-xs-max) {
max-width: 50%;
}
} }
.controls { .controls {
line-height: $list-text-height; line-height: $list-text-height;
.badge {
@media (max-width: $screen-xs-max) {
display: none;
}
}
a:hover { a:hover {
text-decoration: none; text-decoration: none;
} }
...@@ -605,6 +615,12 @@ pre.light-well { ...@@ -605,6 +615,12 @@ pre.light-well {
top: 2px; top: 2px;
} }
} }
.description p {
@media (max-width: $screen-xs-max) {
max-width: 50%;
}
}
} }
.bottom { .bottom {
......
...@@ -7,11 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -7,11 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30) @pipelines = PipelinesFinder
@pipelines = @pipelines.includes(project: :namespace) .new(project)
.execute(scope: @scope)
.page(params[:page])
.per(30)
@running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count @running_or_pending_count = PipelinesFinder
@pipelines_count = PipelinesFinder.new(project).execute.count .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 end
def new def new
......
...@@ -142,7 +142,7 @@ module Ci ...@@ -142,7 +142,7 @@ module Ci
end end
def artifacts def artifacts
builds.latest.with_artifacts_not_expired builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end end
def project_id def project_id
...@@ -191,7 +191,11 @@ module Ci ...@@ -191,7 +191,11 @@ module Ci
end end
def manual_actions def manual_actions
builds.latest.manual_actions builds.latest.manual_actions.includes(project: [:namespace])
end
def stuck?
builds.pending.any?(&:stuck?)
end end
def retryable? def retryable?
...@@ -283,6 +287,10 @@ module Ci ...@@ -283,6 +287,10 @@ module Ci
end end
end end
def has_yaml_errors?
yaml_errors.present?
end
def environments def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq builds.where.not(environment: nil).success.pluck(:environment).uniq
end end
......
...@@ -87,7 +87,7 @@ class Environment < ActiveRecord::Base ...@@ -87,7 +87,7 @@ class Environment < ActiveRecord::Base
end end
def update_merge_request_metrics? def update_merge_request_metrics?
self.name == "production" (environment_type || name) == "production"
end end
def first_deployment_for(commit) def first_deployment_for(commit)
......
...@@ -26,6 +26,7 @@ class Label < ActiveRecord::Base ...@@ -26,6 +26,7 @@ class Label < ActiveRecord::Base
# Don't allow ',' for label titles # Don't allow ',' for label titles
validates :title, presence: true, format: { with: /\A[^,]+\z/ } validates :title, presence: true, format: { with: /\A[^,]+\z/ }
validates :title, uniqueness: { scope: [:group_id, :project_id] } validates :title, uniqueness: { scope: [:group_id, :project_id] }
validates :title, length: { maximum: 255 }
default_scope { order(title: :asc) } default_scope { order(title: :asc) }
......
...@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base ...@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline :success_pipeline
] ]
EXCLUDED_WATCHER_EVENTS = [
:success_pipeline
]
store :events, accessors: EMAIL_EVENTS, coder: JSON store :events, accessors: EMAIL_EVENTS, coder: JSON
before_create :set_events before_create :set_events
......
...@@ -127,7 +127,7 @@ class Project < ActiveRecord::Base ...@@ -127,7 +127,7 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy 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 :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members alias_method :members, :project_members
......
...@@ -74,7 +74,7 @@ class User < ActiveRecord::Base ...@@ -74,7 +74,7 @@ class User < ActiveRecord::Base
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project 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 :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id has_many :snippets, dependent: :destroy, foreign_key: :author_id
...@@ -466,7 +466,7 @@ class User < ActiveRecord::Base ...@@ -466,7 +466,7 @@ class User < ActiveRecord::Base
end end
def remove_project_authorizations(project_ids) def remove_project_authorizations(project_ids)
project_authorizations.where(id: project_ids).delete_all project_authorizations.where(project_id: project_ids).delete_all
end end
def set_authorized_projects_column 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 ...@@ -3,6 +3,10 @@ class CommitEntity < API::Entities::RepoCommit
expose :author, using: UserEntity expose :author, using: UserEntity
expose :author_gravatar_url do |commit|
GravatarService.new.execute(commit.author_email)
end
expose :commit_url do |commit| expose :commit_url do |commit|
namespace_project_tree_url( namespace_project_tree_url(
request.project.namespace, 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 ...@@ -2,14 +2,11 @@ module RequestAwareEntity
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
include Gitlab::Routing.url_helpers include Gitlab::Routing
include Gitlab::Allowable
end end
def request def request
@options.fetch(:request) options.fetch(:request)
end
def can?(object, action, subject)
Ability.allowed?(object, action, subject)
end end
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 ...@@ -620,7 +620,10 @@ class NotificationService
custom_action = build_custom_key(action, target) custom_action = build_custom_key(action, target)
recipients = target.participants(current_user) 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 = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project) recipients = reject_mention_users(recipients, project)
......
...@@ -35,7 +35,7 @@ module Users ...@@ -35,7 +35,7 @@ module Users
# rows not in the new list or with a different access level should be # rows not in the new list or with a different access level should be
# removed. # removed.
if !fresh[project_id] || fresh[project_id] != row.access_level if !fresh[project_id] || fresh[project_id] != row.access_level
array << row.id array << row.project_id
end end
end end
...@@ -100,7 +100,7 @@ module Users ...@@ -100,7 +100,7 @@ module Users
end end
def current_authorizations def current_authorizations
user.project_authorizations.select(:id, :project_id, :access_level) user.project_authorizations.select(:project_id, :access_level)
end end
def fresh_authorizations def fresh_authorizations
......
- page_title "CI Lint" - page_title "CI Lint"
- page_description "Validate your GitLab CI configuration file" - 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 %h2 Check your .gitlab-ci.yml
%hr
.row .ci-linter
= form_tag ci_lint_path, method: :post do .row
.form-group = form_tag ci_lint_path, method: :post do
= label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap') .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 .col-sm-12
= text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true) .pull-left.prepend-top-10
.col-sm-12 = 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 .row.prepend-top-20
.col-sm-12 .col-sm-12
.results .results.ci-template
= render partial: 'create' if defined?(@status) = render partial: 'create' if defined?(@status)
...@@ -21,5 +21,5 @@ ...@@ -21,5 +21,5 @@
= render 'shared/group_tips' = render 'shared/group_tips'
.form-actions .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' = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
.nav-text .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 .nav-controls
= form_tag(filter_branches_path, method: :get) do = form_tag(filter_branches_path, method: :get) do
......
...@@ -78,9 +78,9 @@ ...@@ -78,9 +78,9 @@
.btn-group.inline .btn-group.inline
- if actions.any? - if actions.any?
.btn-group .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') = custom_icon('icon_play')
= icon('caret-down') = icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build| - actions.each do |build|
%li %li
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
%span= build.name.humanize %span= build.name.humanize
- if artifacts.present? - if artifacts.present?
.btn-group .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("download")
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
......
...@@ -35,21 +35,34 @@ ...@@ -35,21 +35,34 @@
= link_to ci_lint_path, class: 'btn btn-default' do = link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint %span CI Lint
.content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
.content-list.pipelines
- if @pipelines.blank? - if @pipelines.blank?
%div %div
.nothing-here-block No pipelines to show .nothing-here-block No pipelines to show
- else - else
.table-holder .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
%table.table.ci-table.js-pipeline-table "icon_status_canceled" => custom_icon("icon_status_canceled"),
%thead "icon_status_running" => custom_icon("icon_status_running"),
%th.pipeline-status Status "icon_status_skipped" => custom_icon("icon_status_skipped"),
%th.pipeline-info Pipeline "icon_status_created" => custom_icon("icon_status_created"),
%th.pipeline-commit Commit "icon_status_pending" => custom_icon("icon_status_pending"),
%th.pipeline-stages Stages "icon_status_success" => custom_icon("icon_status_success"),
%th.pipeline-date "icon_status_failed" => custom_icon("icon_status_failed"),
%th.pipeline-actions.hidden-xs "icon_status_warning" => custom_icon("icon_status_warning"),
= render @pipelines, commit_sha: true, stage: true, allow_retry: true "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
"stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
= paginate @pipelines, theme: 'gitlab' "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 %i.fa.fa-paperclip
%span Choose File ... %span Choose File ...
&nbsp; &nbsp;
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
- if show_counter - if show_counter
.right .right
= issuables.size = issuables.size
.pull-right= number_with_delimiter(issuables.size)
- class_prefix = dom_class(issuables).pluralize - class_prefix = dom_class(issuables).pluralize
%ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } %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 ...@@ -114,6 +114,8 @@ module Gitlab
config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js" config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.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/*" config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets # 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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -989,7 +989,7 @@ ActiveRecord::Schema.define(version: 20161227192806) do ...@@ -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", ["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 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 "user_id"
t.integer "project_id" t.integer "project_id"
t.integer "access_level" t.integer "access_level"
...@@ -1511,4 +1511,4 @@ ActiveRecord::Schema.define(version: 20161227192806) do ...@@ -1511,4 +1511,4 @@ ActiveRecord::Schema.define(version: 20161227192806) do
add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users" add_foreign_key "u2f_registrations", "users"
end end
\ No newline at end of file
...@@ -338,8 +338,11 @@ LDAP server please double-check the LDAP `port` and `method` settings used by ...@@ -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 GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
`method: 'ssl'` and `port: 636`. `method: 'ssl'` and `port: 636`.
### Login with valid credentials rejected ### Troubleshooting
If there is an unexpected error while authenticating the user with the LDAP If a user account is blocked or unblocked due to the LDAP configuration, a
backend, the login is rejected and details about the error are logged to 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`. `production.log`.
...@@ -271,9 +271,9 @@ sudo usermod -aG redis git ...@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source ### Clone the Source
# Clone GitLab repository # 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 ### 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 ...@@ -50,7 +50,7 @@ exception of the staging and production stages, where only data deployed to
production are measured. production are measured.
Specifically, if your CI is not set up and you have not defined a `production` 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. 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 ...@@ -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. | | 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. | | 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. | | 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. | | 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: ...@@ -79,10 +79,13 @@ Here's a little explanation of how this works behind the scenes:
etc. etc.
To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all. 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 So, the Cycle Analytics dashboard won't present any data:
label present in the Issue Board or assigned a milestone or a project has no - For merge requests that do not close an issue.
`production` environment (for staging and production stages), the Cycle Analytics - For issues not labeled with a label present in the Issue Board.
dashboard won't present any data at all. - For issues not assigned a milestone.
- For staging and production stages, if the project has no `production` or `production/*`
environment.
## Example workflow ## Example workflow
......
...@@ -5,6 +5,9 @@ GitLab support is enabled on your GitLab instance. ...@@ -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) 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. 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) ![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 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: ...@@ -73,7 +73,7 @@ In all of the below cases, the notification will be sent to:
...with notification level "Participating" or higher ...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 - 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 - 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 ...@@ -14,7 +14,11 @@ module API
end end
# Retain 405 error rather than a 500 error for Grape 0.15.0+. # 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| rescue_from Grape::Exceptions::Base do |e|
error! e.message, e.status, e.headers error! e.message, e.status, e.headers
end end
......
module API module API
module Helpers module Helpers
include Gitlab::Utils include Gitlab::Utils
include Helpers::Pagination
SUDO_HEADER = "HTTP_SUDO" SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo SUDO_PARAM = :sudo
...@@ -85,12 +86,6 @@ module API ...@@ -85,12 +86,6 @@ module API
IssuesFinder.new(current_user, project_id: user_project.id).find(id) IssuesFinder.new(current_user, project_id: user_project.id).find(id)
end 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! def authenticate!
unauthorized! unless current_user unauthorized! unless current_user
end end
...@@ -368,38 +363,6 @@ module API ...@@ -368,38 +363,6 @@ module API
@sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
end 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 def secret_token
Gitlab::Shell.secret_token Gitlab::Shell.secret_token
end 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 ...@@ -105,7 +105,7 @@ module Ci
break break
elsif s.scan(/</) elsif s.scan(/</)
@out << '&lt;' @out << '&lt;'
elsif s.scan(/\n/) elsif s.scan(/\r?\n/)
@out << '<br>' @out << '<br>'
else else
@out << s.scan(/./m) @out << s.scan(/./m)
......
...@@ -8,6 +8,16 @@ module Ci ...@@ -8,6 +8,16 @@ module Ci
rack_response({ 'message' => '404 Not found' }.to_json, 404) rack_response({ 'message' => '404 Not found' }.to_json, 404)
end 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| rescue_from :all do |exception|
handle_api_exception(exception) handle_api_exception(exception)
end end
......
...@@ -5,8 +5,8 @@ class EmailTemplateInterceptor ...@@ -5,8 +5,8 @@ class EmailTemplateInterceptor
def self.delivering_email(message) def self.delivering_email(message)
# Remove HTML part if HTML emails are disabled. # Remove HTML part if HTML emails are disabled.
unless current_application_settings.html_emails_enabled unless current_application_settings.html_emails_enabled
message.part.delete_if do |part| message.parts.delete_if do |part|
part.content_type.try(:start_with?, 'text/html') part.content_type.start_with?('text/html')
end end
end end
end end
......
...@@ -38,21 +38,21 @@ module Gitlab ...@@ -38,21 +38,21 @@ module Gitlab
def allowed? def allowed?
if ldap_user if ldap_user
unless ldap_config.active_directory unless ldap_config.active_directory
user.activate if user.ldap_blocked? unblock_user(user, 'is available again') if user.ldap_blocked?
return true return true
end end
# Block user in GitLab if he/she was blocked in AD # 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) 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 false
else else
user.activate if user.ldap_blocked? unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
true true
end end
else else
# Block the user if they no longer exist in LDAP/AD # Block the user if they no longer exist in LDAP/AD
user.ldap_block block_user(user, 'does not exist anymore')
false false
end end
end end
...@@ -69,6 +69,24 @@ module Gitlab ...@@ -69,6 +69,24 @@ module Gitlab
@ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) @ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
end 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 def update_user
update_email update_email
update_ssh_keys if sync_ssh_keys? update_ssh_keys if sync_ssh_keys?
......
...@@ -70,8 +70,12 @@ module Gitlab ...@@ -70,8 +70,12 @@ module Gitlab
def tag_endpoint(trans, env) def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY] 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 end
private private
......
...@@ -5,13 +5,33 @@ describe Projects::PipelinesController do ...@@ -5,13 +5,33 @@ describe Projects::PipelinesController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project, :public) } let(:project) { create(:empty_project, :public) }
let(:pipeline) { create(:ci_pipeline, project: project) }
before do before do
sign_in(user) sign_in(user)
end 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 describe 'GET stages.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when accessing existing stage' do context 'when accessing existing stage' do
before do before do
create(:ci_build, pipeline: pipeline, stage: 'build') create(:ci_build, pipeline: pipeline, stage: 'build')
......
...@@ -31,6 +31,14 @@ FactoryGirl.define do ...@@ -31,6 +31,14 @@ FactoryGirl.define do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end end
end end
# Populates pipeline with errors
#
pipeline.config_processor if evaluator.config
end
trait :invalid do
config(rspec: nil)
end end
end end
end end
......
...@@ -8,6 +8,10 @@ FactoryGirl.define do ...@@ -8,6 +8,10 @@ FactoryGirl.define do
is_shared false is_shared false
active true active true
trait :online do
contacted_at Time.now
end
trait :shared do trait :shared do
is_shared true is_shared true
end end
......
require 'spec_helper' require 'spec_helper'
describe 'CI Lint' do describe 'CI Lint', js: true do
before do before do
login_as :user login_as :user
end end
...@@ -8,7 +8,10 @@ describe 'CI Lint' do ...@@ -8,7 +8,10 @@ describe 'CI Lint' do
describe 'YAML parsing' do describe 'YAML parsing' do
before do before do
visit ci_lint_path 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' click_on 'Validate'
end end
...@@ -40,7 +43,7 @@ describe 'CI Lint' do ...@@ -40,7 +43,7 @@ describe 'CI Lint' do
let(:yaml_content) { 'my yaml content' } let(:yaml_content) { 'my yaml content' }
it 'loads previous YAML content after validation' do 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 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 ...@@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do
expect(subject.convert("<")[:html]).to eq('&lt;') expect(subject.convert("<")[:html]).to eq('&lt;')
end 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 describe "incremental update" do
shared_examples 'stateable converter' do shared_examples 'stateable converter' do
let(:pass1) { subject.convert(pre_text) } let(:pass1) { subject.convert(pre_text) }
......
...@@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
it 'blocks user in GitLab' do it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed? access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end end
end end
...@@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
it 'blocks user in GitLab' do it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory')
access.allowed? access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end end
end end
...@@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do
end end
it 'does not unblock user in GitLab' do it 'does not unblock user in GitLab' do
expect(access).not_to receive(:unblock_user)
access.allowed? access.allowed?
expect(user).to be_blocked expect(user).to be_blocked
expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
end end
...@@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do
end end
it 'unblocks user in GitLab' do it 'unblocks user in GitLab' do
expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore')
access.allowed? access.allowed?
expect(user).not_to be_blocked
end end
end end
end end
...@@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
it 'blocks user in GitLab' do it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed? access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end end
end end
...@@ -99,14 +103,57 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -99,14 +103,57 @@ describe Gitlab::LDAP::Access, lib: true do
end end
it 'unblocks the user if it exists' do it 'unblocks the user if it exists' do
expect(access).to receive(:unblock_user).with(user, 'is available again')
access.allowed? access.allowed?
expect(user).not_to be_blocked
end end
end end
end 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 describe '#update_user' do
subject { access.update_user } subject { access.update_user }
let(:entry) do let(:entry) do
......
...@@ -888,6 +888,48 @@ describe Ci::Pipeline, models: true do ...@@ -888,6 +888,48 @@ describe Ci::Pipeline, models: true do
end end
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 describe 'notifications when pipeline success or failed' do
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -63,6 +63,23 @@ describe Environment, models: true do ...@@ -63,6 +63,23 @@ describe Environment, models: true do
end end
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 describe '#first_deployment_for' do
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) }
......
...@@ -31,12 +31,14 @@ describe Label, models: true do ...@@ -31,12 +31,14 @@ describe Label, models: true do
it 'validates title' do it 'validates title' do
is_expected.not_to allow_value('G,ITLAB').for(:title) is_expected.not_to allow_value('G,ITLAB').for(:title)
is_expected.not_to allow_value('').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('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('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("customer's request").for(:title)
is_expected.to allow_value('s' * 255).for(:title)
end end
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 ...@@ -45,4 +45,8 @@ describe CommitEntity do
subject subject
end end
it 'exposes gravatar url that belongs to author' do
expect(subject.fetch(:author_gravatar_url)).to match /gravatar/
end
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