Commit ec3a50f1 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into 8998_skip_pending_commits_if_not_head

* upstream/master: (64 commits)
  Merge branch 'open-redirect-fix-continue-to' into 'security'
  Merge branch 'open-redirect-host-fix' into 'security'
  Merge branch 'path-disclosure-proj-import-export' into 'security'
  Merge branch '29364-private-projects-mr-fix'
  Merge branch '30125-markdown-security'
  Issue title realtime
  Update CHANGELOG.md for 8.16.9
  Update CHANGELOG.md for 8.17.5
  Update CHANGELOG.md for 9.0.4
  Add "search" optional param and docs for V4
  Use PDFLab to render PDFs in GitLab
  Separate Scala from Java in CI examples
  Fix broken link
  Reorganize CI examples, add more links
  Refactor CI index page
  Remove deprecated field from workhorse response
  Use gitlab-workhorse 1.4.3
  Document how ETag caching middleware handles query parameters
  Make group skip validation in the frontend
  Use NamespaceValidator::WILDCARD_ROUTES in ETag caching middleware
  ...
parents f7014cd5 aaa49c2c
......@@ -2,6 +2,31 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.0.4 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 9.0.3 (2017-04-05)
- Fix name colision when importing GitHub pull requests from forked repositories. !9719
- Fix GitHub Importer for PRs of deleted forked repositories. !9992
- Fix environment folder route when special chars present in environment name. !10250
- Improve Markdown rendering when a lot of merge requests are referenced. !10252
- Allow users to import GitHub projects to subgroups.
- Backport API changes needed to fix sticking in EE.
- Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery. (mhasbini)
- Make CI build to use optimistic locking only on status change.
- Fix race condition where a namespace would be deleted before a project was deleted.
- Fix linking to new issue with selected template via url parameter.
- Remove unnecessary ORDER BY clause when updating todos. (mhasbini)
- API: Make the /notes endpoint work with noteable iid instead of id.
- Fixes method not replacing URL parameters correctly and breaking pipelines pagination.
- Move issue, mr, todos next to profile dropdown in top nav.
## 9.0.2 (2017-03-29)
- Correctly update paths when changing a child group.
......@@ -303,6 +328,14 @@ entry.
- Change development tanuki favicon colors to match logo color order.
- API issues - support filtering by iids.
## 8.17.5 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.17.4 (2017-03-19)
- Only show public emails in atom feeds.
......@@ -516,6 +549,14 @@ entry.
- Remove deprecated GitlabCiService.
- Requeue pending deletion projects.
## 8.16.9 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.16.8 (2017-03-19)
- Only show public emails in atom feeds.
......
......@@ -476,10 +476,10 @@ AwardsHandler.prototype.setupSearch = function setupSearch() {
this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
const term = $(e.target).val().trim();
// Clean previous search results
$('ul.emoji-menu-search, h5.emoji-search').remove();
$('ul.emoji-menu-search, h5.emoji-search-title').remove();
if (term.length > 0) {
// Generate a search result block
const h5 = $('<h5 class="emoji-search" />').text('Search results');
const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.searchEmojis(term).show();
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
......
/* eslint-disable no-new */
import Vue from 'vue';
import PDFLab from 'vendor/pdflab';
import workerSrc from 'vendor/pdf.worker';
Vue.use(PDFLab, {
workerSrc,
});
export default () => {
const el = document.getElementById('js-pdf-viewer');
new Vue({
el,
data() {
return {
error: false,
loadError: false,
loading: true,
pdf: el.dataset.endpoint,
};
},
methods: {
onLoad() {
this.loading = false;
},
onError(error) {
this.loading = false;
this.loadError = true;
this.error = error;
},
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="PDF loading">
</i>
</div>
<pdf-lab
v-if="!loadError"
:pdf="pdf"
@pdflabload="onLoad"
@pdflaberror="onError" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occured whilst loading the file. Please try again later.
</span>
<span v-else>
An error occured whilst decoding the file.
</span>
</p>
</div>
`,
});
};
import renderPDF from './pdf';
document.addEventListener('DOMContentLoaded', renderPDF);
import JSZip from 'jszip';
import JSZipUtils from 'jszip-utils';
export default class SketchLoader {
constructor(container) {
this.container = container;
this.loadingIcon = this.container.querySelector('.js-loading-icon');
this.load();
}
load() {
return this.getZipFile()
.then(data => JSZip.loadAsync(data))
.then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array'))
.then((content) => {
const url = window.URL || window.webkitURL;
const blob = new Blob([new Uint8Array(content)], {
type: 'image/png',
});
const previewUrl = url.createObjectURL(blob);
this.render(previewUrl);
})
.catch(this.error.bind(this));
}
getZipFile() {
return new JSZip.external.Promise((resolve, reject) => {
JSZipUtils.getBinaryContent(this.container.dataset.endpoint, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
render(previewUrl) {
const previewLink = document.createElement('a');
const previewImage = document.createElement('img');
previewLink.href = previewUrl;
previewLink.target = '_blank';
previewImage.src = previewUrl;
previewImage.className = 'img-responsive';
previewLink.appendChild(previewImage);
this.container.appendChild(previewLink);
this.removeLoadingIcon();
}
error() {
const errorMsg = document.createElement('p');
errorMsg.className = 'prepend-top-default append-bottom-default text-center';
errorMsg.textContent = `
Cannot show preview. For previews on sketch files, they must have the file format
introduced by Sketch version 43 and above.
`;
this.container.appendChild(errorMsg);
this.removeLoadingIcon();
}
removeLoadingIcon() {
if (this.loadingIcon) {
this.loadingIcon.remove();
}
}
}
/* eslint-disable no-new */
import SketchLoader from './sketch';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('js-sketch-viewer');
new SketchLoader(el);
});
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */
import Vue from 'vue';
import boardList from './board_list';
import boardBlankState from './board_blank_state';
require('./board_delete');
......@@ -16,7 +16,7 @@ require('./board_list');
gl.issueBoards.Board = Vue.extend({
template: '#js-board-template',
components: {
'board-list': gl.issueBoards.BoardList,
boardList,
'board-delete': gl.issueBoards.BoardDelete,
boardBlankState,
},
......
/* eslint-disable comma-dangle, space-before-function-paren, max-len */
/* global Sortable */
import Vue from 'vue';
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
import eventHub from '../eventhub';
(() => {
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
const Store = gl.issueBoards.BoardsStore;
gl.issueBoards.BoardList = Vue.extend({
template: '#js-board-list-template',
components: {
boardCard,
boardNewIssue,
},
export default {
name: 'BoardList',
props: {
disabled: Boolean,
list: Object,
issues: Array,
loading: Boolean,
issueLinkBase: String,
rootPath: String,
},
data () {
disabled: {
type: Boolean,
required: true,
},
list: {
type: Object,
required: true,
},
issues: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
},
data() {
return {
scrollOffset: 250,
filters: Store.state.filters,
showCount: false,
showIssueForm: false
showIssueForm: false,
};
},
watch: {
filters: {
handler () {
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
deep: true
},
issues () {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
this.list.page += 1;
this.list.getIssues(false);
}
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
});
}
components: {
boardCard,
boardNewIssue,
},
methods: {
listHeight () {
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight () {
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop () {
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
loadNextPage () {
loadNextPage() {
const getIssues = this.list.nextPage();
if (getIssues) {
......@@ -79,11 +68,40 @@ import boardCard from './board_card';
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
},
},
watch: {
filters: {
handler() {
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
deep: true,
},
issues() {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() &&
this.list.issuesSize > this.list.issues.length) {
this.list.page += 1;
this.list.getIssues(false);
}
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
});
},
},
created() {
gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
},
mounted () {
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues',
......@@ -100,7 +118,8 @@ import boardCard from './board_card';
gl.issueBoards.onStart();
},
onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
gl.issueBoards.BoardsStore
.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
this.$nextTick(() => {
e.item.remove();
......@@ -108,24 +127,71 @@ import boardCard from './board_card';
},
onUpdate: (e) => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
gl.issueBoards.BoardsStore
.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
},
onMove(e) {
return !e.related.classList.contains('board-list-count');
}
},
});
this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more
this.$refs.list.onscroll = () => {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
};
this.$refs.list.addEventListener('scroll', this.onScroll);
},
beforeDestroy() {
gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
},
});
})();
eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
this.$refs.list.removeEventListener('scroll', this.onScroll);
},
template: `
<div class="board-list-component">
<div
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
</div>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
ref="issue"
:index="index"
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
<li
class="board-list-count text-center"
v-if="showCount"
data-id="-1">
<i
class="fa fa-spinner fa-spin"
aria-label="Loading more issues"
aria-hidden="true"
v-show="list.loadingMore">
</i>
<span v-if="list.issues.length === list.issuesSize">
Showing all issues
</span>
<span v-else>
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
</span>
</li>
</ul>
</div>
`,
};
/* global ListIssue */
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
export default {
......@@ -49,7 +51,7 @@ export default {
},
cancel() {
this.title = '';
gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`);
eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
},
mounted() {
......
......@@ -24,6 +24,7 @@ export default Vue.component('environment-component', {
state: store.state,
visibility: 'available',
isLoading: false,
isLoadingFolderContent: false,
cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment,
......@@ -68,15 +69,21 @@ export default Vue.component('environment-component', {
this.fetchEnvironments();
eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder);
},
beforeDestroyed() {
eventHub.$off('refreshEnvironments');
eventHub.$off('toggleFolder');
},
methods: {
toggleRow(model) {
return this.store.toggleFolder(model.name);
toggleFolder(folder, folderUrl) {
this.store.toggleFolder(folder);
if (!folder.isOpen) {
this.fetchChildEnvironments(folder, folderUrl);
}
},
/**
......@@ -117,6 +124,21 @@ export default Vue.component('environment-component', {
new Flash('An error occurred while fetching the environments.');
});
},
fetchChildEnvironments(folder, folderUrl) {
this.isLoadingFolderContent = true;
this.service.getFolderContent(folderUrl)
.then(resp => resp.json())
.then((response) => {
this.store.setfolderContent(folder, response.environments);
this.isLoadingFolderContent = false;
})
.catch(() => {
this.isLoadingFolderContent = false;
new Flash('An error occurred while fetching the environments.');
});
},
},
template: `
......@@ -179,7 +201,8 @@ export default Vue.component('environment-component', {
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:service="service"/>
:service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
......@@ -45,11 +45,20 @@ export default {
new Flash('An error occured while making the request.');
});
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
},
template: `
<div class="btn-group" role="group">
<button
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body"
data-toggle="dropdown"
......@@ -58,15 +67,23 @@ export default {
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<i
class="fa fa-caret-down"
aria-hidden="true"/>
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"/>
</span>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
class="js-manual-action-link no-btn">
:class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg}
<span>
{{action.name}}
......
......@@ -7,6 +7,7 @@ import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
import MonitoringButtonComponent from './environment_monitoring';
import CommitComponent from '../../vue_shared/components/commit';
import eventHub from '../event_hub';
/**
* Envrionment Item Component
......@@ -141,6 +142,7 @@ export default {
const parsedAction = {
name: gl.text.humanize(action.name),
play_path: action.play_path,
playable: action.playable,
};
return parsedAction;
});
......@@ -410,7 +412,6 @@ export default {
folderUrl() {
return `${window.location.pathname}/folders/${this.model.folderName}`;
},
},
/**
......@@ -428,15 +429,37 @@ export default {
return true;
},
methods: {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model, this.folderUrl);
},
},
template: `
<tr>
<tr :class="{ 'js-child-row': model.isChildren }">
<td>
<a v-if="!model.isFolder"
class="environment-name"
:class="{ 'prepend-left-default': model.isChildren }"
:href="environmentPath">
{{model.name}}
</a>
<a v-else class="folder-name" :href="folderUrl">
<span v-else
class="folder-name"
@click="onClickFolder"
role="button">
<span class="folder-icon">
<i
v-show="model.isOpen"
class="fa fa-caret-down"
aria-hidden="true" />
<i
v-show="!model.isOpen"
class="fa fa-caret-right"
aria-hidden="true"/>
</span>
<span class="folder-icon">
<i class="fa fa-folder" aria-hidden="true"></i>
</span>
......@@ -448,7 +471,7 @@ export default {
<span class="badge">
{{model.size}}
</span>
</a>
</span>
</td>
<td class="deployment-column">
......
......@@ -31,6 +31,18 @@ export default {
type: Object,
required: true,
},
isLoadingFolderContent: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
folderUrl(model) {
return `${window.location.pathname}/folders/${model.folderName}`;
},
},
template: `
......@@ -53,6 +65,31 @@ export default {
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service"></tr>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent">
<td colspan="6" class="text-center">
<i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/>
</td>
</tr>
<template v-else>
<tr is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service"></tr>
<tr>
<td colspan="6" class="text-center">
<a :href="folderUrl(model)" class="btn btn-default">
Show all
</a>
</td>
</tr>
</template>
</template>
</template>
</tbody>
</table>
......
......@@ -7,6 +7,7 @@ Vue.use(VueResource);
export default class EnvironmentsService {
constructor(endpoint) {
this.environments = Vue.resource(endpoint);
this.folderResults = 3;
}
get(scope, page) {
......@@ -16,4 +17,8 @@ export default class EnvironmentsService {
postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true });
}
getFolderContent(folderUrl) {
return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`);
}
}
......@@ -38,7 +38,12 @@ export default class EnvironmentsStore {
let filtered = {};
if (env.size > 1) {
filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
filtered = Object.assign({}, env, {
isFolder: true,
folderName: env.name,
isOpen: false,
children: [],
});
}
if (env.latest) {
......@@ -85,4 +90,67 @@ export default class EnvironmentsStore {
this.state.stoppedCounter = count;
return count;
}
/**
* Toggles folder open property for the given folder.
*
* @param {Object} folder
* @return {Array}
*/
toggleFolder(folder) {
return this.updateFolder(folder, 'isOpen', !folder.isOpen);
}
/**
* Updates the folder with the received environments.
*
*
* @param {Object} folder Folder to update
* @param {Array} environments Received environments
* @return {Object}
*/
setfolderContent(folder, environments) {
const updatedEnvironments = environments.map((env) => {
let updated = env;
if (env.latest) {
updated = Object.assign({}, env, env.latest);
delete updated.latest;
} else {
updated = env;
}
updated.isChildren = true;
return updated;
});
return this.updateFolder(folder, 'children', updatedEnvironments);
}
/**
* Given a folder a prop and a new value updates the correct folder.
*
* @param {Object} folder
* @param {String} prop
* @param {String|Boolean|Object|Array} newValue
* @return {Array}
*/
updateFolder(folder, prop, newValue) {
const environments = this.state.environments;
const updatedEnvironments = environments.map((env) => {
const updateEnv = Object.assign({}, env);
if (env.isFolder && env.id === folder.id) {
updateEnv[prop] = newValue;
}
return updateEnv;
});
this.state.environments = updatedEnvironments;
return updatedEnvironments;
}
}
......@@ -45,14 +45,14 @@ window.GroupsSelect = (function() {
page,
per_page: GroupsSelect.PER_PAGE,
all_available,
skip_groups,
};
},
results: function (data, page) {
if (data.length) return { results: [] };
const results = data.length ? data : data.results || [];
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skip_groups.indexOf(group.id) === -1);
return {
results,
......
import Vue from 'vue';
import IssueTitle from './issue_title';
import '../vue_shared/vue_resource_interceptor';
const vueOptions = () => ({
el: '.issue-title-entrypoint',
components: {
IssueTitle,
},
data() {
const issueTitleData = document.querySelector('.issue-title-data').dataset;
return {
initialTitle: issueTitleData.initialTitle,
endpoint: issueTitleData.endpoint,
};
},
template: `
<IssueTitle
:initialTitle="initialTitle"
:endpoint="endpoint"
/>
`,
});
(() => new Vue(vueOptions()))();
import Visibility from 'visibilityjs';
import Poll from './../lib/utils/poll';
import Service from './services/index';
export default {
props: {
initialTitle: { required: true, type: String },
endpoint: { required: true, type: String },
},
data() {
const resource = new Service(this.$http, this.endpoint);
const poll = new Poll({
resource,
method: 'getTitle',
successCallback: (res) => {
this.renderResponse(res);
},
errorCallback: (err) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.error('ISSUE SHOW TITLE REALTIME ERROR', err);
} else {
throw new Error(err);
}
},
});
return {
poll,
timeoutId: null,
title: this.initialTitle,
};
},
methods: {
fetch() {
this.poll.makeRequest();
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
renderResponse(res) {
const body = JSON.parse(res.body);
this.triggerAnimation(body);
},
triggerAnimation(body) {
const { title } = body;
/**
* since opacity is changed, even if there is no diff for Vue to update
* we must check the title even on a 304 to ensure no visual change
*/
if (this.title === title) return;
this.$el.style.opacity = 0;
this.timeoutId = setTimeout(() => {
this.title = title;
this.$el.style.transition = 'opacity 0.2s ease';
this.$el.style.opacity = 1;
clearTimeout(this.timeoutId);
}, 100);
},
},
created() {
this.fetch();
},
template: `
<h2 class='title' v-html='title'></h2>
`,
};
export default class Service {
constructor(resource, endpoint) {
this.resource = resource;
this.endpoint = endpoint;
}
getTitle() {
return this.resource.get(this.endpoint);
}
}
......@@ -38,6 +38,14 @@ export default {
new Flash('An error occured while making the request.');
});
},
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
},
template: `
......@@ -51,16 +59,23 @@ export default {
aria-label="Manual job"
:disabled="isLoading">
${playIconSvg}
<i class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<i
class="fa fa-caret-down"
aria-hidden="true" />
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-pipeline-action-link no-btn"
@click="onClickAction(action.path)">
class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)"
:class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg}
<span>{{action.name}}</span>
</button>
......
/* eslint-disable no-underscore-dangle*/
import '../../vue_realtime_listener';
import VueRealtimeListener from '../../vue_realtime_listener';
export default class PipelinesStore {
constructor() {
......@@ -56,6 +56,6 @@ export default class PipelinesStore {
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals);
VueRealtimeListener(removeIntervals, startIntervals);
}
}
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
const removeAll = () => {
removeIntervals();
window.removeEventListener('beforeunload', removeIntervals);
export default (removeIntervals, startIntervals) => {
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
document.removeEventListener('beforeunload', removeAll);
};
window.removeEventListener('onbeforeload', removeIntervals);
window.addEventListener('beforeunload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
document.addEventListener('beforeunload', removeAll);
// add removeAll methods to stack
const stack = gl.VueRealtimeListener.reset;
gl.VueRealtimeListener.reset = () => {
gl.VueRealtimeListener.reset = stack;
removeAll();
stack();
};
};
// remove all event listeners and intervals
gl.VueRealtimeListener.reset = () => undefined; // noop
})(window.gl || (window.gl = {}));
window.addEventListener('onbeforeload', removeIntervals);
};
......@@ -292,6 +292,10 @@
}
@media(min-width: $screen-xs-max) {
&.merge-requests .text-content {
margin-top: 40px;
}
&.labels .text-content {
margin-top: 70px;
}
......
......@@ -459,20 +459,13 @@ a.deploy-project-label {
flex-wrap: wrap;
.btn {
margin: 0 10px 10px 0;
padding: 8px;
margin-left: 10px;
}
> div {
margin-bottom: 10px;
padding-left: 0;
&:last-child {
margin-bottom: 0;
.btn {
margin-right: 0;
}
}
}
}
}
......
......@@ -7,6 +7,7 @@ module ContinueParams
continue_params = continue_params.permit(:to, :notice, :notice_now)
return unless continue_params[:to] && continue_params[:to].start_with?('/')
return if continue_params[:to].start_with?('//')
continue_params
end
......
......@@ -7,7 +7,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
@sort = params[:sort]
@todos = @todos.page(params[:page])
if @todos.out_of_range? && @todos.total_pages != 0
redirect_to url_for(params.merge(page: @todos.total_pages))
redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true))
end
end
......
......@@ -10,6 +10,7 @@ class Groups::ApplicationController < ApplicationController
unless @group
id = params[:group_id] || params[:id]
@group = Group.find_by_full_path(id)
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
unless @group && can?(current_user, :read_group, @group)
@group = nil
......
class Import::BaseController < ApplicationController
private
def find_or_create_namespace(name, owner)
return current_user.namespace if name == owner
def find_or_create_namespace(names, owner)
return current_user.namespace if names == owner
return current_user.namespace unless current_user.can_create_group?
names = params[:target_namespace].presence || names
full_path_namespace = Namespace.find_by_full_path(names)
return full_path_namespace if full_path_namespace
names.split('/').inject(nil) do |parent, name|
begin
name = params[:target_namespace].presence || name
namespace = Group.create!(name: name, path: name, owner: current_user)
namespace = Group.create!(name: name,
path: name,
owner: current_user,
parent: parent)
namespace.add_owner(current_user)
namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
Namespace.find_by_full_path(name)
Namespace.where(parent: parent).find_by_path_or_name(name)
end
end
end
end
......@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
:related_branches, :can_create_branch]
:related_branches, :can_create_branch, :rendered_title]
# Allow read any issue
before_action :authorize_read_issue!, only: [:show]
before_action :authorize_read_issue!, only: [:show, :rendered_title]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
......@@ -31,7 +31,7 @@ class Projects::IssuesController < Projects::ApplicationController
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
end
if params[:label_name].present?
......@@ -200,6 +200,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
def rendered_title
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: { title: view_context.markdown_field(@issue, :title) }
end
protected
def issue
......
......@@ -43,7 +43,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true))
end
if params[:label_name].present?
......
......@@ -407,7 +407,10 @@ module ProjectsHelper
def sanitize_repo_path(project, message)
return '' unless message.present?
message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end
def project_feature_options
......
......@@ -46,10 +46,18 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG'
end
def pdf?
name && File.extname(name) == '.pdf'
end
def ipython_notebook?
text? && language&.name == 'Jupyter Notebook'
end
def sketch?
binary? && extname.downcase.delete('.') == 'sketch'
end
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
......@@ -67,8 +75,12 @@ class Blob < SimpleDelegator
end
elsif image? || svg?
'image'
elsif pdf?
'pdf'
elsif ipython_notebook?
'notebook'
elsif sketch?
'sketch'
elsif text?
'text'
else
......
......@@ -540,6 +540,8 @@ module Ci
end
def dependencies
return [] if empty_dependencies?
depended_jobs = depends_on_builds
return depended_jobs unless options[:dependencies].present?
......@@ -549,6 +551,10 @@ module Ci
end
end
def empty_dependencies?
options[:dependencies]&.empty?
end
private
def update_artifacts_size
......
......@@ -40,6 +40,8 @@ class Issue < ActiveRecord::Base
scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
after_save :expire_etag_cache
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
......@@ -59,10 +61,6 @@ class Issue < ActiveRecord::Base
before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now
end
before_transition closed: any do |issue|
issue.closed_at = nil
end
end
def hook_attrs
......@@ -256,4 +254,13 @@ class Issue < ActiveRecord::Base
def publicly_visible?
project.public? && !confidential?
end
def expire_etag_cache
key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path(
project.namespace,
project,
self
)
Gitlab::EtagCaching::Store.new.touch(key)
end
end
......@@ -150,7 +150,7 @@ class Namespace < ActiveRecord::Base
end
def any_project_has_container_registry_tags?
projects.any?(&:has_container_registry_tags?)
all_projects.any?(&:has_container_registry_tags?)
end
def send_update_instructions
......@@ -214,6 +214,12 @@ class Namespace < ActiveRecord::Base
@old_repository_storage_paths ||= repository_storage_paths
end
# Includes projects from this namespace and projects from all subgroups
# that belongs to this namespace
def all_projects
Project.inside_path(full_path)
end
private
def repository_storage_paths
......@@ -221,7 +227,7 @@ class Namespace < ActiveRecord::Base
# pending delete. Unscoping also get rids of the default order, which causes
# problems with SELECT DISTINCT.
Project.unscoped do
projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path)
all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path)
end
end
......
......@@ -114,6 +114,8 @@ class Project < ActiveRecord::Base
has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
has_one :prometheus_service, dependent: :destroy, inverse_of: :project
has_one :mock_ci_service, dependent: :destroy
has_one :mock_deployment_service, dependent: :destroy
has_one :mock_monitoring_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
......
class MockDeploymentService < DeploymentService
def title
'Mock deployment'
end
def description
'Mock deployment service'
end
def self.to_param
'mock_deployment'
end
# No terminals support
def terminals(environment)
[]
end
end
class MockMonitoringService < MonitoringService
def title
'Mock monitoring'
end
def description
'Mock monitoring service'
end
def self.to_param
'mock_monitoring'
end
def metrics(environment)
JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
end
end
......@@ -59,7 +59,7 @@ class Repository
def raw_repository
return nil unless path_with_namespace
@raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
@raw_repository ||= initialize_raw_repository
end
# Return absolute path to repository
......@@ -146,12 +146,7 @@ class Repository
# may cause the branch to "disappear" erroneously or have the wrong SHA.
#
# See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
raw_repo =
if fresh_repo
Gitlab::Git::Repository.new(path_to_repo)
else
raw_repository
end
raw_repo = fresh_repo ? initialize_raw_repository : raw_repository
raw_repo.find_branch(name)
end
......@@ -505,9 +500,7 @@ class Repository
end
end
def branch_names
branches.map(&:name)
end
delegate :branch_names, to: :raw_repository
cache_method :branch_names, fallback: []
delegate :tag_names, to: :raw_repository
......@@ -1168,4 +1161,8 @@ class Repository
def repository_storage_path
@project.repository_storage_path
end
def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git')
end
end
......@@ -238,7 +238,9 @@ class Service < ActiveRecord::Base
slack
teamcity
]
service_names << 'mock_ci' if Rails.env.development?
if Rails.env.development?
service_names += %w[mock_ci mock_deployment mock_monitoring]
end
service_names.sort_by(&:downcase)
end
......
......@@ -11,4 +11,6 @@ class BuildActionEntity < Grape::Entity
build.project,
build)
end
expose :playable?, as: :playable
end
......@@ -16,6 +16,7 @@ class BuildEntity < Grape::Entity
path_to(:play_namespace_project_build, build)
end
expose :playable?, as: :playable
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
......
......@@ -21,7 +21,9 @@ module MergeRequests
delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
def find_source_project
source_project || project
return source_project if source_project.present? && can?(current_user, :read_project, source_project)
project
end
def find_target_project
......
......@@ -62,6 +62,7 @@ module Users
:email,
:external,
:force_random_password,
:password_automatically_set,
:hide_no_password,
:hide_no_ssh_key,
:key_id,
......@@ -85,6 +86,7 @@ module Users
[
:email,
:email_confirmation,
:password_automatically_set,
:name,
:password,
:username
......
- page_title "Merge Requests"
.top-area
- if @group_merge_requests.empty?
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- if current_user
.nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request"
= render 'shared/issuable/filter', type: :merge_requests
= render 'shared/issuable/filter', type: :merge_requests
.row-content-block.second-block
.row-content-block.second-block
Only merge requests from
%strong= @group.name
group are listed here.
- if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
= render 'shared/merge_requests'
.prepend-top-default
= render 'shared/merge_requests'
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pdf_viewer')
.file-content#js-pdf-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sketch_viewer')
.file-content#js-sketch-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
.js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
= icon('spinner spin 2x', 'aria-hidden' => 'true');
......@@ -8,7 +8,6 @@
= page_specific_javascript_bundle_tag('boards')
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head"
......
.board-list-component
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ ":list" => "list",
"v-if" => 'list.type !== "closed" && showIssueForm' }
%ul.board-list{ "ref" => "list",
"v-show" => "!loading",
":data-board" => "list.id",
":class" => '{ "is-smaller": showIssueForm }' }
%board-card{ "v-for" => "(issue, index) in issues",
"ref" => "issue",
":index" => "index",
":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath",
":disabled" => "disabled",
":key" => "issue.id" }
%li.board-list-count.text-center{ "v-if" => "showCount",
"data-issue-id" => "-1" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
%span{ "v-else" => true }
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
......@@ -49,11 +49,12 @@
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
.issue-details.issuable-details
.detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
%h2.title
= markdown_field(@issue, :title)
.issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title),
"endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
} }
.issue-title-entrypoint
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki
......@@ -77,3 +78,5 @@
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue
= page_specific_javascript_bundle_tag('issue_show')
%ul.content-list.mr-list.issuable-list
- if @merge_requests.exists?
= render @merge_requests
- if @merge_requests.blank?
%li
.nothing-here-block No merge requests to show
- else
= render 'shared/empty_states/merge_requests'
- if @merge_requests.present?
= paginate @merge_requests, theme: "gitlab"
......@@ -7,16 +7,19 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('filtered_search')
%div{ class: container_class }
- if @project.merge_requests.exists?
%div{ class: container_class }
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
New Merge Request
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do
New merge request
= render 'shared/issuable/search_bar', type: :merge_requests
.merge-requests-holder
= render 'merge_requests'
- else
= render 'shared/empty_states/merge_requests', button_path: new_namespace_project_merge_request_path(@project.namespace, @project)
......@@ -78,7 +78,7 @@
- if git_import_enabled?
%button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL')
.import_gitlab_project
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
......@@ -109,6 +109,9 @@
%p Please wait a moment, this page will automatically refresh when ready.
:javascript
var importBtnTooltip = "Please enter a valid project name.";
var $importBtnWrapper = $('.import_gitlab_project');
$('.how_to_import_link').bind('click', function (e) {
e.preventDefault();
var import_modal = $(this).next(".modal").show();
......@@ -123,15 +126,8 @@
$(".btn_import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
});
$('.btn_import_gitlab_project').attr('disabled',true)
$('.import_gitlab_project').attr('title', 'Project path and name required.');
$('.import_gitlab_project').click(function( event ) {
if($('.btn_import_gitlab_project').attr('disabled')) {
event.preventDefault();
new Flash("Please enter path and name for the project to be imported to.");
}
});
$('.btn_import_gitlab_project').attr('disabled', $('#project_path').val().trim().length === 0);
$importBtnWrapper.attr('title', importBtnTooltip);
$('#new_project').submit(function(){
var $path = $('#project_path');
......@@ -139,13 +135,13 @@
});
$('#project_path').keyup(function(){
if($(this).val().length !=0) {
if($(this).val().trim().length !== 0) {
$('.btn_import_gitlab_project').attr('disabled', false);
$('.import_gitlab_project').attr('title','');
$(".flash-container").html("")
$importBtnWrapper.attr('title','');
$importBtnWrapper.removeClass('has-tooltip');
} else {
$('.btn_import_gitlab_project').attr('disabled',true);
$('.import_gitlab_project').attr('title', 'Project path and name required.');
$importBtnWrapper.addClass('has-tooltip');
}
});
......
- commit_message = @page.persisted? ? "Update #{@page.title}" : "Create #{@page.title}"
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page)
......@@ -28,7 +30,7 @@
.form-group
= f.label :commit_message, class: 'control-label'
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18, value: commit_message
.form-actions
- if @page && @page.persisted?
......
......@@ -6,4 +6,4 @@
= paginate @merge_requests, theme: "gitlab"
- else
.nothing-here-block No merge requests to show
= render 'shared/empty_states/merge_requests'
- button_path = local_assigns.fetch(:button_path, false)
- project_select_button = local_assigns.fetch(:project_select_button, false)
- has_button = button_path || project_select_button
.row.empty-state.merge-requests
.col-xs-12{ class: "#{'col-sm-6 pull-right' if has_button}" }
.svg-content
= render 'shared/empty_states/icons/merge_requests.svg'
.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.text-content
- if has_button
%h4
Merge requests are a place to propose changes you've made to a project and discuss those changes with others.
%p
Interested parties can even contribute by pushing commits if they want to.
- if project_select_button
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: 'New merge request'
- else
= link_to 'New merge request', button_path, class: 'btn btn-new', title: 'New merge request', id: 'new_merge_request_link'
- else
%h4.text-center
There are no merge requests to show.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="755 221 385 225" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="278" height="179" rx="10"/><mask id="d" width="278" height="179" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="e" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="f" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#F9F9F9" transform="translate(752 227)"><rect width="120" height="22" x="30" rx="11"/><rect width="132" height="22" y="44" rx="11"/><rect width="190" height="22" x="208" y="66" rx="11"/><rect width="158" height="22" x="129" y="197" rx="11"/><rect width="158" height="22" x="66" y="154" rx="11"/><rect width="350" height="22" x="31" y="110" rx="11"/><path d="M153 22H21h21.5c6 0 11 5 11 11s-5 11-11 11H21h132-36.5c-6 0-11-5-11-11s5-11 11-11H153zm252 66H288h36.5c6 0 11 5 11 11s-5 11-11 11H288h117-36.5c-6 0-11-5-11-11s5-11 11-11H405zm-244 44H44h36.5c6 0 11 5 11 11s-5 11-11 11H44h117-36.5c-6 0-11-5-11-11s5-11 11-11H161zm75 44H119h21.5c6 0 11 5 11 11s-5 11-11 11H119h117-51.5c-6 0-11-5-11-11s5-11 11-11H236z"/></g><g transform="translate(812 240)"><use fill="#FFF" stroke="#EEE" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#EEE" d="M4 29h271v4H4z"/><g transform="translate(34 60)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 93)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#FC6D26" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#FC6D26" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 126)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(157 59)"><rect width="6" height="2" y="1" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="23" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="34" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="33" fill="#EEE" rx="2"/><rect width="15" height="4" x="58" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="55" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="29" y="44" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" y="33" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="15" y="55" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" y="33" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="48" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="62" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="77" y="22" fill="#EEE" rx="2"/><rect width="6" height="2" y="45" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="56" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="67" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="66" fill="#6B4FBB" rx="2"/><rect width="15" height="4" x="39" y="88" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="77" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="88" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="77" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="34" y="66" fill="#EEE" rx="2"/><rect width="10" height="4" x="72" y="77" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="77" fill="#EEE" rx="2"/><rect width="6" height="2" y="78" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="89" fill="#FDE5D8" rx="1"/></g></g><g transform="translate(1057 221)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="8" mask="url(#e)" xlink:href="#b"/><rect width="29" height="3" x="14" y="14" fill="#FDB692" rx="1.5"/><rect width="39" height="3" x="14" y="23" fill="#FDB692" rx="1.5"/><rect width="29" height="3" x="14" y="32" fill="#FDB692" rx="1.5"/></g><g transform="translate(1046 285)"><circle cx="16" cy="15" r="15" fill="#FFF7F4" stroke="#FC6D26" stroke-width="3"/><path stroke="#FC6D26" stroke-width="2" d="M0 14h1c5 0 9.2-2.7 11.4-6.7M14 1V0"/><path stroke="#FC6D26" stroke-width="2" d="M7.8 3c3 4.3 7.8 7 13.2 7 3.3 0 6.3-1 9-2.7"/><circle cx="10.5" cy="17.5" r="1.5" fill="#FC6D26"/><circle cx="21.5" cy="17.5" r="1.5" fill="#FC6D26"/></g><g transform="translate(825 370)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M25 7h2.7C25 2.8 20.4 0 15 0 9.6 0 5 2.8 2.3 7H5l2.5-3L10 7l2.5-3L15 7l2.5-3L20 7l2.5-3L25 7z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g><g transform="matrix(-1 0 0 1 840 306)"><use fill="#FFF" stroke="#E2DCF2" stroke-width="8" mask="url(#f)" xlink:href="#c"/><rect width="29" height="3" x="24" y="14" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="23" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="32" fill="#6B4FBB" opacity=".5" rx="1.5"/></g></g></svg>
---
title: Added merge requests empty state
merge_request: 7342
author:
---
title: Fix linking to new issue with selected template via url parameter
merge_request:
author:
---
title: Backport API changes needed to fix sticking in EE
title: Add back expandable folder behavior
merge_request:
author:
---
title: Fix name colision when importing GitHub pull requests from forked repositories
merge_request: 9719
author:
---
title: Don’t show source project name when user does not have access
merge_request:
author:
---
title: Fix project title validation, prevent clicking on disabled button
merge_request: 9931
author:
---
title: Fix GitHub Importer for PRs of deleted forked repositories
merge_request: 9992
author:
---
title: 'API: Make the /notes endpoint work with noteable iid instead of id'
merge_request:
author:
---
title: Improve Markdown rendering when a lot of merge requests are referenced
merge_request: 10252
author:
---
title: Remove the class attribute from the whitelist for HTML generated from Markdown.
merge_request:
author:
---
title: Allow users to import GitHub projects to subgroups
title: Add search optional param and docs for V4
merge_request:
author:
---
title: Fixes method not replacing URL parameters correctly and breaking pipelines
pagination
merge_request:
author:
---
title: Move issue, mr, todos next to profile dropdown in top nav
merge_request:
author:
---
title: Added mock deployment and monitoring service with environments fixtures
merge_request:
author:
---
title: Fix subgroup repository disappearance if group was moved
merge_request: 10414
author:
---
title: Removed the duplicated search icon in the award emoji menu
merge_request:
author:
---
title: Fix path disclosure in project import/export
merge_request:
author:
---
title: Fix environment folder route when special chars present in environment name
merge_request: 10250
author:
---
title: Skip groups validation on the client
merge_request:
author:
---
title: Create subgroups if they don't exist while importing projects
merge_request:
author:
---
title: Fix wiki commit message
merge_request: 10464
author: blackst0ne
---
title: Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery
merge_request:
author: mhasbini
---
title: Incorporate Gitaly client for refs service
merge_request: 9291
author:
---
title: Do not set closed_at to nil when issue is reopened
merge_request:
author:
---
title: Make CI build to use optimistic locking only on status change
merge_request:
author:
---
title: Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
merge_request:
author:
---
title: Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
merge_request:
author:
---
title: Fix race condition where a namespace would be deleted before a project was
deleted
merge_request:
author:
---
title: Handle SSH keys that have multiple spaces between each marker
merge_request:
author:
---
title: Disable pipeline and environment actions that are not playable
merge_request: 10052
author:
---
title: Remove unnecessary ORDER BY clause when updating todos
merge_request:
author: mhasbini
......@@ -326,3 +326,21 @@
:why: https://github.com/domenic/opener/blob/1.4.3/LICENSE.txt
:versions: []
:when: 2017-02-21 22:33:41.729629000 Z
- - :approve
- jszip
- :who: Phil Hughes
:why: https://github.com/Stuk/jszip/blob/master/LICENSE.markdown
:versions: []
:when: 2017-04-05 10:38:46.275721000 Z
- - :approve
- jszip-utils
- :who: Phil Hughes
:why: https://github.com/Stuk/jszip-utils/blob/master/LICENSE.markdown
:versions: []
:when: 2017-04-05 10:39:32.676232000 Z
- - :approve
- pako
- :who: Phil Hughes
:why: https://github.com/nodeca/pako/blob/master/LICENSE
:versions: []
:when: 2017-04-05 10:43:45.897720000 Z
......@@ -2,17 +2,5 @@ require 'uri'
# Make sure we initialize our Gitaly channels before Sidekiq starts multi-threaded execution.
if Gitlab.config.gitaly.enabled || Rails.env.test?
Gitlab.config.repositories.storages.each do |name, params|
address = params['gitaly_address']
unless address.present?
raise "storage #{name.inspect} is missing a gitaly_address"
end
unless URI(address).scheme.in?(%w(tcp unix))
raise "Unsupported Gitaly address: #{address.inspect}"
end
Gitlab::GitalyClient.configure_channel(name, address)
end
Gitlab::GitalyClient.configure_channels
end
......@@ -250,6 +250,7 @@ constraints(ProjectUrlConstrainer.new) do
get :referenced_merge_requests
get :related_branches
get :can_create_branch
get :rendered_title
end
collection do
post :bulk_update
......
......@@ -37,6 +37,8 @@ var config = {
monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
sketch_viewer: './blob/sketch_viewer.js',
pdf_viewer: './blob/pdf_viewer.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js',
......@@ -44,6 +46,7 @@ var config = {
u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
vue_pipelines: './vue_pipelines_index/index.js',
issue_show: './issue_show/index.js',
},
output: {
......@@ -52,7 +55,7 @@ var config = {
filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js'
},
devtool: 'inline-source-map',
devtool: 'cheap-module-source-map',
module: {
rules: [
......@@ -64,7 +67,11 @@ var config = {
{
test: /\.svg$/,
use: 'raw-loader'
}
}, {
test: /\.(worker.js|pdf)$/,
exclude: /node_modules/,
loader: 'file-loader',
},
]
},
......@@ -106,6 +113,7 @@ var config = {
'issuable',
'merge_conflicts',
'notebook_viewer',
'pdf_viewer',
'vue_pipelines',
],
minChunks: function(module, count) {
......@@ -163,6 +171,7 @@ if (IS_PRODUCTION) {
}
if (IS_DEV_SERVER) {
config.devtool = 'cheap-module-eval-source-map';
config.devServer = {
port: DEV_SERVER_PORT,
headers: { 'Access-Control-Allow-Origin': '*' },
......
require './spec/support/sidekiq'
class Gitlab::Seeder::Environments
def initialize(project)
@project = project
end
def seed!
@project.create_mock_deployment_service!(active: true)
@project.create_mock_monitoring_service!(active: true)
create_master_deployments!('production')
create_master_deployments!('staging')
create_merge_request_review_deployments!
end
private
def create_master_deployments!(name)
@project.repository.commits('master', limit: 4).map do |commit|
create_deployment!(
@project,
name,
'master',
commit.id
)
end
end
def create_merge_request_review_deployments!
@project.merge_requests.sample(4).map do |merge_request|
next unless merge_request.diff_head_sha
create_deployment!(
merge_request.source_project,
"review/#{merge_request.source_branch}",
merge_request.source_branch,
merge_request.diff_head_sha
)
end
end
def create_deployment!(project, name, ref, sha)
environment = find_or_create_environment!(project, name)
environment.deployments.create!(
project: project,
ref: ref,
sha: sha,
tag: false,
deployable: find_deployable(project, name)
)
end
def find_or_create_environment!(project, name)
project.environments.find_or_create_by!(name: name).tap do |environment|
environment.update(external_url: "https://google.com/#{name}")
end
end
def find_deployable(project, environment)
project.builds.where(environment: environment).sample
end
end
Gitlab::Seeder.quiet do
Project.all.sample(5).each do |project|
project_environments = Gitlab::Seeder::Environments.new(project)
project_environments.seed!
end
end
......@@ -8,11 +8,10 @@ class MigrateRepoSize < ActiveRecord::Migration
project_data.each do |project|
id = project['id']
namespace_path = project['namespace_path'] || ''
repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default['path']
path = File.join(repos_path, namespace_path, project['project_path'] + '.git')
path = File.join(namespace_path, project['project_path'] + '.git')
begin
repo = Gitlab::Git::Repository.new(path)
repo = Gitlab::Git::Repository.new('default', path)
if repo.empty?
print '-'
else
......
This diff is collapsed.
# GitLab CI Documentation
# GitLab Continuous Integration (GitLab CI)
## CI User documentation
![CI/CD pipeline graph](img/cicd_pipeline_infograph.png)
The benefits of Continuous Integration are huge when automation plays an
integral part of your workflow. GitLab comes with built-in Continuous
Integration, Continuous Deployment, and Continuous Delivery support to build,
test, and deploy your application.
Here's some info we've gathered to get you started.
## Getting started
The first steps towards your GitLab CI journey.
- [Getting started with GitLab CI](quick_start/README.md)
- [CI examples for various languages](examples/README.md)
- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
- [Pipelines and jobs](pipelines.md)
- [Environments and deployments](environments.md)
- [Learn how `.gitlab-ci.yml` works](yaml/README.md)
- [Configure a Runner, the application that runs your jobs](runners/README.md)
- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
- [Use CI to build Docker images](docker/using_docker_build.md)
- **Articles:**
- [Getting started with GitLab and GitLab CI - Intro to CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
- [Continuous Integration, Delivery, and Deployment with GitLab - Intro to CI/CD](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
- [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
- [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
- [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
- **Videos:**
- [Demo (March, 2017): how to get started using CI/CD with GitLab](https://about.gitlab.com/2017/03/13/ci-cd-demo/)
- [Webcast (April, 2016): getting started with CI in GitLab](https://about.gitlab.com/2016/04/20/webcast-recording-and-slides-introduction-to-ci-in-gitlab/)
- **Third-party videos:**
- [Intégration continue avec GitLab (September, 2016)](https://www.youtube.com/watch?v=URcMBXjIr24&t=13s)
- [GitLab CI for Minecraft Plugins (July, 2016)](https://www.youtube.com/watch?v=Z4pcI9F8yf8)
## Reference guides
Once you get familiar with the getting started guides, you'll find yourself
digging into specific reference guides.
- [`.gitlab-ci.yml` reference](yaml/README.md) - Learn all about the ins and
outs of `.gitlab-ci.yml` definitions
- [CI Variables](variables/README.md) - Learn how to use variables defined in
your `.gitlab-ci.yml` or secured ones defined in your project's settings
- [Use SSH keys in your build environment](ssh_keys/README.md)
- [Trigger jobs through the API](triggers/README.md)
- **The permissions model** - Learn about the access levels a user can have for
performing certain CI actions
- [User permissions](../user/permissions.md#gitlab-ci)
- [Jobs permissions](../user/permissions.md#jobs-permissions)
## GitLab CI + Docker
Leverage the power of Docker to run your CI pipelines.
- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
- [Use CI to build Docker images](docker/using_docker_build.md)
- [CI services (linked Docker containers)](services/README.md)
- **Articles:**
- [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
## Advanced use
Once you get familiar with the basics of GitLab CI, it's time to dive in and
learn how to leverage its potential even more.
- [Environments and deployments](environments.md) - Separate your jobs into
environments and use them for different purposes like testing, building and
deploying
- [Job artifacts](../user/project/pipelines/job_artifacts.md)
- [User permissions](../user/permissions.md#gitlab-ci)
- [Jobs permissions](../user/permissions.md#jobs-permissions)
- [API](../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
- [Review Apps](review_apps/index.md)
- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs
- [Git submodules](git_submodules.md) - How to run your CI jobs when Git
submodules are involved
- [Auto deploy](autodeploy/index.md)
- [Use SSH keys in your build environment](ssh_keys/README.md)
- [Trigger jobs through the GitLab API](triggers/README.md)
## Review Apps
- [Review Apps](review_apps/index.md)
- **Articles:**
- [Introducing Review Apps](https://about.gitlab.com/2016/11/22/introducing-review-apps/)
- [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
## GitLab CI for GitLab Pages
See the topic on [GitLab Pages](../user/project/pages/index.md).
## Special configuration
You can change the default behavior of GitLab CI in your whole GitLab instance
as well as in each project.
- **Project specific**
- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
- **Affecting the whole GitLab instance**
- [Continuous Integration admin settings](../user/admin_area/settings/continuous_integration.md)
## Examples
>**Note:**
A collection of `.gitlab-ci.yml` files is maintained at the
[GitLab CI Yml project][gitlab-ci-templates].
If your favorite programming language or framework is missing we would love
your help by sending a merge request with a `.gitlab-ci.yml`.
Here is an collection of tutorials and guides on setting up your CI pipeline.
- [GitLab CI examples](examples/README.md) for the following languages and frameworks:
- [PHP](examples/php.md)
- [Ruby](examples/test-and-deploy-ruby-application-to-heroku.md)
- [Python](examples/test-and-deploy-python-application-to-heroku.md)
- [Clojure](examples/test-clojure-application.md)
- [Scala](examples/test-scala-application.md)
- [Phoenix](examples/test-phoenix-application.md)
- [Run PHP Composer & NPM scripts then deploy them to a staging server](examples/deployment/composer-npm-deploy.md)
- **Blog posts**
- [Automated Debian packaging](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
- [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
- [Setting up CI for iOS projects](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
- [Using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
- [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
- [CI/CD with GitLab in action](https://about.gitlab.com/2017/03/13/ci-cd-demo/)
- [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
- **Miscellaneous**
- [Using `dpl` as deployment tool](examples/deployment/README.md)
- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
- [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
## Integrations
- **Articles:**
- [Continuous Delivery with GitLab and Convox](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
- [Getting Started with GitLab and Shippable Continuous Integration](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
- [GitLab Partners with DigitalOcean to make Continuous Integration faster, safer, and more affordable](https://about.gitlab.com/2016/04/19/gitlab-partners-with-digitalocean-to-make-continuous-integration-faster-safer-and-more-affordable/)
## Why GitLab CI?
- **Articles:**
- [Why We Chose GitLab CI for our CI/CD Solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/)
- [Building our web-app on GitLab CI: 5 reasons why Captain Train migrated from Jenkins to GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/)
## Breaking changes
- [CI variables renaming](variables/README.md#9-0-renaming) Read about the
- [CI variables renaming for GitLab 9.0](variables/README.md#9-0-renaming) Read about the
deprecated CI variables and what you should use for GitLab 9.0+.
- [New CI job permissions model](../user/project/new_ci_build_permissions_model.md)
Read about what changed in GitLab 8.12 and how that affects your jobs.
There's a new way to access your Git submodules and LFS objects in jobs.
[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
# CI Examples
# GitLab CI Examples
A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
If your favorite programming language or framework are missing we would love your help by sending a merge request
......@@ -6,22 +6,73 @@ with a `.gitlab-ci.yml`.
Apart from those, here is an collection of tutorials and guides on setting up your CI pipeline:
## Languages, frameworks, OSs
### PHP
- [Testing a PHP application](php.md)
- [Run PHP Composer & NPM scripts then deploy them to a staging server](deployment/composer-npm-deploy.md)
### Ruby
- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
### Python
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
- [Test a Clojure application](test-clojure-application.md)
### Java
- **Articles:**
- [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
### Scala
- [Test a Scala application](test-scala-application.md)
### Clojure
- [Test a Clojure application](test-clojure-application.md)
### Elixir
- [Test a Phoenix application](test-phoenix-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
- [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
- [Run PHP Composer & NPM scripts then deploy them to a staging server](deployment/composer-npm-deploy.md)
- Help your favorite programming language and GitLab by sending a merge request
with a guide for that language.
- **Articles:**
- [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
### iOS
- **Articles:**
- [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
### Android
## Outside the documentation
- **Articles:**
- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
### Other
- [Using `dpl` as deployment tool](deployment/README.md)
- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
- **Articles:**
- [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
## GitLab CI for GitLab Pages
- [Example projects](https://gitlab.com/pages)
- **Articles:**
- [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](../../user/project/pages/getting_started_part_four.md)
- [SSGs Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/):
examples for Ruby-, NodeJS-, Python-, and GoLang-based SSGs
- [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
- [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
See the topic [GitLab Pages](../../user/project/pages/index.md) for a complete overview.
## More
Contributions are very much welcomed! You can help your favorite programming
language and GitLab by sending a merge request with a guide for that language.
[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment