Commit 88cc9d52 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into feature/sm/35954-create-kubernetes-cluster-on-gke-from-k8s-service

parents d6e22e83 8921af39
......@@ -5,3 +5,4 @@ app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb
......@@ -195,6 +195,10 @@ entry.
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
- [BUGIFX] Improves subgroup creation permissions. !13418
## 9.5.7 (2017-10-03)
- Fix gitlab rake:import:repos task.
## 9.5.6 (2017-09-29)
- [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242
......
......@@ -910,7 +910,7 @@ GEM
json (>= 1.8.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
unf_ext (0.0.7.4)
unicode-display_width (1.3.0)
unicorn (5.1.0)
kgio (~> 2.6)
......
import Jed from 'jed';
import sprintf from './sprintf';
/**
This is required to require all the translation folders in the current directory
this saves us having to do this manually & keep up to date with new languages
**/
function requireAll(requireContext) { return requireContext.keys().map(requireContext); }
const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/));
const locales = allLocales.reduce((d, obj) => {
const data = d;
const localeKey = Object.keys(obj)[0];
data[localeKey] = obj[localeKey];
return data;
}, {});
const langAttribute = document.querySelector('html').getAttribute('lang');
const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(locales[lang]);
const locale = new Jed(window.translations || {});
/**
Translates `text`
@param text The text to be translated
@returns {String} The translated text
**/
......
......@@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper';
$el.text(gl.text.addDelimiter(count));
};
MergeRequest.prototype.hideCloseButton = function() {
const el = document.querySelector('.merge-request .issuable-actions');
const closeDropdownItem = el.querySelector('li.close-item');
if (closeDropdownItem) {
closeDropdownItem.classList.add('hidden');
// Selects the next dropdown item
el.querySelector('li.report-item').click();
} else {
// No dropdown just hide the Close button
el.querySelector('.btn-close').classList.add('hidden');
}
// Dropdown for mobile screen
el.querySelector('li.js-close-item').classList.add('hidden');
};
return MergeRequest;
})();
}).call(window);
<template>
<div class="cell">
<code-cell
type="input"
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:output="output"
:code-css-class="codeCssClass" />
</div>
</template>
<script>
import CodeCell from './code/index.vue';
import OutputCell from './output/index.vue';
......@@ -51,6 +36,21 @@ export default {
};
</script>
<template>
<div class="cell">
<code-cell
type="input"
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:output="output"
:code-css-class="codeCssClass" />
</div>
</template>
<style scoped>
.cell {
flex-direction: column;
......
<template>
<div :class="type">
<prompt
:type="promptType"
:count="count" />
<pre
class="language-python"
:class="codeCssClass"
ref="code"
v-text="code">
</pre>
</div>
</template>
<script>
import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue';
......@@ -55,3 +41,17 @@
},
};
</script>
<template>
<div :class="type">
<prompt
:type="promptType"
:count="count" />
<pre
class="language-python"
:class="codeCssClass"
ref="code"
v-text="code">
</pre>
</div>
</template>
<template>
<div class="cell text-cell">
<prompt />
<div class="markdown" v-html="markdown"></div>
</div>
</template>
<script>
/* global katex */
import marked from 'marked';
......@@ -95,6 +88,13 @@
};
</script>
<template>
<div class="cell text-cell">
<prompt />
<div class="markdown" v-html="markdown"></div>
</div>
</template>
<style>
.markdown .katex {
display: block;
......
<template>
<div class="output">
<prompt />
<div v-html="rawCode"></div>
</div>
</template>
<script>
import Prompt from '../prompt.vue';
......@@ -20,3 +13,10 @@ export default {
},
};
</script>
<template>
<div class="output">
<prompt />
<div v-html="rawCode"></div>
</div>
</template>
<template>
<div class="output">
<prompt />
<img
:src="'data:' + outputType + ';base64,' + rawCode" />
</div>
</template>
<script>
import Prompt from '../prompt.vue';
......@@ -25,3 +17,11 @@ export default {
},
};
</script>
<template>
<div class="output">
<prompt />
<img
:src="'data:' + outputType + ';base64,' + rawCode" />
</div>
</template>
<template>
<component :is="componentName"
type="output"
:outputType="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass" />
</template>
<script>
import CodeCell from '../code/index.vue';
import Html from './html.vue';
......@@ -81,3 +72,12 @@ export default {
},
};
</script>
<template>
<component :is="componentName"
type="output"
:outputType="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass" />
</template>
<template>
<div class="prompt">
<span v-if="type && count">
{{ type }} [{{ count }}]:
</span>
</div>
</template>
<script>
export default {
props: {
......@@ -21,6 +13,14 @@
};
</script>
<template>
<div class="prompt">
<span v-if="type && count">
{{ type }} [{{ count }}]:
</span>
</div>
</template>
<style scoped>
.prompt {
padding: 0 10px;
......
<template>
<div v-if="hasNotebook">
<component
v-for="(cell, index) in cells"
:is="cellType(cell.cell_type)"
:cell="cell"
:key="index"
:code-css-class="codeCssClass" />
</div>
</template>
<script>
import {
MarkdownCell,
......@@ -59,6 +48,17 @@
};
</script>
<template>
<div v-if="hasNotebook">
<component
v-for="(cell, index) in cells"
:is="cellType(cell.cell_type)"
:cell="cell"
:key="index"
:code-css-class="codeCssClass" />
</div>
</template>
<style>
.cell,
.input,
......
......@@ -272,6 +272,7 @@
v-model="note"
ref="textarea"
slot="textarea"
:disabled="isSubmitting"
placeholder="Write a comment or drag your files here..."
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()">
......
<template>
<div class="pdf-viewer" v-if="hasPDF">
<page v-for="(page, index) in pages"
:key="index"
:v-if="!loading"
:page="page"
:number="index + 1" />
</div>
</template>
<script>
import pdfjsLib from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min';
......@@ -64,6 +54,16 @@
};
</script>
<template>
<div class="pdf-viewer" v-if="hasPDF">
<page v-for="(page, index) in pages"
:key="index"
:v-if="!loading"
:page="page"
:number="index + 1" />
</div>
</template>
<style>
.pdf-viewer {
background: url('./assets/img/bg.gif');
......
<template>
<canvas
class="pdf-page"
ref="canvas"
:data-page="number" />
</template>
<script>
export default {
props: {
......@@ -48,6 +41,13 @@
};
</script>
<template>
<canvas
class="pdf-page"
ref="canvas"
:data-page="number" />
</template>
<style>
.pdf-page {
margin: 8px auto 0 auto;
......
export default () => {
$('.fork-thumbnail a').on('click', function forkThumbnailClicked() {
$('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
if ($(this).hasClass('disabled')) return false;
$('.fork-namespaces').hide();
return $('.save-project-loader').show();
return $('.js-fork-content').toggle();
});
};
<script>
/* globals Flash */
import { mapGetters, mapActions } from 'vuex';
import '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import store from '../stores';
import collapsibleContainer from './collapsible_container.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
export default {
name: 'registryListApp',
props: {
endpoint: {
type: String,
required: true,
},
},
store,
components: {
collapsibleContainer,
loadingIcon,
},
computed: {
...mapGetters([
'isLoading',
'repos',
]),
},
methods: {
...mapActions([
'setMainEndpoint',
'fetchRepos',
]),
},
created() {
this.setMainEndpoint(this.endpoint);
},
mounted() {
this.fetchRepos()
.catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
},
};
</script>
<template>
<div>
<loading-icon
v-if="isLoading"
size="3"
/>
<collapsible-container
v-else-if="!isLoading && repos.length"
v-for="(item, index) in repos"
:key="index"
:repo="item"
/>
<p v-else-if="!isLoading && !repos.length">
{{__("No container images stored for this project. Add one by following the instructions above.")}}
</p>
</div>
</template>
<script>
/* globals Flash */
import { mapActions } from 'vuex';
import '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import tableRegistry from './table_registry.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
export default {
name: 'collapsibeContainerRegisty',
props: {
repo: {
type: Object,
required: true,
},
},
components: {
clipboardButton,
loadingIcon,
tableRegistry,
},
directives: {
tooltip,
},
data() {
return {
isOpen: false,
};
},
computed: {
clipboardText() {
return `docker pull ${this.repo.location}`;
},
},
methods: {
...mapActions([
'fetchRepos',
'fetchList',
'deleteRepo',
]),
toggleRepo() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.fetchList({ repo: this.repo })
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
}
},
handleDeleteRepository() {
this.deleteRepo(this.repo)
.then(() => this.fetchRepos())
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
},
showError(message) {
Flash((errorMessages[message]));
},
},
};
</script>
<template>
<div class="container-image">
<div
class="container-image-head">
<button
type="button"
@click="toggleRepo"
class="js-toggle-repo btn-link">
<i
class="fa"
:class="{
'fa-chevron-right': !isOpen,
'fa-chevron-up': isOpen,
}"
aria-hidden="true">
</i>
{{repo.name}}
</button>
<clipboard-button
v-if="repo.location"
:text="clipboardText"
:title="repo.location"
/>
<div class="controls hidden-xs pull-right">
<button
v-if="repo.canDelete"
type="button"
class="js-remove-repo btn btn-danger"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
v-tooltip
@click="handleDeleteRepository">
<i
class="fa fa-trash"
aria-hidden="true">
</i>
</button>
</div>
</div>
<loading-icon
v-if="repo.isLoading"
class="append-bottom-20"
size="2"
/>
<div
v-else-if="!repo.isLoading && isOpen"
class="container-image-tags">
<table-registry
v-if="repo.list.length"
:repo="repo"
/>
<div
v-else
class="nothing-here-block">
{{s__("ContainerRegistry|No tags in Container Registry for this container image.")}}
</div>
</div>
</div>
</template>
<script>
/* globals Flash */
import { mapActions } from 'vuex';
import { n__ } from '../../locale';
import '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
export default {
props: {
repo: {
type: Object,
required: true,
},
},
components: {
clipboardButton,
tablePagination,
},
mixins: [
timeagoMixin,
],
directives: {
tooltip,
},
computed: {
shouldRenderPagination() {
return this.repo.pagination.total > this.repo.pagination.perPage;
},
},
methods: {
...mapActions([
'fetchList',
'deleteRegistry',
]),
layers(item) {
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
},
onPageChange(pageNumber) {
this.fetchList({ repo: this.repo, page: pageNumber })
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
},
clipboardText(text) {
return `docker pull ${text}`;
},
showError(message) {
Flash((errorMessages[message]));
},
},
};
</script>
<template>
<div>
<table class="table tags">
<thead>
<tr>
<th>{{s__('ContainerRegistry|Tag')}}</th>
<th>{{s__('ContainerRegistry|Tag ID')}}</th>
<th>{{s__("ContainerRegistry|Size")}}</th>
<th>{{s__("ContainerRegistry|Created")}}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, i) in repo.list"
:key="i">
<td>
{{item.tag}}
<clipboard-button
v-if="item.location"
:title="item.location"
:text="clipboardText(item.location)"
/>
</td>
<td>
<span
v-tooltip
:title="item.revision"
data-placement="bottom">
{{item.shortRevision}}
</span>
</td>
<td>
{{item.size}}
<template v-if="item.size && item.layers">
&middot;
</template>
{{layers(item)}}
</td>
<td>
{{timeFormated(item.createdAt)}}
</td>
<td class="content">
<button
v-if="item.canDelete"
type="button"
class="js-delete-registry btn btn-danger hidden-xs pull-right"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
data-container="body"
v-tooltip
@click="handleDeleteRegistry(item)">
<i
class="fa fa-trash"
aria-hidden="true">
</i>
</button>
</td>
</tr>
</tbody>
</table>
<table-pagination
v-if="shouldRenderPagination"
:change="onPageChange"
:page-info="repo.pagination"
/>
</div>
</template>
import { __ } from '../locale';
export const errorMessagesTypes = {
FETCH_REGISTRY: 'FETCH_REGISTRY',
FETCH_REPOS: 'FETCH_REPOS',
DELETE_REPO: 'DELETE_REPO',
DELETE_REGISTRY: 'DELETE_REGISTRY',
};
export const errorMessages = {
[errorMessagesTypes.FETCH_REGISTRY]: __('Something went wrong while fetching the registry list.'),
[errorMessagesTypes.FETCH_REPOS]: __('Something went wrong while fetching the projects.'),
[errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'),
[errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'),
};
import Vue from 'vue';
import registryApp from './components/app.vue';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-vue-registry-images',
components: {
registryApp,
},
data() {
const dataset = document.querySelector(this.$options.el).dataset;
return {
endpoint: dataset.endpoint,
};
},
render(createElement) {
return createElement('registry-app', {
props: {
endpoint: this.endpoint,
},
});
},
}));
import Vue from 'vue';
import VueResource from 'vue-resource';
import * as types from './mutation_types';
Vue.use(VueResource);
export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING);
return Vue.http.get(state.endpoint)
.then(res => res.json())
.then((response) => {
commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, response);
});
};
export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
return Vue.http.get(repo.tagsPath, { params: { page } })
.then((response) => {
const headers = response.headers;
return response.json().then((resp) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
});
});
};
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath)
.then(res => res.json());
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath)
.then(res => res.json());
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
export const isLoading = state => state.isLoading;
export const repos = state => state.repos;
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
isLoading: false,
endpoint: '', // initial endpoint to fetch the repos list
/**
* Each object in `repos` has the following strucure:
* {
* name: String,
* isLoading: Boolean,
* tagsPath: String // endpoint to request the list
* destroyPath: String // endpoit to delete the repo
* list: Array // List of the registry images
* }
*
* Each registry image inside `list` has the following structure:
* {
* tag: String,
* revision: String
* shortRevision: String
* size: Number
* layers: Number
* createdAt: String
* destroyPath: String // endpoit to delete each image
* }
*/
repos: [],
},
actions,
getters,
mutations,
});
export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT';
export const SET_REPOS_LIST = 'SET_REPOS_LIST';
export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING';
export const SET_REGISTRY_LIST = 'SET_REGISTRY_LIST';
export const TOGGLE_REGISTRY_LIST_LOADING = 'TOGGLE_REGISTRY_LIST_LOADING';
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
export default {
[types.SET_MAIN_ENDPOINT](state, endpoint) {
Object.assign(state, { endpoint });
},
[types.SET_REPOS_LIST](state, list) {
Object.assign(state, {
repos: list.map(el => ({
canDelete: !!el.destroy_path,
destroyPath: el.destroy_path,
id: el.id,
isLoading: false,
list: [],
location: el.location,
name: el.path,
tagsPath: el.tags_path,
})),
});
},
[types.TOGGLE_MAIN_LOADING](state) {
Object.assign(state, { isLoading: !state.isLoading });
},
[types.SET_REGISTRY_LIST](state, { repo, resp, headers }) {
const listToUpdate = state.repos.find(el => el.id === repo.id);
const normalizedHeaders = normalizeHeaders(headers);
const pagination = parseIntPagination(normalizedHeaders);
listToUpdate.pagination = pagination;
listToUpdate.list = resp.map(element => ({
tag: element.name,
revision: element.revision,
shortRevision: element.short_revision,
size: element.size,
layers: element.layers,
location: element.location,
createdAt: element.created_at,
destroyPath: element.destroy_path,
canDelete: !!element.destroy_path,
}));
},
[types.TOGGLE_REGISTRY_LIST_LOADING](state, list) {
const listToUpdate = state.repos.find(el => el.id === list.id);
listToUpdate.isLoading = !listToUpdate.isLoading;
},
};
......@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="loading" showDisabledButton />
<status-icon status="loading" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Checking ability to merge automatically
......
......@@ -12,7 +12,7 @@ export default {
<div class="mr-widget-body media">
<status-icon
status="failed"
showDisabledButton />
:show-disabled-button="true" />
<div class="media-body space-children">
<span
v-if="mr.shouldBeRebased"
......
......@@ -51,7 +51,7 @@ export default {
</span>
</template>
<template v-else>
<status-icon status="failed" showDisabledButton />
<status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
<span
......
......@@ -24,7 +24,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton />
<status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold js-branch-text">
<span class="capitalize">
......
......@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="success" showDisabledButton />
<status-icon status="success" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Ready to be merged automatically.
......
......@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton />
<status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
......
......@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton />
<status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
......
......@@ -38,24 +38,40 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
mergeButtonClass() {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) {
return failedClass;
return 'failed';
} else if (!pipeline) {
return defaultClass;
return 'success';
} else if (isPipelineActive) {
return inActionClass;
return 'pending';
} else if (isPipelineFailed) {
return 'failed';
}
return 'success';
},
mergeButtonClass() {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
if (this.status === 'failed') {
return failedClass;
} else if (this.status === 'pending') {
return inActionClass;
}
return defaultClass;
},
iconClass() {
if (this.status === 'failed' || !this.commitMessage.length || !this.isMergeAllowed() || this.mr.preventMerge) {
return 'failed';
}
return 'success';
},
mergeButtonText() {
if (this.isMergingImmediately) {
return 'Merge in progress';
......@@ -156,6 +172,7 @@ export default {
eventHub.$emit('FetchActionsContent');
if (window.mergeRequest) {
window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged');
window.mergeRequest.hideCloseButton();
window.mergeRequest.decreaseCounter();
}
stopPolling();
......@@ -208,7 +225,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="success" />
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5">
......
......@@ -7,7 +7,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton />
<status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging
......
......@@ -10,7 +10,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton />
<status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
There are unresolved discussions. Please resolve these discussions
......
......@@ -38,7 +38,7 @@ export default {
},
template: `
<div class="mr-widget-body media">
<status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" />
<status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
......
<script>
/**
* Falls back to the code used in `copy_to_clipboard.js`
*/
export default {
name: 'clipboardButton',
props: {
text: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
},
};
</script>
<template>
<button
type="button"
class="btn btn-transparent btn-clipboard"
:data-title="title"
:data-clipboard-text="text">
<i
aria-hidden="true"
class="fa fa-clipboard">
</i>
</button>
</template>
......@@ -23,6 +23,7 @@
&.s60 { @include avatar-size(60px, 12px); }
&.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); }
&.s100 { @include avatar-size(100px, 15px); }
&.s110 { @include avatar-size(110px, 15px); }
&.s140 { @include avatar-size(140px, 15px); }
&.s160 { @include avatar-size(160px, 20px); }
......@@ -78,6 +79,7 @@
&.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
&.s100 { font-size: 36px; line-height: 98px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
&.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; }
......
......@@ -11,6 +11,7 @@
.prepend-top-10 { margin-top: 10px; }
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
......@@ -129,11 +130,6 @@ span.update-author {
}
}
.user-mention {
color: $user-mention-color;
font-weight: $gl-font-weight-bold;
}
.field_with_errors {
display: inline;
}
......
......@@ -6,3 +6,14 @@
.gfm-commit_range {
@extend .commit-sha;
}
.gfm-project_member {
padding: 0 2px;
border-radius: #{$border-radius-default / 2};
background-color: $user-mention-bg;
&:hover {
background-color: $user-mention-bg-hover;
text-decoration: none;
}
}
......@@ -48,31 +48,24 @@
}
&:hover {
background-color: $white-normal;
border-color: $border-white-normal;
border-color: $gray-darkest;
color: $gl-text-color;
}
}
}
.select2-drop {
box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0;
border-radius: $border-radius-default;
border: none;
.select2-drop,
.select2-drop.select2-drop-above {
box-shadow: 0 2px 4px $dropdown-shadow-color;
border-radius: $border-radius-base;
border: 1px solid $dropdown-border-color;
min-width: 175px;
color: $gl-text-color;
}
.select2-results .select2-result-label,
.select2-more-results {
padding: 10px 15px;
}
.select2-drop {
color: $gl-grayish-blue;
}
.select2-highlighted {
background: $gl-link-color !important;
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid $dropdown-border-color;
margin-top: -6px;
}
.select2-results li.select2-result-with-children > .select2-result-label {
......@@ -87,13 +80,11 @@
}
}
.select2-dropdown-open {
.select2-dropdown-open,
.select2-dropdown-open.select2-drop-above {
.select2-choice {
border-color: $border-white-normal;
border-color: $gray-darkest;
outline: 0;
background-image: none;
background-color: $white-dark;
box-shadow: $gl-btn-active-gradient;
}
}
......@@ -131,28 +122,14 @@
}
}
}
&.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices {
border-color: $border-white-normal;
box-shadow: $gl-btn-active-gradient;
}
}
.select2-drop-active {
margin-top: 6px;
margin-top: $dropdown-vertical-offset;
font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
.select2-results {
max-height: 350px;
.select2-highlighted {
background: $gl-primary;
}
}
}
......@@ -186,19 +163,35 @@
background-size: 16px 16px !important;
}
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: $gray-light;
display: list-item;
padding: 10px 15px;
}
.select2-results {
margin: 0;
padding: 10px 0;
padding: #{$gl-padding / 2} 0;
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-result-label,
.select2-more-results {
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-highlighted {
background: transparent;
color: $gl-text-color;
.select2-result-label {
background: $dropdown-item-hover-bg;
}
}
.select2-result {
padding: 0 1px;
}
}
.ajax-users-select {
......@@ -265,56 +258,10 @@
min-width: 250px !important;
}
// TODO: change global style
.ajax-project-dropdown,
.ajax-users-dropdown,
body[data-page="projects:edit"] #select2-drop,
body[data-page="projects:new"] #select2-drop,
body[data-page="projects:merge_requests:edit"] #select2-drop,
body[data-page="projects:blob:new"] #select2-drop,
body[data-page="profiles:show"] #select2-drop,
body[data-page="admin:groups:show"] #select2-drop,
body[data-page="projects:issues:show"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop {
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
color: $gl-text-color;
}
&.select2-drop-above {
border-top: none;
margin-top: -4px;
}
.select2-results {
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
}
.select2-result {
padding: 0 1px;
.select2-match {
font-weight: $gl-font-weight-bold;
text-decoration: none;
}
.select2-result-label {
padding: #{$gl-padding / 2} $gl-padding;
}
&.select2-highlighted {
background-color: transparent !important;
color: $gl-text-color;
.select2-result-label {
background-color: $dropdown-item-hover-bg;
}
}
}
.select2-result-selectable,
.select2-result-unselectable {
.select2-match {
font-weight: $gl-font-weight-bold;
text-decoration: none;
}
}
......@@ -262,7 +262,8 @@ $well-pre-bg: #eee;
$well-pre-color: #555;
$loading-color: #555;
$update-author-color: #999;
$user-mention-color: #2fa0bb;
$user-mention-bg: rgba($blue-500, 0.044);
$user-mention-bg-hover: rgba($blue-500, 0.15);
$time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
......
......@@ -9,6 +9,14 @@
.container-image-head {
padding: 0 16px;
line-height: 4em;
.btn-link {
padding: 0;
&:focus {
outline: none;
}
}
}
.table.tags {
......
......@@ -499,73 +499,56 @@ a.deploy-project-label {
}
}
.fork-namespaces {
.row {
-webkit-flex-wrap: wrap;
display: -webkit-flex;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.fork-thumbnail {
height: 200px;
width: calc((100% / 2) - #{$gl-padding * 2});
.fork-thumbnail {
border-radius: $border-radius-base;
background-color: $white-light;
border: 1px solid $border-white-light;
height: 202px;
margin: $gl-padding;
text-align: center;
width: 169px;
@media (min-width: $screen-md-min) {
width: calc((100% / 4) - #{$gl-padding * 2});
}
&:hover:not(.disabled),
&.forked {
background-color: $row-hover;
border-color: $row-hover-border;
}
@media (min-width: $screen-lg-min) {
width: calc((100% / 5) - #{$gl-padding * 2});
}
.no-avatar {
width: 100px;
height: 100px;
background-color: $gray-light;
border: 1px solid $white-normal;
margin: 0 auto;
border-radius: 50%;
i {
font-size: 100px;
color: $white-normal;
}
}
&:hover:not(.disabled),
&.forked {
background-color: $row-hover;
border-color: $row-hover-border;
}
a {
display: block;
width: 100%;
height: 100%;
padding-top: $gl-padding;
color: $gl-text-color;
&.disabled {
opacity: .3;
cursor: not-allowed;
&:hover {
text-decoration: none;
}
}
.caption {
min-height: 30px;
padding: $gl-padding 0;
}
}
.avatar-container,
.identicon {
float: none;
margin-left: auto;
margin-right: auto;
}
img {
border-radius: 50%;
max-width: 100px;
}
a {
display: block;
width: 100%;
height: 100%;
padding-top: $gl-padding;
text-decoration: none;
&.disabled {
opacity: .3;
cursor: not-allowed;
}
}
}
.fork-thumbnail-container {
display: flex;
flex-wrap: wrap;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
> h5 {
width: 100%;
}
}
.project-template,
.project-import {
.form-group {
......
......@@ -12,3 +12,7 @@
margin-left: 10px;
}
}
.registry-placeholder {
min-height: 60px;
}
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def index
set_index_vars
@personal_access_token = finder.build
end
def create
......@@ -40,7 +41,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def set_index_vars
@scopes = Gitlab::Auth.available_scopes
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
end
......
......@@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController
def index
@sort = params[:sort].presence || sort_value_recently_updated
@branches = BranchesFinder.new(@repository, params).execute
@branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
respond_to do |format|
......
......@@ -6,17 +6,26 @@ module Projects
def index
@images = project.container_repositories
respond_to do |format|
format.html
format.json do
render json: ContainerRepositoriesSerializer
.new(project: project, current_user: current_user)
.represent(@images)
end
end
end
def destroy
if image.destroy
redirect_to project_container_registry_index_path(@project),
status: 302,
notice: 'Image repository has been removed successfully!'
respond_to do |format|
format.json { head :no_content }
end
else
redirect_to project_container_registry_index_path(@project),
status: 302,
alert: 'Failed to remove image repository!'
respond_to do |format|
format.json { head :bad_request }
end
end
end
......
......@@ -3,20 +3,35 @@ module Projects
class TagsController < ::Projects::Registry::ApplicationController
before_action :authorize_update_container_image!, only: [:destroy]
def index
respond_to do |format|
format.json do
render json: ContainerTagsSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(tags)
end
end
end
def destroy
if tag.delete
redirect_to project_container_registry_index_path(@project),
status: 302,
notice: 'Registry tag has been removed successfully!'
respond_to do |format|
format.json { head :no_content }
end
else
redirect_to project_container_registry_index_path(@project),
status: 302,
alert: 'Failed to remove registry tag!'
respond_to do |format|
format.json { head :bad_request }
end
end
end
private
def tags
Kaminari::PaginatableArray.new(image.tags, limit: 15)
end
def image
@image ||= project.container_repositories
.find(params[:repository_id])
......
module EventsHelper
ICON_NAMES_BY_EVENT_TYPE = {
'pushed to' => 'icon_commit',
'pushed new' => 'icon_commit',
'created' => 'icon_status_open',
'opened' => 'icon_status_open',
'closed' => 'icon_status_closed',
'accepted' => 'icon_code_fork',
'commented on' => 'icon_comment_o',
'deleted' => 'icon_trash_o'
'pushed to' => 'commit',
'pushed new' => 'commit',
'created' => 'status_open',
'opened' => 'status_open',
'closed' => 'status_closed',
'accepted' => 'fork',
'commented on' => 'comment',
'deleted' => 'remove',
'imported' => 'import',
'joined' => 'users'
}.freeze
def link_to_author(event, self_added: false)
......@@ -197,7 +199,7 @@ module EventsHelper
def icon_for_event(note)
icon_name = ICON_NAMES_BY_EVENT_TYPE[note]
custom_icon(icon_name) if icon_name
sprite_icon(icon_name) if icon_name
end
def icon_for_profile_event(event)
......
......@@ -34,6 +34,7 @@ class Key < ActiveRecord::Base
value&.delete!("\n\r")
value.strip! unless value.blank?
write_attribute(:key, value)
@public_key = nil
end
def publishable_key
......
......@@ -560,14 +560,20 @@ class MergeRequest < ActiveRecord::Base
commits_for_notes_limit = 100
commit_ids = commit_shas.take(commits_for_notes_limit)
Note.where(
"(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
"((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
mr_id: id,
commit_ids: commit_ids,
target_project_id: target_project_id,
source_project_id: source_project_id
)
commit_notes = Note
.except(:order)
.where(project_id: [source_project_id, target_project_id])
.where(noteable_type: 'Commit', commit_id: commit_ids)
# We're using a UNION ALL here since this results in better performance
# compared to using OR statements. We're using UNION ALL since the queries
# used won't produce any duplicates (e.g. a note for a commit can't also be
# a note for an MR).
union = Gitlab::SQL::Union
.new([notes, commit_notes], remove_duplicates: false)
.to_sql
Note.from("(#{union}) #{Note.table_name}")
end
alias_method :discussion_notes, :related_notes
......@@ -742,10 +748,9 @@ class MergeRequest < ActiveRecord::Base
end
def has_ci?
has_ci_integration = source_project.try(:ci_service)
uses_gitlab_ci = all_pipelines.any?
return false if has_no_commits?
(has_ci_integration || uses_gitlab_ci) && commits.any?
!!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
end
def branch_missing?
......
......@@ -17,6 +17,8 @@ class PersonalAccessToken < ActiveRecord::Base
validates :scopes, presence: true
validate :validate_scopes
after_initialize :set_default_scopes, if: :persisted?
def revoke!
update!(revoked: true)
end
......@@ -32,4 +34,8 @@ class PersonalAccessToken < ActiveRecord::Base
errors.add :scopes, "can only contain available scopes"
end
end
def set_default_scopes
self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
end
end
......@@ -989,7 +989,7 @@ class Repository
end
def create_ref(ref, ref_path)
fetch_ref(path_to_repo, ref, ref_path)
raw_repository.write_ref(ref_path, ref)
end
def ls_files(ref)
......
......@@ -31,7 +31,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def remove_wip_path
if can?(current_user, :update_merge_request, merge_request.project)
if work_in_progress? && can?(current_user, :update_merge_request, merge_request.project)
remove_wip_project_merge_request_path(project, merge_request)
end
end
......
class ContainerRepositoriesSerializer < BaseSerializer
entity ContainerRepositoryEntity
end
class ContainerRepositoryEntity < Grape::Entity
include RequestAwareEntity
expose :id, :path, :location
expose :tags_path do |repository|
project_registry_repository_tags_path(project, repository, format: :json)
end
expose :destroy_path, if: -> (*) { can_destroy? } do |repository|
project_container_registry_path(project, repository, format: :json)
end
private
alias_method :repository, :object
def project
request.project
end
def can_destroy?
can?(request.current_user, :update_container_image, project)
end
end
class ContainerTagEntity < Grape::Entity
include RequestAwareEntity
expose :name, :location, :revision, :total_size, :created_at
expose :destroy_path, if: -> (*) { can_destroy? } do |tag|
project_registry_repository_tag_path(project, tag.repository, tag.name, format: :json)
end
private
alias_method :tag, :object
def project
request.project
end
def can_destroy?
# TODO: We check permission against @project, not tag,
# as tag is no AR object that is attached to project
can?(request.current_user, :update_container_image, project)
end
end
class ContainerTagsSerializer < BaseSerializer
entity ContainerTagEntity
def with_pagination(request, response)
tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
end
def paginated?
@paginator.present?
end
def represent(resource, opts = {})
resource = @paginator.paginate(resource) if paginated?
super(resource, opts)
end
end
......@@ -23,7 +23,6 @@ class MergeRequestEntity < IssuableEntity
expose :closed_event, using: EventEntity
# User entities
expose :author, using: UserEntity
expose :merge_user, using: UserEntity
# Diff sha's
......@@ -31,7 +30,6 @@ class MergeRequestEntity < IssuableEntity
merge_request.diff_head_sha if merge_request.diff_head_commit
end
expose :merge_commit_sha
expose :merge_commit_message
expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline
......
......@@ -37,9 +37,9 @@
- if content_for?(:library_javascripts)
= yield :library_javascripts
= javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js")
= webpack_bundle_tag "webpack_runtime"
= webpack_bundle_tag "common"
= webpack_bundle_tag "locale"
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
......
......@@ -9,50 +9,36 @@
%br
Forking a repository allows you to make changes without affecting the original project.
.col-lg-9
.fork-namespaces
- if @namespaces.present?
%label.label-light
%span
Click to fork the project
- @namespaces.in_groups_of(6, false) do |group|
.row
- group.each do |namespace|
- avatar = namespace_icon(namespace, 100)
- if fork = namespace.find_fork_of(@project)
.fork-thumbnail.forked
= link_to project_path(fork) do
- if /no_((\w*)_)*avatar/.match(avatar)
.no-avatar
= icon 'question'
- else
= image_tag avatar
.caption
= namespace.human_name
- else
- can_create_project = current_user.can?(:create_projects, namespace)
.fork-thumbnail{ class: ("disabled" unless can_create_project) }
= link_to project_forks_path(@project, namespace_key: namespace.id),
method: "POST",
class: ("disabled has-tooltip" unless can_create_project),
title: (_('You have reached your project limit') unless can_create_project) do
- if /no_((\w*)_)*avatar/.match(avatar)
.no-avatar
= icon 'question'
- else
= image_tag avatar
.caption
= namespace.human_name
- else
%label.label-light
%span
No available namespaces to fork the project.
%br
%small
You must have permission to create a project in a namespace before forking.
- if @namespaces.present?
.fork-thumbnail-container.js-fork-content
%h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
Click to fork the project
- @namespaces.each do |namespace|
- avatar = namespace_icon(namespace, 100)
- can_create_project = current_user.can?(:create_projects, namespace)
- forked_project = namespace.find_fork_of(@project)
- fork_path = forked_project ? project_path(forked_project) : project_forks_path(@project, namespace_key: namespace.id)
.bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: [("disabled" unless can_create_project), ("forked" if forked_project)] }
= link_to fork_path,
method: "POST",
class: [("js-fork-thumbnail" unless forked_project), ("disabled has-tooltip" unless can_create_project)],
title: (_('You have reached your project limit') unless can_create_project) do
- if /no_((\w*)_)*avatar/.match(avatar)
= project_identicon(namespace, class: "avatar s100 identicon")
- else
.avatar-container.s100
= image_tag(avatar, class: "avatar s100")
%h5.prepend-top-default
= namespace.human_name
- else
%strong
No available namespaces to fork the project.
%p.prepend-top-default
You must have permission to create a project in a namespace before forking.
.save-project-loader.hide
.center
%h2
%i.fa.fa-spinner.fa-spin
Forking repository
%p Please wait a moment, this page will automatically refresh when ready.
.save-project-loader.hide.js-fork-content
%h2.text-center
= icon('spinner spin')
Forking repository
%p.text-center
Please wait a moment, this page will automatically refresh when ready.
......@@ -2,13 +2,13 @@
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list.related-merge-requests
- has_any_ci = @merge_requests.any?(&:head_pipeline)
- has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- @merge_requests.each do |merge_request|
%li
%span.merge-request-ci-status
- if merge_request.head_pipeline
= render_pipeline_status(merge_request.head_pipeline)
- elsif has_any_ci
- elsif has_any_head_pipeline
= icon('blank fw')
%span.merge-request-id
= merge_request.to_reference
......
......@@ -29,7 +29,7 @@
- unless current_user == @merge_request.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- if can_update_merge_request
%li{ class: merge_request_button_visibility(@merge_request, true) }
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
%li{ class: merge_request_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
......
.container-image.js-toggle-container
.container-image-head
= link_to "#", class: "js-toggle-button" do
= icon('chevron-down', 'aria-hidden': 'true')
= escape_once(image.path)
= clipboard_button(clipboard_text: "docker pull #{image.location}")
- if can?(current_user, :update_container_image, @project)
.controls.hidden-xs.pull-right
= link_to project_container_registry_path(@project, image),
class: 'btn btn-remove has-tooltip',
title: 'Remove repository',
data: { confirm: 'Are you sure?' },
method: :delete do
= icon('trash cred', 'aria-hidden': 'true')
.container-image-tags.js-toggle-content.hide
- if image.has_tags?
.table-holder
%table.table.tags
%thead
%tr
%th Tag
%th Tag ID
%th Size
%th Created
- if can?(current_user, :update_container_image, @project)
%th
= render partial: 'tag', collection: image.tags
- else
.nothing-here-block No tags in Container Registry for this container image.
- page_title "Container Registry"
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
%section
.settings-header
%h4
= page_title
%p
With the Docker Container Registry integrated into GitLab, every project
can have its own space to store its Docker images.
= s_('ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.')
%p.append-bottom-0
= succeed '.' do
Learn more about
= link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank'
= s_('ContainerRegistry|Learn more about')
= link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank'
.row.registry-placeholder.prepend-bottom-10
.col-lg-12
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
.col-lg-9
.panel.panel-default
.panel-heading
%h4.panel-title
How to use the Container Registry
.panel-body
%p
First log in to GitLab&rsquo;s Container Registry using your GitLab username
and password. If you have
= link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank'
you need to use a
= succeed ':' do
= link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank'
%pre
docker login #{Gitlab.config.registry.host_port}
%br
%p
Once you log in, you&rsquo;re free to create and upload a container image
using the common
%code build
and
%code push
commands:
%pre
:plain
docker build -t #{escape_once(@project.container_registry_url)} .
docker push #{escape_once(@project.container_registry_url)}
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('registry_list')
%hr
%h5.prepend-top-default
Use different image names
%p.light
GitLab supports up to 3 levels of image names. The following
examples of images are valid for your project:
%pre
:plain
#{escape_once(@project.container_registry_url)}:tag
#{escape_once(@project.container_registry_url)}/optional-image-name:tag
#{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
- if @images.blank?
%p.settings-message.text-center.append-bottom-default
No container images stored for this project. Add one by following the
instructions above.
- else
= render partial: 'image', collection: @images
.row.prepend-top-10
.col-lg-12
.panel.panel-default
.panel-heading
%h4.panel-title
= s_('ContainerRegistry|How to use the Container Registry')
.panel-body
%p
- link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank')
- link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank')
= s_('ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:').html_safe % { link_2fa: link_2fa, link_token: link_token }
%pre
docker login #{Gitlab.config.registry.host_port}
%br
%p
= s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
%pre
:plain
docker build -t #{escape_once(@project.container_registry_url)} .
docker push #{escape_once(@project.container_registry_url)}
%hr
%h5.prepend-top-default
= s_('ContainerRegistry|Use different image names')
%p.light
= s_('ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:')
%pre
:plain
#{escape_once(@project.container_registry_url)}:tag
#{escape_once(@project.container_registry_url)}/optional-image-name:tag
#{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
......@@ -23,7 +23,7 @@
- disabled_class = 'disabled'
- disabled_title = @service.disabled_title
= link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel'
= link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel'
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
%hr
......
......@@ -2,12 +2,11 @@
- release = @releases.find { |release| release.tag == tag.name }
%li.flex-row
.row-main-content.str-truncated
= link_to project_tag_path(@project, tag.name), class: 'item-title ref-name' do
= icon('tag')
= tag.name
= icon('tag')
= link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name prepend-left-4'
- if protected_tag?(@project, tag)
%span.label.label-success
%span.label.label-success.prepend-left-4
protected
- if tag.message.present?
......
- type = impersonation ? "impersonation" : "personal access"
%h5.prepend-top-0
Add a #{type} Token
Add a #{type} token
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique #{type} Token.
Pick a name for the application, and we'll give you a unique #{type} token.
= form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
......
---
title: "Add missing space in Sidekiq memory killer log message"
merge_request: 14553
author: Benjamin Drung
type: fixed
---
title: Fix the default branches sorting to actually be 'Last updated'
merge_request: 14295
author:
type: fixed
---
title: Re-arrange <script> tags before <template> tags in .vue files
merge_request: 14671
author:
type: changed
---
title: Hide close MR button after merge without reloading page
merge_request: 14122
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: fix merge request widget status icon for failed CI
merge_request:
author:
type: fixed
---
title: Use explicit boolean true attribute for show-disabled-button in Vue files
merge_request: 14672
author:
type: fixed
---
title: Set default scope on PATs that don't have one set to allow them to be revoked
merge_request:
author:
type: fixed
---
title: Add link to OpenID Connect documentation
merge_request: 14368
author: Markus Koller
type: other
---
title: Fix edit project service cancel button position
merge_request: 14596
author: Matt Coleman
type: fixed
---
title: Makes @mentions links have a different styling for better separation
merge_request:
author:
type: added
---
title: Use a UNION ALL for getting merge request notes
merge_request:
author:
type: other
---
title: Adjusts tag link to avoid underlining spaces
merge_request: 14544
author: Guilherme Vieira
type: fixed
......@@ -105,6 +105,7 @@ module Gitlab
config.assets.precompile << "lib/ace.js"
config.assets.precompile << "vendor/assets/fonts/*"
config.assets.precompile << "test.css"
config.assets.precompile << "locale/**/app.js"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
......
......@@ -499,6 +499,8 @@ production: &base
# Gitaly settings
gitaly:
# Path to the directory containing Gitaly client executables.
client_path: /home/git/gitaly
# Default Gitaly authentication token. Can be overriden per storage. Can
# be left blank when Gitaly is running locally on a Unix socket, which
# is the normal way to deploy Gitaly.
......@@ -664,7 +666,7 @@ test:
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
gitaly:
enabled: true
client_path: tmp/tests/gitaly
token: secret
backup:
path: tmp/tests/backups
......
......@@ -39,3 +39,17 @@ module GettextI18nRailsJs
end
end
end
class PoToJson
# This is required to modify the JS locale file output to our import needs
# Overwrites: https://github.com/webhippie/po_to_json/blob/master/lib/po_to_json.rb#L46
def generate_for_jed(language, overwrite = {})
@options = parse_options(overwrite.merge(language: language))
@parsed ||= inject_meta(parse_document)
generated = build_json_for(build_jed_for(@parsed))
[
"window.translations = #{generated};"
].join(" ")
end
end
......@@ -281,7 +281,7 @@ constraints(ProjectUrlConstrainer.new) do
namespace :registry do
resources :repository, only: [] do
resources :tags, only: [:destroy],
resources :tags, only: [:index, :destroy],
constraints: { id: Gitlab::Regex.container_registry_tag_regex }
end
end
......
......@@ -68,6 +68,7 @@ var config = {
prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches',
protected_tags: './protected_tags',
registry_list: './registry/index.js',
repo: './repo/index.js',
sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
......@@ -121,10 +122,6 @@ var config = {
name: '[name].[hash].[ext]',
}
},
{
test: /locale\/\w+\/(.*)\.js$/,
loader: 'exports-loader?locales',
},
{
test: /monaco-editor\/\w+\/vs\/loader\.js$/,
use: [
......@@ -200,6 +197,7 @@ var config = {
'pdf_viewer',
'pipelines',
'pipelines_details',
'registry_list',
'repo',
'schedule_form',
'schedules_index',
......@@ -222,7 +220,7 @@ var config = {
// create cacheable common library bundles
new webpack.optimize.CommonsChunkPlugin({
names: ['main', 'locale', 'common', 'webpack_runtime'],
names: ['main', 'common', 'webpack_runtime'],
}),
// enable scope hoisting
......
......@@ -7,11 +7,13 @@ class AddFastForwardOptionToProject < ActiveRecord::Migration
disable_ddl_transaction!
def add
def up
add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
end
def down
remove_column(:projects, :merge_requests_ff_only_enabled)
if column_exists?(:projects, :merge_requests_ff_only_enabled)
remove_column(:projects, :merge_requests_ff_only_enabled)
end
end
end
# rubocop:disable all
class MakeSureFastForwardOptionExists < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
# We had to fix the migration db/migrate/20150827121444_add_fast_forward_option_to_project.rb
# And this is why it's possible that someone has ran the migrations but does
# not have the merge_requests_ff_only_enabled column. This migration makes sure it will
# be added
unless column_exists?(:projects, :merge_requests_ff_only_enabled)
add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
end
end
def down
if column_exists?(:projects, :merge_requests_ff_only_enabled)
remove_column(:projects, :merge_requests_ff_only_enabled)
end
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170928100231) do
ActiveRecord::Schema.define(version: 20171004121444) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -121,6 +121,15 @@ GET /projects/:id/merge_requests?labels=bug,reproduced
GET /projects/:id/merge_requests?my_reaction_emoji=star
```
`project_id` represents the ID of the project where the MR resides.
`project_id` will always equal `target_project_id`.
In the case of a merge request from the same project,
`source_project_id`, `target_project_id` and `project_id`
will be the same. In the case of a merge request from a fork,
`target_project_id` and `project_id` will be the same and
`source_project_id` will be the fork project's ID.
Parameters:
| Attribute | Type | Required | Description |
......
## Enable or disable GitLab CI
## Enable or disable GitLab CI/CD
_To effectively use GitLab CI, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
file present at the root directory of your project and a
[runner](runners/README.md) properly set up. You can read our
[quick start guide](quick_start/README.md) to get you started._
[quick start guide](quick_start/README.md) to get you started.
If you are using an external CI server like Jenkins or Drone CI, it is advised
to disable GitLab CI in order to not have any conflicts with the commits status
If you are using an external CI/CD server like Jenkins or Drone CI, it is advised
to disable GitLab CI/CD in order to not have any conflicts with the commits status
API.
---
GitLab CI is exposed via the `/pipelines` and `/builds` pages of a project.
Disabling GitLab CI in a project does not delete any previous jobs.
In fact, the `/pipelines` and `/builds` pages can still be accessed, although
GitLab CI/CD is exposed via the `/pipelines` and `/jobs` pages of a project.
Disabling GitLab CI/CD in a project does not delete any previous jobs.
In fact, the `/pipelines` and `/jobs` pages can still be accessed, although
it's hidden from the left sidebar menu.
GitLab CI is enabled by default on new installations and can be disabled either
GitLab CI/CD is enabled by default on new installations and can be disabled either
individually under each project's settings, or site-wide by modifying the
settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
respectively.
### Per-project user setting
The setting to enable or disable GitLab CI can be found with the name **Pipelines**
under the **Sharing & Permissions** area of a project's settings along with
**Merge Requests**. Choose one of **Disabled**, **Only team members** and
**Everyone with access** and hit **Save changes** for the settings to take effect.
The setting to enable or disable GitLab CI/CD can be found under your project's
**Settings > General > Permissions**. Choose one of "Disabled", "Only team members"
or "Everyone with access" and hit **Save changes** for the settings to take effect.
![Sharing & Permissions settings](img/permissions_settings.png)
![Sharing & Permissions settings](../user/project/settings/img/sharing_and_permissions_settings.png)
---
### Site-wide administrator setting
### Site-wide admin setting
You can disable GitLab CI site-wide, by modifying the settings in `gitlab.yml`
You can disable GitLab CI/CD site-wide, by modifying the settings in `gitlab.yml`
and `gitlab.rb` for source and Omnibus installations respectively.
Two things to note:
1. Disabling GitLab CI, will affect only newly-created projects. Projects that
1. Disabling GitLab CI/CD, will affect only newly-created projects. Projects that
had it enabled prior to this modification, will work as before.
1. Even if you disable GitLab CI, users will still be able to enable it in the
1. Even if you disable GitLab CI/CD, users will still be able to enable it in the
project's settings.
---
For installations from source, open `gitlab.yml` with your editor and set
`builds` to `false`:
......
......@@ -26,7 +26,7 @@ so every environment can have one or more deployments. GitLab keeps track of
your deployments, so you always know what is currently being deployed on your
servers. If you have a deployment service such as [Kubernetes][kubernetes-service]
enabled for your project, you can use it to assist with your deployments, and
can even access a web terminal for your environment from within GitLab!
can even access a [web terminal](#web-terminals) for your environment from within GitLab!
To better understand how environments and deployments work, let's consider an
example. We assume that you have already created a project in GitLab and set up
......@@ -119,7 +119,7 @@ where you can find information of the last deployment status of an environment.
Here's how the Environments page looks so far.
![Staging environment view](img/environments_available_staging.png)
![Environment view](img/environments_available.png)
There's a bunch of information there, specifically you can see:
......@@ -229,7 +229,7 @@ You can find it in the pipeline, job, environment, and deployment views.
| Pipelines | Single pipeline | Environments | Deployments | jobs |
| --------- | ----------------| ------------ | ----------- | -------|
| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_builds.png) |
| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_jobs.png) |
Clicking on the play button in either of these places will trigger the
`deploy_prod` job, and the deployment will be recorded under a new
......@@ -402,7 +402,7 @@ places within GitLab.
| In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button |
| -------------------- | ------------ | ----------- |
| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_link_url.png) | ![Environment URL in deployments](img/environments_link_url_deployments.png) |
| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_available.png) | ![Environment URL in deployments](img/deployments_view.png) |
If a merge request is eventually merged to the default branch (in our case
`master`) and that branch also deploys to an environment (in our case `staging`
......@@ -574,7 +574,7 @@ Once configured, GitLab will attempt to retrieve [supported performance metrics]
environment which has had a successful deployment. If monitoring data was
successfully retrieved, a Monitoring button will appear for each environment.
![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png)
![Environment Detail with Metrics](img/deployments_view.png)
Clicking on the Monitoring button will display a new page, showing up to the last
8 hours of performance data. It may take a minute or two for data to appear
......@@ -593,10 +593,11 @@ Web terminals were added in GitLab 8.15 and are only available to project
masters and owners.
If you deploy to your environments with the help of a deployment service (e.g.,
the [Kubernetes service][kubernetes-service], GitLab can open
the [Kubernetes service][kubernetes-service]), GitLab can open
a terminal session to your environment! This is a very powerful feature that
allows you to debug issues without leaving the comfort of your web browser. To
enable it, just follow the instructions given in the service documentation.
enable it, just follow the instructions given in the service integration
documentation.
Once enabled, your environments will gain a "terminal" button:
......
doc/ci/img/deployments_view.png

19.5 KB | W: | H:

doc/ci/img/deployments_view.png

59.7 KB | W: | H:

doc/ci/img/deployments_view.png
doc/ci/img/deployments_view.png
doc/ci/img/deployments_view.png
doc/ci/img/deployments_view.png
  • 2-up
  • Swipe
  • Onion skin
doc/ci/img/environments_link_url_mr.png

17.5 KB | W: | H:

doc/ci/img/environments_link_url_mr.png

33.6 KB | W: | H:

doc/ci/img/environments_link_url_mr.png
doc/ci/img/environments_link_url_mr.png
doc/ci/img/environments_link_url_mr.png
doc/ci/img/environments_link_url_mr.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -149,14 +149,15 @@ script:
## Secret variables
>**Notes:**
- This feature requires GitLab Runner 0.4.0 or higher.
- Group-level secret variables added in GitLab 9.4.
- Be aware that secret variables are not masked, and their values can be shown
in the job logs if explicitly asked to do so. If your project is public or
internal, you can set the pipelines private from your project's Pipelines
settings. Follow the discussion in issue [#13784][ce-13784] for masking the
secret variables.
NOTE: **Note:**
Group-level secret variables were added in GitLab 9.4.
CAUTION: **Important:**
Be aware that secret variables are not masked, and their values can be shown
in the job logs if explicitly asked to do so. If your project is public or
internal, you can set the pipelines private from your [project's Pipelines
settings](../../user/project/pipelines/settings.md#visibility-of-pipelines).
Follow the discussion in issue [#13784][ce-13784] for masking the secret variables.
GitLab CI allows you to define per-project or per-group secret variables
that are set in the pipeline environment. The secret variables are stored out of
......@@ -171,6 +172,8 @@ Likewise, group-level secret variables can be added by going to your group's
**Settings > CI/CD**, then finding the section called **Secret variables**.
Any variables of [subgroups] will be inherited recursively.
![Secret variables](img/secret_variables.png)
Once you set them, they will be available for all subsequent pipelines. You can also
[protect your variables](#protected-secret-variables).
......@@ -202,7 +205,7 @@ are set in the build environment. These variables are only defined for
the project services that you are using to learn which variables they define.
An example project service that defines deployment variables is
[Kubernetes Service](../../user/project/integrations/kubernetes.md).
[Kubernetes Service](../../user/project/integrations/kubernetes.md#deployment-variables).
## Debug tracing
......@@ -439,7 +442,7 @@ export CI_REGISTRY_USER="gitlab-ci-token"
export CI_REGISTRY_PASSWORD="longalfanumstring"
```
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables"
[eep]: https://about.gitlab.com/gitlab-ee/ "Available only in GitLab Enterprise Edition Premium"
[envs]: ../environments.md
[protected branches]: ../../user/project/protected_branches.md
......
......@@ -470,7 +470,25 @@ On those a default key should not be provided.
```
#### Ordering
1. Order for a Vue Component:
1. Tag order in `.vue` file
```
<script>
// ...
</script>
<template>
// ...
</template>
// We don't use scoped styles but there are few instances of this
<style>
// ...
</style>
```
1. Properties in a Vue Component:
1. `name`
1. `props`
1. `mixins`
......@@ -490,6 +508,7 @@ On those a default key should not be provided.
1. `beforeDestroy`
1. `destroyed`
#### Vue and Bootstrap
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
......
......@@ -428,7 +428,7 @@ is a good example of this pattern.
## Style guide
Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs)
Please refer to the Vue section of our [style guide](style_guide_js.md#vue-js)
for best practices while writing your Vue components and templates.
## Testing Vue Components
......
......@@ -302,7 +302,7 @@ range of inputs, might look like this:
```ruby
describe "#==" do
using Rspec::Parameterized::TableSyntax
using RSpec::Parameterized::TableSyntax
let(:project1) { create(:project) }
let(:project2) { create(:project) }
......
......@@ -11,6 +11,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/)
- **Integrations:**
- [GitLab as OAuth2 authentication service provider](../../integration/oauth_provider.md#introduction-to-oauth)
- [GitLab as OpenID Connect identity provider](../../integration/openid_connect_provider.md)
## GitLab administrators
......
......@@ -17,25 +17,25 @@ have its own space to store its Docker images.
You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
---
## Enable the Container Registry for your project
NOTE: **Note:**
If you cannot find the Container Registry entry under your project's settings,
that means that it is not enabled in your GitLab instance. Ask your administrator
to enable it.
1. First, ask your system administrator to enable GitLab Container Registry
following the [administration documentation](../../administration/container_registry.md).
If you are using GitLab.com, this is enabled by default so you can start using
the Registry immediately.
1. Go to your project's settings and enable the **Container Registry** feature
on your project. For new projects this might be enabled by default. For
existing projects (prior GitLab 8.8), you will have to explicitly enable it.
![Enable Container Registry](img/container_registry_enable.png)
1. Go to your [project's General settings](settings/index.md#sharing-and-permissions)
and enable the **Container Registry** feature on your project. For new
projects this might be enabled by default. For existing projects
(prior GitLab 8.8), you will have to explicitly enable it.
1. Hit **Save changes** for the changes to take effect. You should now be able
to see the **Registry** link in the project menu.
to see the **Registry** link in the sidebar.
![Container Registry tab](img/container_registry_tab.png)
![Container Registry](img/container_registry.png)
## Build and push images
......
doc/user/project/img/issue_board.png

50.2 KB | W: | H:

doc/user/project/img/issue_board.png

80.7 KB | W: | H:

doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/img/labels_default.png

31.3 KB | W: | H:

doc/user/project/img/labels_default.png

23.8 KB | W: | H:

doc/user/project/img/labels_default.png
doc/user/project/img/labels_default.png
doc/user/project/img/labels_default.png
doc/user/project/img/labels_default.png
  • 2-up
  • Swipe
  • Onion skin
doc/user/project/img/labels_filter.png

31.2 KB | W: | H:

doc/user/project/img/labels_filter.png

18.6 KB | W: | H:

doc/user/project/img/labels_filter.png
doc/user/project/img/labels_filter.png
doc/user/project/img/labels_filter.png
doc/user/project/img/labels_filter.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -12,6 +12,8 @@ Other interesting links:
- [GitLab Issue Board landing page on about.gitlab.com][landing]
- [YouTube video introduction to Issue Boards][youtube]
![GitLab Issue Board](img/issue_board.png)
## Overview
The Issue Board builds on GitLab's existing
......@@ -89,10 +91,6 @@ two defaults:
- **Backlog** (default): shows all open issues that does not belong to one of lists. Always appears on the very left.
- **Closed** (default): shows all closed issues. Always appears on the very right.
![GitLab Issue Board](img/issue_board.png)
---
In short, here's a list of actions you can take in an Issue Board:
- [Create a new list](#creating-a-new-list).
......
......@@ -20,8 +20,6 @@ Head over a single project and navigate to **Issues > Labels**.
The first time you visit this page, you'll notice that there are no labels
created yet.
![Generate new labels](img/labels_generate.png)
Creating a new label from scratch is as easy as pressing the **New label**
button. From there on you can choose the name, give it an optional description,
a color and you are set.
......@@ -32,21 +30,23 @@ When you are ready press the **Create label** button to create the new label.
---
## Default Labels
It's possible to populate the labels for your project from a set of predefined labels.
### Generate GitLab's predefined label set
## Default labels
![Generate new labels](img/labels_generate.png)
The very first time you visit the labels area, it's gonna be empty. In that
case, it's possible to populate the labels for your project from a set of
predefined labels.
Click the link to 'Generate a default set of labels' and GitLab will
generate a set of predefined labels for you. There are 8 default generated labels
in total and you can see them in the screenshot below.
![Default generated labels](img/labels_default.png)
generate them for you. There are 8 default generated labels in total:
---
- bug
- confirmed
- critical
- discussion
- documentation
- enhancement
- suggestion
- support
## Labels Overview
......@@ -102,30 +102,25 @@ If you work on a large or popular project, try subscribing only to the labels
that are relevant to you. You’ll notice it’ll be much easier to focus on what’s
important.
## Create a new label right from the issue tracker
> Introduced in GitLab 8.6.
## Create a new label when inside an issue
There are times when you are already in the issue tracker searching for a
There are times when you are already inside an issue searching to assign a
label, only to realize it doesn't exist. Instead of going to the **Labels**
page and being distracted from your original purpose, you can create new
labels on the fly.
Select **Create new** from the labels dropdown list, provide a name, pick a
color and hit **Create**.
Expand the issue sidebar and select **Create new label** from the labels dropdown
list. Provide a name, pick a color and hit **Create**. The new label will be
ready to used right away!
![Create new label on the fly](img/labels_new_label_on_the_fly_create.png)
![New label on the fly](img/labels_new_label_on_the_fly.png)
## Assigning labels to issues and merge requests
There are generally two ways to assign a label to an issue or merge request.
You can assign a label when you first create or edit an issue or merge request.
![Assign label in new issue](img/labels_assign_label_in_new_issue.png)
---
The first one is to assign a label when you first create or edit an issue or
merge request.
The second way is by using the right sidebar when inside an issue or merge
request. Expand it and hit **Edit** in the labels area. Start typing the name
......
......@@ -2,24 +2,19 @@
> [Introduced][ce-3514] in GitLab 8.7.
---
GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
with introducing a **Cherry-pick** button in Merge Requests and commit details.
with introducing a **Cherry-pick** button in merge requests and commit details.
## Cherry-picking a Merge Request
## Cherry-picking a merge request
After the Merge Request has been merged, a **Cherry-pick** button will be available
to cherry-pick the changes introduced by that Merge Request:
After the merge request has been merged, a **Cherry-pick** button will be available
to cherry-pick the changes introduced by that merge request.
![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
---
You can cherry-pick the changes directly into the selected branch or you can opt to
create a new Merge Request with the cherry-pick changes:
![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
After you click that button, a modal will appear where you can choose to
cherry-pick the changes directly into the selected branch or you can opt to
create a new merge request with the cherry-pick changes
## Cherry-picking a Commit
......@@ -27,15 +22,9 @@ You can cherry-pick a Commit from the Commit details page:
![Cherry-pick commit](img/cherry_pick_changes_commit.png)
---
Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
directly into the target branch or create a new Merge Request to cherry-pick the
changes:
![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
---
Similar to cherry-picking a merge request, you can opt to cherry-pick the changes
directly into the target branch or create a new merge request to cherry-pick the
changes.
Please note that when cherry-picking merge commits, the mainline will always be the
first parent. If you want to use a different mainline then you need to do that
......
......@@ -3,6 +3,8 @@
Merge requests allow you to exchange changes you made to source code and
collaborate with other people on the same project.
![Merge request view](img/merge_request.png)
## Overview
A Merge Request (**MR**) is the basis of GitLab as a code collaboration
......
......@@ -2,51 +2,39 @@
> [Introduced][ce-1990] in GitLab 8.5.
---
GitLab implements Git's powerful feature to [revert any commit][git-revert]
with introducing a **Revert** button in Merge Requests and commit details.
with introducing a **Revert** button in merge requests and commit details.
## Reverting a Merge Request
_**Note:** The **Revert** button will only be available for Merge Requests
created since GitLab 8.5. However, you can still revert a Merge Request
by reverting the merge commit from the list of Commits page._
NOTE: **Note:**
The **Revert** button will only be available for merge requests
created since GitLab 8.5. However, you can still revert a merge request
by reverting the merge commit from the list of Commits page.
After the Merge Request has been merged, a **Revert** button will be available
to revert the changes introduced by that Merge Request:
![Revert Merge Request](img/revert_changes_mr.png)
---
You can revert the changes directly into the selected branch or you can opt to
create a new Merge Request with the revert changes:
to revert the changes introduced by that merge request.
![Revert Merge Request modal](img/revert_changes_mr_modal.png)
![Revert Merge Request](img/cherry_pick_changes_mr.png)
---
After you click that button, a modal will appear where you can choose to
revert the changes directly into the selected branch or you can opt to
create a new merge request with the revert changes.
After the Merge Request has been reverted, the **Revert** button will not be
After the merge request has been reverted, the **Revert** button will not be
available anymore.
## Reverting a Commit
You can revert a Commit from the Commit details page:
![Revert commit](img/revert_changes_commit.png)
---
Similar to reverting a Merge Request, you can opt to revert the changes
directly into the target branch or create a new Merge Request to revert the
changes:
![Revert commit modal](img/revert_changes_commit_modal.png)
![Revert commit](img/cherry_pick_changes_commit.png)
---
Similar to reverting a merge request, you can opt to revert the changes
directly into the target branch or create a new merge request to revert the
changes.
After the Commit has been reverted, the **Revert** button will not be available
After the commit has been reverted, the **Revert** button will not be available
anymore.
Please note that when reverting merge commits, the mainline will always be the
......
......@@ -26,7 +26,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I fork to my namespace' do
page.within '.fork-namespaces' do
page.within '.fork-thumbnail-container' do
click_link current_user.name
end
end
......
......@@ -53,14 +53,15 @@ module Gitlab
# Rugged repo object
attr_reader :rugged
attr_reader :storage, :gl_repository, :relative_path
attr_reader :storage, :gl_repository, :relative_path, :gitaly_resolver
# 'path' must be the path to a _bare_ git repository, e.g.
# /path/to/my-repo.git
# This initializer method is only used on the client side (gitlab-ce).
# Gitaly-ruby uses a different initializer.
def initialize(storage, relative_path, gl_repository)
@storage = storage
@relative_path = relative_path
@gl_repository = gl_repository
@gitaly_resolver = Gitlab::GitalyClient
storage_path = Gitlab.config.repositories.storages[@storage]['path']
@path = File.join(storage_path, @relative_path)
......@@ -676,7 +677,13 @@ module Gitlab
end
def rm_branch(branch_name, user:)
OperationService.new(user, self).rm_branch(find_branch(branch_name))
gitaly_migrate(:operation_user_delete_branch) do |is_enabled|
if is_enabled
gitaly_operations_client.user_delete_branch(branch_name, user)
else
OperationService.new(user, self).rm_branch(find_branch(branch_name))
end
end
end
def rm_tag(tag_name, user:)
......@@ -981,9 +988,9 @@ module Gitlab
def with_repo_tmp_commit(start_repository, start_branch_name, sha)
tmp_ref = fetch_ref(
start_repository.path,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
"refs/tmp/#{SecureRandom.hex}/head"
start_repository,
source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
target_ref: "refs/tmp/#{SecureRandom.hex}/head"
)
yield commit(sha)
......@@ -1015,13 +1022,27 @@ module Gitlab
end
end
def write_ref(ref_path, sha)
rugged.references.create(ref_path, sha, force: true)
def write_ref(ref_path, ref)
raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ')
raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00")
command = [Gitlab.config.git.bin_path] + %w[update-ref --stdin -z]
input = "update #{ref_path}\x00#{ref}\x00\x00"
output, status = circuit_breaker.perform do
popen(command, path) { |stdin| stdin.write(input) }
end
raise GitError, output unless status.zero?
end
def fetch_ref(source_path, source_ref, target_ref)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
message, status = run_git(args)
def fetch_ref(source_repository, source_ref:, target_ref:)
message, status = GitalyClient.migrate(:fetch_ref) do |is_enabled|
if is_enabled
gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref)
else
local_fetch_ref(source_repository.path, source_ref: source_ref, target_ref: target_ref)
end
end
# Make sure ref was created, and raise Rugged::ReferenceError when not
raise Rugged::ReferenceError, message if status != 0
......@@ -1030,9 +1051,9 @@ module Gitlab
end
# Refactoring aid; allows us to copy code from app/models/repository.rb
def run_git(args)
def run_git(args, env: {})
circuit_breaker.perform do
popen([Gitlab.config.git.bin_path, *args], path)
popen([Gitlab.config.git.bin_path, *args], path, env)
end
end
......@@ -1489,9 +1510,33 @@ module Gitlab
OperationService.new(user, self).add_branch(branch_name, target_object.oid)
find_branch(branch_name)
rescue Rugged::ReferenceError
rescue Rugged::ReferenceError => ex
raise InvalidRef, ex
end
def local_fetch_ref(source_path, source_ref:, target_ref:)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args)
end
def gitaly_fetch_ref(source_repository, source_ref:, target_ref:)
gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh'))
gitaly_address = gitaly_resolver.address(source_repository.storage)
gitaly_token = gitaly_resolver.token(source_repository.storage)
request = Gitaly::SSHUploadPackRequest.new(repository: source_repository.gitaly_repository)
env = {
'GITALY_ADDRESS' => gitaly_address,
'GITALY_PAYLOAD' => request.to_json,
'GITALY_WD' => Dir.pwd,
'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack"
}
env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present?
args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref})
run_git(args, env: env)
end
end
end
end
......@@ -31,7 +31,7 @@ module Gitlab
output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
unless status.zero?
raise "Got a non-zero exit code while calling out `#{args.join(' ')}`."
raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
end
output.split("\n")
......
......@@ -60,6 +60,20 @@ module Gitlab
target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit)
end
def user_delete_branch(branch_name, user)
request = Gitaly::UserDeleteBranchRequest.new(
repository: @gitaly_repo,
branch_name: GitalyClient.encode(branch_name),
user: Util.gitaly_user(user)
)
response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request)
if pre_receive_error = response.pre_receive_error.presence
raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
end
end
end
end
end
......@@ -25,7 +25,7 @@ module Gitlab
Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\
"#{MAX_RSS}"
Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"\
Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']} "\
"in #{GRACE_TIME} seconds"
sleep(GRACE_TIME)
......
......@@ -12,8 +12,9 @@ module Gitlab
#
# Project.where("id IN (#{sql})")
class Union
def initialize(relations)
def initialize(relations, remove_duplicates: true)
@relations = relations
@remove_duplicates = remove_duplicates
end
def to_sql
......@@ -25,7 +26,11 @@ module Gitlab
@relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
end
fragments.join("\nUNION\n")
fragments.join("\n#{union_keyword}\n")
end
def union_keyword
@remove_duplicates ? 'UNION' : 'UNION ALL'
end
end
end
......
......@@ -3,8 +3,8 @@ namespace :gitlab do
desc 'GitLab | Assets | Compile all frontend assets'
task compile: [
'yarn:check',
'rake:assets:precompile',
'gettext:po_to_json',
'rake:assets:precompile',
'webpack:compile',
'fix_urls'
]
......
source 'https://rubygems.org'
gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'capybara', '~> 2.12.1'
gem 'capybara-screenshot', '~> 1.0.14'
gem 'rake', '~> 12.0.0'
......
......@@ -3,6 +3,7 @@ GEM
specs:
addressable (2.5.0)
public_suffix (~> 2.0, >= 2.0.2)
byebug (9.0.6)
capybara (2.12.1)
addressable
mime-types (>= 1.16)
......@@ -13,22 +14,27 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
capybara-webkit (1.12.0)
capybara (>= 2.3.0, < 2.13.0)
json
childprocess (0.7.0)
ffi (~> 1.0, >= 1.0.11)
coderay (1.1.1)
diff-lcs (1.3)
ffi (1.9.18)
json (2.0.3)
launchy (2.4.3)
addressable (~> 2.3)
method_source (0.8.2)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_portile2 (2.1.0)
nokogiri (1.7.0.1)
mini_portile2 (~> 2.1.0)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
pry-byebug (3.4.2)
byebug (~> 9.0)
pry (~> 0.10)
public_suffix (2.0.5)
rack (2.0.1)
rack-test (0.6.3)
......@@ -52,6 +58,7 @@ GEM
childprocess (~> 0.5)
rubyzip (~> 1.0)
websocket (~> 1.0)
slop (3.6.0)
websocket (1.2.4)
xpath (2.0.0)
nokogiri (~> 1.3)
......@@ -62,7 +69,7 @@ PLATFORMS
DEPENDENCIES
capybara (~> 2.12.1)
capybara-screenshot (~> 1.0.14)
capybara-webkit (~> 1.12.0)
pry-byebug (~> 3.4.1)
rake (~> 12.0.0)
rspec (~> 3.5)
selenium-webdriver (~> 2.53)
......
......@@ -4,8 +4,6 @@ module QA
class Menu < Page::Base
def go_to_license
link = find_link 'License'
# Click space to scroll this link into the view
link.send_keys(:space)
link.click
end
end
......
......@@ -43,8 +43,7 @@ module QA
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
'chromeOptions' => {
'binary' => '/usr/bin/google-chrome-stable',
'args' => %w[headless no-sandbox disable-gpu window-size=1280,1024]
'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680]
}
)
......
......@@ -96,18 +96,6 @@ describe Projects::MergeRequestsController do
expect(response).to match_response_schema('entities/merge_request')
end
end
context 'number of queries', :request_store do
it 'verifies number of queries' do
# pre-create objects
merge_request
recorded = ActiveRecord::QueryRecorder.new { go(format: :json) }
expect(recorded.count).to be_within(5).of(30)
expect(recorded.cached_count).to eq(0)
end
end
end
describe "as diff" do
......
......@@ -42,6 +42,13 @@ describe Projects::Registry::RepositoriesController do
expect { go_to_index }.to change { ContainerRepository.all.count }.by(1)
expect(ContainerRepository.first).to be_root_repository
end
it 'json has a list of projects' do
go_to_index(format: :json)
expect(response).to have_http_status(:ok)
expect(response).to match_response_schema('registry/repositories')
end
end
context 'when there are no tags for this repository' do
......@@ -58,6 +65,31 @@ describe Projects::Registry::RepositoriesController do
it 'does not ensure root container repository' do
expect { go_to_index }.not_to change { ContainerRepository.all.count }
end
it 'responds with json if asked' do
go_to_index(format: :json)
expect(response).to have_http_status(:ok)
expect(json_response).to be_kind_of(Array)
end
end
end
end
describe 'DELETE destroy' do
context 'when root container repository exists' do
let!(:repository) do
create(:container_repository, :root, project: project)
end
before do
stub_container_registry_tags(repository: :any, tags: [])
end
it 'deletes a repository' do
expect { delete_repository(repository) }.to change { ContainerRepository.all.count }.by(-1)
expect(response).to have_http_status(:no_content)
end
end
end
......@@ -77,8 +109,16 @@ describe Projects::Registry::RepositoriesController do
end
end
def go_to_index
def go_to_index(format: :html)
get :index, namespace_id: project.namespace,
project_id: project
project_id: project,
format: format
end
def delete_repository(repository)
delete :destroy, namespace_id: project.namespace,
project_id: project,
id: repository,
format: :json
end
end
......@@ -4,24 +4,83 @@ describe Projects::Registry::TagsController do
let(:user) { create(:user) }
let(:project) { create(:project, :private) }
let(:repository) do
create(:container_repository, name: 'image', project: project)
end
before do
sign_in(user)
stub_container_registry_config(enabled: true)
end
context 'when user has access to registry' do
describe 'GET index' do
let(:tags) do
Array.new(40) { |i| "tag#{i}" }
end
before do
project.add_developer(user)
stub_container_registry_tags(repository: /image/, tags: tags)
end
describe 'POST destroy' do
context 'when user can control the registry' do
before do
project.add_developer(user)
end
it 'receive a list of tags' do
get_tags
expect(response).to have_http_status(:ok)
expect(response).to match_response_schema('registry/tags')
expect(response).to include_pagination_headers
end
end
context 'when user can read the registry' do
before do
project.add_reporter(user)
end
it 'receive a list of tags' do
get_tags
expect(response).to have_http_status(:ok)
expect(response).to match_response_schema('registry/tags')
expect(response).to include_pagination_headers
end
end
context 'when user does not have access to registry' do
before do
project.add_guest(user)
end
it 'does not receive a list of tags' do
get_tags
expect(response).to have_http_status(:not_found)
end
end
private
def get_tags
get :index, namespace_id: project.namespace,
project_id: project,
repository_id: repository,
format: :json
end
end
describe 'POST destroy' do
context 'when user has access to registry' do
before do
project.add_developer(user)
end
context 'when there is matching tag present' do
before do
stub_container_registry_tags(repository: /image/, tags: %w[rc1 test.])
end
let(:repository) do
create(:container_repository, name: 'image', project: project)
stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.])
end
it 'makes it possible to delete regular tag' do
......@@ -37,12 +96,15 @@ describe Projects::Registry::TagsController do
end
end
end
end
def destroy_tag(name)
post :destroy, namespace_id: project.namespace,
project_id: project,
repository_id: repository,
id: name
private
def destroy_tag(name)
post :destroy, namespace_id: project.namespace,
project_id: project,
repository_id: repository,
id: name,
format: :json
end
end
end
......@@ -12,7 +12,7 @@ FactoryGirl.define do
deployment.project ||= deployment.environment.project
unless deployment.project.repository_exists?
allow(deployment.project.repository).to receive(:fetch_ref)
allow(deployment.project.repository).to receive(:create_ref)
end
end
end
......
require 'spec_helper'
describe "Container Registry" do
describe "Container Registry", js: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
......@@ -41,16 +41,19 @@ describe "Container Registry" do
expect_any_instance_of(ContainerRepository)
.to receive(:delete_tags!).and_return(true)
click_on 'Remove repository'
click_on(class: 'js-remove-repo')
end
scenario 'user removes a specific tag from container repository' do
visit_container_registry
find('.js-toggle-repo').trigger('click')
wait_for_requests
expect_any_instance_of(ContainerRegistry::Tag)
.to receive(:delete).and_return(true)
click_on 'Remove tag'
click_on(class: 'js-delete-registry')
end
end
......
......@@ -29,7 +29,7 @@ feature 'Download buttons in branches page' do
describe 'when checking branches' do
context 'with artifacts' do
before do
visit project_branches_path(project)
visit project_branches_path(project, search: 'binary-encoding')
end
scenario 'shows download artifacts button' do
......
......@@ -5,12 +5,6 @@ describe 'Branches' do
let(:project) { create(:project, :public, :repository) }
let(:repository) { project.repository }
def set_protected_branch_name(branch_name)
find(".js-protected-branch-select").click
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
context 'logged in as developer' do
before do
sign_in(user)
......@@ -18,12 +12,10 @@ describe 'Branches' do
end
describe 'Initial branches page' do
it 'shows all the branches' do
it 'shows all the branches sorted by last updated by default' do
visit project_branches_path(project)
repository.branches_sorted_by(:name).first(20).each do |branch|
expect(page).to have_content("#{branch.name}")
end
expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc))
end
it 'sorts the branches by name' do
......@@ -32,22 +24,7 @@ describe 'Branches' do
click_button "Last updated" # Open sorting dropdown
click_link "Name"
sorted = repository.branches_sorted_by(:name).first(20).map do |branch|
Regexp.escape(branch.name)
end
expect(page).to have_content(/#{sorted.join(".*")}/)
end
it 'sorts the branches by last updated' do
visit project_branches_path(project)
click_button "Last updated" # Open sorting dropdown
click_link "Last updated"
sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch|
Regexp.escape(branch.name)
end
expect(page).to have_content(/#{sorted.join(".*")}/)
expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name))
end
it 'sorts the branches by oldest updated' do
......@@ -56,10 +33,7 @@ describe 'Branches' do
click_button "Last updated" # Open sorting dropdown
click_link "Oldest updated"
sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch|
Regexp.escape(branch.name)
end
expect(page).to have_content(/#{sorted.join(".*")}/)
expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc))
end
it 'avoids a N+1 query in branches index' do
......@@ -99,28 +73,6 @@ describe 'Branches' do
expect(find('.all-branches')).to have_selector('li', count: 0)
end
end
describe 'Delete protected branch' do
before do
project.add_user(user, :master)
visit project_protected_branches_path(project)
set_protected_branch_name('fix')
click_on "Protect"
within(".protected-branches-list") { expect(page).to have_content('fix') }
expect(ProtectedBranch.count).to eq(1)
project.add_user(user, :developer)
end
it 'does not allow devleoper to removes protected branch', js: true do
visit project_branches_path(project)
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
expect(page).to have_css('.btn-remove.disabled')
end
end
end
context 'logged in as master' do
......@@ -136,37 +88,6 @@ describe 'Branches' do
expect(page).to have_content("Protected branches can be managed in project settings")
end
end
describe 'Delete protected branch' do
before do
visit project_protected_branches_path(project)
set_protected_branch_name('fix')
click_on "Protect"
within(".protected-branches-list") { expect(page).to have_content('fix') }
expect(ProtectedBranch.count).to eq(1)
end
it 'removes branch after modal confirmation', js: true do
visit project_branches_path(project)
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
page.find('[data-target="#modal-delete-branch"]').trigger(:click)
expect(page).to have_css('.js-delete-branch[disabled]')
fill_in 'delete_branch_input', with: 'fix'
click_link 'Delete protected branch'
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
expect(page).to have_content('No branches to show')
end
end
end
context 'logged out' do
......@@ -180,4 +101,13 @@ describe 'Branches' do
end
end
end
def sorted_branches(repository, count:, sort_by:)
sorted_branches =
repository.branches_sorted_by(sort_by).first(count).map do |branch|
Regexp.escape(branch.name)
end
Regexp.new(sorted_branches.join('.*'))
end
end
require 'spec_helper'
feature 'Protected Branches', js: true do
let(:user) { create(:user, :admin) }
feature 'Protected Branches', :js do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
before do
sign_in(user)
end
context 'logged in as developer' do
before do
project.add_developer(user)
sign_in(user)
end
def set_protected_branch_name(branch_name)
find(".js-protected-branch-select").trigger('click')
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
describe 'Delete protected branch' do
before do
create(:protected_branch, project: project, name: 'fix')
expect(ProtectedBranch.count).to eq(1)
end
it 'does not allow developer to removes protected branch' do
visit project_branches_path(project)
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
describe "explicit protected branches" do
it "allows creating explicit protected branches" do
visit project_protected_branches_path(project)
set_protected_branch_name('some-branch')
click_on "Protect"
expect(page).to have_css('.btn-remove.disabled')
end
end
end
within(".protected-branches-list") { expect(page).to have_content('some-branch') }
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.name).to eq('some-branch')
context 'logged in as master' do
before do
project.add_master(user)
sign_in(user)
end
it "displays the last commit on the matching branch if it exists" do
commit = create(:commit, project: project)
project.repository.add_branch(user, 'some-branch', commit.id)
describe 'Delete protected branch' do
before do
create(:protected_branch, project: project, name: 'fix')
expect(ProtectedBranch.count).to eq(1)
end
visit project_protected_branches_path(project)
set_protected_branch_name('some-branch')
click_on "Protect"
it 'removes branch after modal confirmation' do
visit project_branches_path(project)
within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) }
end
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
it "displays an error message if the named branch does not exist" do
visit project_protected_branches_path(project)
set_protected_branch_name('some-branch')
click_on "Protect"
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
page.find('[data-target="#modal-delete-branch"]').trigger(:click)
within(".protected-branches-list") { expect(page).to have_content('branch was removed') }
expect(page).to have_css('.js-delete-branch[disabled]')
fill_in 'delete_branch_input', with: 'fix'
click_link 'Delete protected branch'
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
expect(page).to have_content('No branches to show')
end
end
end
describe "wildcard protected branches" do
it "allows creating protected branches with a wildcard" do
visit project_protected_branches_path(project)
set_protected_branch_name('*-stable')
click_on "Protect"
within(".protected-branches-list") { expect(page).to have_content('*-stable') }
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.name).to eq('*-stable')
context 'logged in as admin' do
before do
sign_in(admin)
end
it "displays the number of matching branches" do
project.repository.add_branch(user, 'production-stable', 'master')
project.repository.add_branch(user, 'staging-stable', 'master')
describe "explicit protected branches" do
it "allows creating explicit protected branches" do
visit project_protected_branches_path(project)
set_protected_branch_name('some-branch')
click_on "Protect"
visit project_protected_branches_path(project)
set_protected_branch_name('*-stable')
click_on "Protect"
within(".protected-branches-list") { expect(page).to have_content('some-branch') }
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.name).to eq('some-branch')
end
within(".protected-branches-list") { expect(page).to have_content("2 matching branches") }
it "displays the last commit on the matching branch if it exists" do
commit = create(:commit, project: project)
project.repository.add_branch(admin, 'some-branch', commit.id)
visit project_protected_branches_path(project)
set_protected_branch_name('some-branch')
click_on "Protect"
within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) }
end
it "displays an error message if the named branch does not exist" do
visit project_protected_branches_path(project)
set_protected_branch_name('some-branch')
click_on "Protect"
within(".protected-branches-list") { expect(page).to have_content('branch was removed') }
end
end
it "displays all the branches matching the wildcard" do
project.repository.add_branch(user, 'production-stable', 'master')
project.repository.add_branch(user, 'staging-stable', 'master')
project.repository.add_branch(user, 'development', 'master')
describe "wildcard protected branches" do
it "allows creating protected branches with a wildcard" do
visit project_protected_branches_path(project)
set_protected_branch_name('*-stable')
click_on "Protect"
within(".protected-branches-list") { expect(page).to have_content('*-stable') }
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.name).to eq('*-stable')
end
visit project_protected_branches_path(project)
set_protected_branch_name('*-stable')
click_on "Protect"
it "displays the number of matching branches" do
project.repository.add_branch(admin, 'production-stable', 'master')
project.repository.add_branch(admin, 'staging-stable', 'master')
visit project_protected_branches_path(project)
click_on "2 matching branches"
visit project_protected_branches_path(project)
set_protected_branch_name('*-stable')
click_on "Protect"
within(".protected-branches-list") do
expect(page).to have_content("production-stable")
expect(page).to have_content("staging-stable")
expect(page).not_to have_content("development")
within(".protected-branches-list") { expect(page).to have_content("2 matching branches") }
end
it "displays all the branches matching the wildcard" do
project.repository.add_branch(admin, 'production-stable', 'master')
project.repository.add_branch(admin, 'staging-stable', 'master')
project.repository.add_branch(admin, 'development', 'master')
visit project_protected_branches_path(project)
set_protected_branch_name('*-stable')
click_on "Protect"
visit project_protected_branches_path(project)
click_on "2 matching branches"
within(".protected-branches-list") do
expect(page).to have_content("production-stable")
expect(page).to have_content("staging-stable")
expect(page).not_to have_content("development")
end
end
end
describe "access control" do
include_examples "protected branches > access control > CE"
end
end
describe "access control" do
include_examples "protected branches > access control > CE"
def set_protected_branch_name(branch_name)
find(".js-protected-branch-select").trigger('click')
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
end
......@@ -93,7 +93,7 @@
"merge_commit_message_with_description": { "type": "string" },
"diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" },
"remove_wip_path": { "type": "string" },
"remove_wip_path": { "type": ["string", "null"] },
"commits_count": { "type": "integer" },
"remove_source_branch": { "type": ["boolean", "null"] },
"merge_ongoing": { "type": "boolean" },
......
{
"type": "array",
"items": {
"$ref": "repository.json"
}
}
{
"type": "object",
"required" : [
"id",
"path",
"location",
"tags_path"
],
"properties" : {
"id": {
"type": "integer"
},
"path": {
"type": "string"
},
"location": {
"type": "string"
},
"tags_path": {
"type": "string"
},
"destroy_path": {
"type": "string"
}
},
"additionalProperties": false
}
{
"type": "object",
"required" : [
"name",
"location"
],
"properties" : {
"name": {
"type": "string"
},
"location": {
"type": "string"
},
"revision": {
"type": "string"
},
"total_size": {
"type": "integer"
},
"created_at": {
"type": "date"
},
"destroy_path": {
"type": "string"
}
},
"additionalProperties": false
}
{
"type": "array",
"items": {
"$ref": "tag.json"
}
}
......@@ -41,6 +41,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
remove_repository(project)
end
it 'merge_requests/merge_request_of_current_user.html.raw' do |example|
merge_request.update(author: admin)
render_merge_request(example.description, merge_request)
end
it 'merge_requests/merge_request_with_task_list.html.raw' do |example|
create(:ci_build, :pending, pipeline: pipeline)
......
......@@ -58,5 +58,44 @@ import IssuablesHelper from '~/helpers/issuables_helper';
expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
});
});
describe('hideCloseButton', () => {
describe('merge request of another user', () => {
beforeEach(() => {
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
this.el = document.querySelector('.merge-request .issuable-actions');
const merge = new MergeRequest();
merge.hideCloseButton();
});
it('hides the dropdown close item and selects the next item', () => {
const closeItem = this.el.querySelector('li.close-item');
const smallCloseItem = this.el.querySelector('.js-close-item');
const reportItem = this.el.querySelector('li.report-item');
expect(closeItem).toHaveClass('hidden');
expect(smallCloseItem).toHaveClass('hidden');
expect(reportItem).toHaveClass('droplab-item-selected');
expect(reportItem).not.toHaveClass('hidden');
});
});
describe('merge request of current_user', () => {
beforeEach(() => {
loadFixtures('merge_requests/merge_request_of_current_user.html.raw');
this.el = document.querySelector('.merge-request .issuable-actions');
const merge = new MergeRequest();
merge.hideCloseButton();
});
it('hides the close button', () => {
const closeButton = this.el.querySelector('.btn-close');
const smallCloseItem = this.el.querySelector('.js-close-item');
expect(closeButton).toHaveClass('hidden');
expect(smallCloseItem).toHaveClass('hidden');
});
});
});
});
}).call(window);
......@@ -33,6 +33,30 @@ describe('issue_comment_form component', () => {
expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path);
});
describe('handleSave', () => {
it('should request to save note when note is entered', () => {
vm.note = 'hello world';
spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
spyOn(vm, 'resizeTextarea');
spyOn(vm, 'stopPolling');
vm.handleSave();
expect(vm.isSubmitting).toEqual(true);
expect(vm.note).toEqual('');
expect(vm.saveNote).toHaveBeenCalled();
expect(vm.stopPolling).toHaveBeenCalled();
expect(vm.resizeTextarea).toHaveBeenCalled();
});
it('should toggle issue state when no note', () => {
spyOn(vm, 'toggleIssueState');
vm.handleSave();
expect(vm.toggleIssueState).toHaveBeenCalled();
});
});
describe('textarea', () => {
it('should render textarea with placeholder', () => {
expect(
......@@ -40,6 +64,22 @@ describe('issue_comment_form component', () => {
).toEqual('Write a comment or drag your files here...');
});
it('should make textarea disabled while requesting', (done) => {
const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button'));
vm.note = 'hello world';
spyOn(vm, 'stopPolling');
spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton.
$submitButton.trigger('click');
vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea.
expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy();
done();
});
});
});
it('should support quick actions', () => {
expect(
vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'),
......
import * as actions from '~/notes/stores/actions';
import testAction from './helpers';
import testAction from '../../helpers/vuex_action_helper';
import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
describe('Actions Notes Store', () => {
......
import Vue from 'vue';
import registry from '~/registry/components/app.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
import { reposServerResponse } from '../mock_data';
describe('Registry List', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(registry);
});
afterEach(() => {
vm.$destroy();
});
describe('with data', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
vm = mountComponent(Component, { endpoint: 'foo' });
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should render a list of repos', (done) => {
setTimeout(() => {
expect(vm.$store.state.repos.length).toEqual(reposServerResponse.length);
Vue.nextTick(() => {
expect(
vm.$el.querySelectorAll('.container-image').length,
).toEqual(reposServerResponse.length);
done();
});
}, 0);
});
describe('delete repository', () => {
it('should be possible to delete a repo', (done) => {
setTimeout(() => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-head .js-remove-repo')).toBeDefined();
done();
});
}, 0);
});
});
describe('toggle repository', () => {
it('should open the container', (done) => {
setTimeout(() => {
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual('fa fa-chevron-up');
done();
});
});
}, 0);
});
});
});
describe('without data', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
vm = mountComponent(Component, { endpoint: 'foo' });
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should render empty message', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('p').textContent.trim(),
).toEqual('No container images stored for this project. Add one by following the instructions above.');
done();
}, 0);
});
});
describe('while loading data', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
vm = mountComponent(Component, { endpoint: 'foo' });
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should render a loading spinner', (done) => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null);
done();
});
});
});
});
import Vue from 'vue';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import store from '~/registry/stores';
import { repoPropsData } from '../mock_data';
describe('collapsible registry container', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(collapsibleComponent);
vm = new Component({
store,
propsData: {
repo: repoPropsData,
},
}).$mount();
});
afterEach(() => {
vm.$destroy();
});
describe('toggle', () => {
it('should be closed by default', () => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right');
});
it('should be open when user clicks on closed repo', (done) => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBeDefined();
expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-up');
done();
});
});
it('should be closed when the user clicks on an opened repo', (done) => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.$el.querySelector('.container-image-head i').className).toEqual('fa fa-chevron-right');
done();
});
});
});
});
describe('delete repo', () => {
it('should be possible to delete a repo', () => {
expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined();
});
});
});
import Vue from 'vue';
import tableRegistry from '~/registry/components/table_registry.vue';
import store from '~/registry/stores';
import { repoPropsData } from '../mock_data';
describe('table registry', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(tableRegistry);
vm = new Component({
store,
propsData: {
repo: repoPropsData,
},
}).$mount();
});
afterEach(() => {
vm.$destroy();
});
it('should render a table with the registry list', () => {
expect(
vm.$el.querySelectorAll('table tbody tr').length,
).toEqual(repoPropsData.list.length);
});
it('should render registry tag', () => {
const textRendered = vm.$el.querySelector('.table tbody tr').textContent.trim().replace(/\s\s+/g, ' ');
expect(textRendered).toContain(repoPropsData.list[0].tag);
expect(textRendered).toContain(repoPropsData.list[0].shortRevision);
expect(textRendered).toContain(repoPropsData.list[0].layers);
expect(textRendered).toContain(repoPropsData.list[0].size);
});
it('should be possible to delete a registry', () => {
expect(
vm.$el.querySelector('.table tbody tr .js-delete-registry'),
).toBeDefined();
});
describe('pagination', () => {
it('should be possible to change the page', () => {
expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
});
});
});
import * as getters from '~/registry/stores/getters';
describe('Getters Registry Store', () => {
let state;
beforeEach(() => {
state = {
isLoading: false,
endpoint: '/root/empty-project/container_registry.json',
repos: [{
canDelete: true,
destroyPath: 'bar',
id: '134',
isLoading: false,
list: [],
location: 'foo',
name: 'gitlab-org/omnibus-gitlab/foo',
tagsPath: 'foo',
}, {
canDelete: true,
destroyPath: 'bar',
id: '123',
isLoading: false,
list: [],
location: 'foo',
name: 'gitlab-org/omnibus-gitlab',
tagsPath: 'foo',
}],
};
});
describe('isLoading', () => {
it('should return the isLoading property', () => {
expect(getters.isLoading(state)).toEqual(state.isLoading);
});
});
describe('repos', () => {
it('should return the repos', () => {
expect(getters.repos(state)).toEqual(state.repos);
});
});
});
export const defaultState = {
isLoading: false,
endpoint: '',
repos: [],
};
export const reposServerResponse = [
{
destroy_path: 'path',
id: '123',
location: 'location',
path: 'foo',
tags_path: 'tags_path',
},
{
destroy_path: 'path_',
id: '456',
location: 'location_',
path: 'bar',
tags_path: 'tags_path_',
},
];
export const registryServerResponse = [
{
name: 'centos7',
short_revision: 'b118ab5b0',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
size: 679,
layers: 19,
location: 'location',
created_at: 1505828744434,
destroy_path: 'path_',
},
{
name: 'centos6',
short_revision: 'b118ab5b0',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
size: 679,
layers: 19,
location: 'location',
created_at: 1505828744434,
}];
export const parsedReposServerResponse = [
{
canDelete: true,
destroyPath: reposServerResponse[0].destroy_path,
id: reposServerResponse[0].id,
isLoading: false,
list: [],
location: reposServerResponse[0].location,
name: reposServerResponse[0].path,
tagsPath: reposServerResponse[0].tags_path,
},
{
canDelete: true,
destroyPath: reposServerResponse[1].destroy_path,
id: reposServerResponse[1].id,
isLoading: false,
list: [],
location: reposServerResponse[1].location,
name: reposServerResponse[1].path,
tagsPath: reposServerResponse[1].tags_path,
},
];
export const parsedRegistryServerResponse = [
{
tag: registryServerResponse[0].name,
revision: registryServerResponse[0].revision,
shortRevision: registryServerResponse[0].short_revision,
size: registryServerResponse[0].size,
layers: registryServerResponse[0].layers,
location: registryServerResponse[0].location,
createdAt: registryServerResponse[0].created_at,
destroyPath: registryServerResponse[0].destroy_path,
canDelete: true,
},
{
tag: registryServerResponse[1].name,
revision: registryServerResponse[1].revision,
shortRevision: registryServerResponse[1].short_revision,
size: registryServerResponse[1].size,
layers: registryServerResponse[1].layers,
location: registryServerResponse[1].location,
createdAt: registryServerResponse[1].created_at,
destroyPath: registryServerResponse[1].destroy_path,
canDelete: false,
},
];
export const repoPropsData = {
canDelete: true,
destroyPath: 'path',
id: '123',
isLoading: false,
list: [
{
tag: 'centos6',
revision: 'b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43',
shortRevision: 'b118ab5b0',
size: 19,
layers: 10,
location: 'location',
createdAt: 1505828744434,
destroyPath: 'path',
canDelete: true,
},
],
location: 'location',
name: 'foo',
tagsPath: 'path',
pagination: {
perPage: 5,
page: 1,
total: 13,
totalPages: 1,
nextPage: null,
previousPage: null,
},
};
import Vue from 'vue';
import VueResource from 'vue-resource';
import _ from 'underscore';
import * as actions from '~/registry/stores/actions';
import * as types from '~/registry/stores/mutation_types';
import testAction from '../../helpers/vuex_action_helper';
import {
defaultState,
reposServerResponse,
registryServerResponse,
parsedReposServerResponse,
} from '../mock_data';
Vue.use(VueResource);
describe('Actions Registry Store', () => {
let interceptor;
let mockedState;
beforeEach(() => {
mockedState = defaultState;
});
describe('server requests', () => {
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
describe('fetchRepos', () => {
beforeEach(() => {
interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}));
};
Vue.http.interceptors.push(interceptor);
});
it('should set receveived repos', (done) => {
testAction(actions.fetchRepos, null, mockedState, [
{ type: types.TOGGLE_MAIN_LOADING },
{ type: types.SET_REPOS_LIST, payload: reposServerResponse },
], done);
});
});
describe('fetchList', () => {
beforeEach(() => {
interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(registryServerResponse), {
status: 200,
}));
};
Vue.http.interceptors.push(interceptor);
});
it('should set received list', (done) => {
mockedState.repos = parsedReposServerResponse;
testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [
{ type: types.TOGGLE_REGISTRY_LIST_LOADING },
{ type: types.SET_REGISTRY_LIST, payload: registryServerResponse },
], done);
});
});
});
describe('setMainEndpoint', () => {
it('should commit set main endpoint', (done) => {
testAction(actions.setMainEndpoint, 'endpoint', mockedState, [
{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' },
], done);
});
});
describe('toggleLoading', () => {
it('should commit toggle main loading', (done) => {
testAction(actions.toggleLoading, null, mockedState, [
{ type: types.TOGGLE_MAIN_LOADING },
], done);
});
});
});
import mutations from '~/registry/stores/mutations';
import * as types from '~/registry/stores/mutation_types';
import {
defaultState,
reposServerResponse,
registryServerResponse,
parsedReposServerResponse,
parsedRegistryServerResponse,
} from '../mock_data';
describe('Mutations Registry Store', () => {
let mockState;
beforeEach(() => {
mockState = defaultState;
});
describe('SET_MAIN_ENDPOINT', () => {
it('should set the main endpoint', () => {
const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
mutations[types.SET_MAIN_ENDPOINT](mockState, 'foo');
expect(mockState).toEqual(expectedState);
});
});
describe('SET_REPOS_LIST', () => {
it('should set a parsed repository list', () => {
mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
expect(mockState.repos).toEqual(parsedReposServerResponse);
});
});
describe('TOGGLE_MAIN_LOADING', () => {
it('should set a parsed repository list', () => {
mutations[types.TOGGLE_MAIN_LOADING](mockState);
expect(mockState.isLoading).toEqual(true);
});
});
describe('SET_REGISTRY_LIST', () => {
it('should set a list of registries in a specific repository', () => {
mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
mutations[types.SET_REGISTRY_LIST](mockState, {
repo: mockState.repos[0],
resp: registryServerResponse,
headers: {
'x-per-page': 2,
'x-page': 1,
'x-total': 10,
},
});
expect(mockState.repos[0].list).toEqual(parsedRegistryServerResponse);
expect(mockState.repos[0].pagination).toEqual({
perPage: 2,
page: 1,
total: 10,
totalPages: NaN,
nextPage: NaN,
previousPage: NaN,
});
});
});
describe('TOGGLE_REGISTRY_LIST_LOADING', () => {
it('should toggle isLoading property for a specific repository', () => {
mutations[types.SET_REPOS_LIST](mockState, reposServerResponse);
mutations[types.SET_REGISTRY_LIST](mockState, {
repo: mockState.repos[0],
resp: registryServerResponse,
headers: {
'x-per-page': 2,
'x-page': 1,
'x-total': 10,
},
});
mutations[types.TOGGLE_REGISTRY_LIST_LOADING](mockState, mockState.repos[0]);
expect(mockState.repos[0].isLoading).toEqual(true);
});
});
});
......@@ -95,35 +95,84 @@ describe('MRWidgetReadyToMerge', () => {
});
});
describe('status', () => {
it('defaults to success', () => {
vm.mr.pipeline = true;
expect(vm.status).toEqual('success');
});
it('returns failed when MR has CI but also has an unknown status', () => {
vm.mr.hasCI = true;
expect(vm.status).toEqual('failed');
});
it('returns default when MR has no pipeline', () => {
expect(vm.status).toEqual('success');
});
it('returns pending when pipeline is active', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineActive = true;
expect(vm.status).toEqual('pending');
});
it('returns failed when pipeline is failed', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineFailed = true;
expect(vm.status).toEqual('failed');
});
});
describe('mergeButtonClass', () => {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
it('should return default class', () => {
it('defaults to success class', () => {
expect(vm.mergeButtonClass).toEqual(defaultClass);
});
it('returns success class for success status', () => {
vm.mr.pipeline = true;
expect(vm.mergeButtonClass).toEqual(defaultClass);
});
it('should return failed class when MR has CI but also has an unknown status', () => {
it('returns info class for pending status', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineActive = true;
expect(vm.mergeButtonClass).toEqual(inActionClass);
});
it('returns failed class for failed status', () => {
vm.mr.hasCI = true;
expect(vm.mergeButtonClass).toEqual(failedClass);
});
});
it('should return default class when MR has no pipeline', () => {
expect(vm.mergeButtonClass).toEqual(defaultClass);
describe('status icon', () => {
it('defaults to tick icon', () => {
expect(vm.iconClass).toEqual('success');
});
it('should return in action class when pipeline is active', () => {
it('shows tick for success status', () => {
vm.mr.pipeline = true;
expect(vm.iconClass).toEqual('success');
});
it('shows tick for pending status', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineActive = true;
expect(vm.mergeButtonClass).toEqual(inActionClass);
expect(vm.iconClass).toEqual('success');
});
it('should return failed class when pipeline is failed', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineFailed = true;
expect(vm.mergeButtonClass).toEqual(failedClass);
it('shows x for failed status', () => {
vm.mr.hasCI = true;
expect(vm.iconClass).toEqual('failed');
});
it('shows x for merge not allowed', () => {
vm.mr.hasCI = true;
expect(vm.iconClass).toEqual('failed');
});
});
......@@ -177,7 +226,7 @@ describe('MRWidgetReadyToMerge', () => {
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
it('should return true when there vm instance is making request', () => {
it('should return true when the vm instance is making request', () => {
vm.isMakingRequest = true;
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
......
......@@ -1444,6 +1444,51 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
describe '#rm_branch' do
shared_examples "user deleting a branch" do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
let(:user) { create(:user) }
let(:branch_name) { "to-be-deleted-soon" }
before do
project.team << [user, :developer]
repository.create_branch(branch_name)
end
it "removes the branch from the repo" do
repository.rm_branch(branch_name, user: user)
expect(repository.rugged.branches[branch_name]).to be_nil
end
end
context "when Gitaly user_delete_branch is enabled" do
it_behaves_like "user deleting a branch"
end
context "when Gitaly user_delete_branch is disabled", skip_gitaly_mock: true do
it_behaves_like "user deleting a branch"
end
end
describe '#write_ref' do
context 'validations' do
using RSpec::Parameterized::TableSyntax
where(:ref_path, :ref) do
'foo bar' | '123'
'foobar' | "12\x003"
end
with_them do
it 'raises ArgumentError' do
expect { repository.write_ref(ref_path, ref) }.to raise_error(ArgumentError)
end
end
end
end
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
......
......@@ -4,10 +4,10 @@ describe Gitlab::GitalyClient::OperationService do
let(:project) { create(:project) }
let(:repository) { project.repository.raw }
let(:client) { described_class.new(repository) }
let(:user) { create(:user) }
let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) }
describe '#user_create_branch' do
let(:user) { create(:user) }
let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) }
let(:branch_name) { 'new' }
let(:start_point) { 'master' }
let(:request) do
......@@ -52,4 +52,41 @@ describe Gitlab::GitalyClient::OperationService do
end
end
end
describe '#user_delete_branch' do
let(:branch_name) { 'my-branch' }
let(:request) do
Gitaly::UserDeleteBranchRequest.new(
repository: repository.gitaly_repository,
branch_name: branch_name,
user: gitaly_user
)
end
let(:response) { Gitaly::UserDeleteBranchResponse.new }
subject { client.user_delete_branch(branch_name, user) }
it 'sends a user_delete_branch message' do
expect_any_instance_of(Gitaly::OperationService::Stub)
.to receive(:user_delete_branch).with(request, kind_of(Hash))
.and_return(response)
subject
end
context "when pre_receive_error is present" do
let(:response) do
Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed")
end
it "throws a PreReceive exception" do
expect_any_instance_of(Gitaly::OperationService::Stub)
.to receive(:user_delete_branch).with(request, kind_of(Hash))
.and_return(response)
expect { subject }.to raise_error(
Gitlab::Git::HooksService::PreReceiveError, "something failed")
end
end
end
end
......@@ -22,5 +22,12 @@ describe Gitlab::SQL::Union do
expect {User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error
expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
end
it 'uses UNION ALL when removing duplicates is disabled' do
union = described_class
.new([relation_1, relation_2], remove_duplicates: false)
expect(union.to_sql).to include('UNION ALL')
end
end
end
......@@ -155,5 +155,15 @@ describe Key, :mailer do
it 'strips white spaces' do
expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end
it 'invalidates the public_key attribute' do
key = build(:key)
original = key.public_key
key.key = valid_key
expect(original.key_text).not_to be_nil
expect(key.public_key.key_text).to eq(valid_key)
end
end
end
......@@ -791,6 +791,49 @@ describe MergeRequest do
end
end
describe '#has_ci?' do
let(:merge_request) { build_stubbed(:merge_request) }
context 'has ci' do
it 'returns true if MR has head_pipeline_id and commits' do
allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
allow(merge_request).to receive(:head_pipeline_id) { double }
allow(merge_request).to receive(:has_no_commits?) { false }
expect(merge_request.has_ci?).to be(true)
end
it 'returns true if MR has any pipeline and commits' do
allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
allow(merge_request).to receive(:head_pipeline_id) { nil }
allow(merge_request).to receive(:has_no_commits?) { false }
allow(merge_request).to receive(:all_pipelines) { [double] }
expect(merge_request.has_ci?).to be(true)
end
it 'returns true if MR has CI service and commits' do
allow(merge_request).to receive_message_chain(:source_project, :ci_service) { double }
allow(merge_request).to receive(:head_pipeline_id) { nil }
allow(merge_request).to receive(:has_no_commits?) { false }
allow(merge_request).to receive(:all_pipelines) { [] }
expect(merge_request.has_ci?).to be(true)
end
end
context 'has no ci' do
it 'returns false if MR has no CI service nor pipeline, and no commits' do
allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
allow(merge_request).to receive(:head_pipeline_id) { nil }
allow(merge_request).to receive(:all_pipelines) { [] }
allow(merge_request).to receive(:has_no_commits?) { true }
expect(merge_request.has_ci?).to be(false)
end
end
end
describe '#all_pipelines' do
shared_examples 'returning pipelines with proper ordering' do
let!(:all_pipelines) do
......
......@@ -636,18 +636,18 @@ describe Repository do
describe '#fetch_ref' do
describe 'when storage is broken', broken_storage: true do
it 'should raise a storage error' do
path = broken_repository.path_to_repo
expect_to_raise_storage_error { broken_repository.fetch_ref(path, '1', '2') }
expect_to_raise_storage_error do
broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
end
end
end
end
describe '#create_ref' do
it 'redirects the call to fetch_ref' do
it 'redirects the call to write_ref' do
ref, ref_path = '1', '2'
expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path)
expect(repository.raw_repository).to receive(:write_ref).with(ref_path, ref)
repository.create_ref(ref, ref_path)
end
......@@ -901,47 +901,6 @@ describe Repository do
end
end
describe '#rm_branch' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:blank_sha) { '0000000000000000000000000000000000000000' }
context 'when pre hooks were successful' do
it 'runs without errors' do
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
.with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
it 'deletes the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
expect(repository.find_branch('feature')).to be_nil
end
end
context 'when pre hooks failed' do
it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.rm_branch(user, 'feature')
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
it 'does not delete the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.rm_branch(user, 'feature')
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
expect(repository.find_branch('feature')).not_to be_nil
end
end
end
describe '#update_branch_with_hooks' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
......@@ -1744,13 +1703,75 @@ describe Repository do
end
describe '#rm_branch' do
let(:user) { create(:user) }
shared_examples "user deleting a branch" do
it 'removes a branch' do
expect(repository).to receive(:before_remove_branch)
expect(repository).to receive(:after_remove_branch)
repository.rm_branch(user, 'feature')
end
end
context 'with gitaly enabled' do
it_behaves_like "user deleting a branch"
context 'when pre hooks failed' do
before do
allow_any_instance_of(Gitlab::GitalyClient::OperationService)
.to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError)
end
it 'gets an error and does not delete the branch' do
expect do
repository.rm_branch(user, 'feature')
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
expect(repository.find_branch('feature')).not_to be_nil
end
end
end
context 'with gitaly disabled', skip_gitaly_mock: true do
it_behaves_like "user deleting a branch"
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:blank_sha) { '0000000000000000000000000000000000000000' }
context 'when pre hooks were successful' do
it 'runs without errors' do
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
.with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
it 'removes a branch' do
expect(repository).to receive(:before_remove_branch)
expect(repository).to receive(:after_remove_branch)
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
it 'deletes the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
repository.rm_branch(user, 'feature')
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
expect(repository.find_branch('feature')).to be_nil
end
end
context 'when pre hooks failed' do
it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.rm_branch(user, 'feature')
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
it 'does not delete the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
repository.rm_branch(user, 'feature')
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
expect(repository.find_branch('feature')).not_to be_nil
end
end
end
end
......
......@@ -300,6 +300,10 @@ describe MergeRequestPresenter do
described_class.new(resource, current_user: user).remove_wip_path
end
before do
allow(resource).to receive(:work_in_progress?).and_return(true)
end
context 'when merge request enabled and has permission' do
it 'has remove_wip_path' do
allow(project).to receive(:merge_requests_enabled?) { true }
......
require 'spec_helper'
describe ContainerRepositoryEntity do
let(:entity) do
described_class.new(repository, request: request)
end
set(:project) { create(:project) }
set(:user) { create(:user) }
set(:repository) { create(:container_repository, project: project) }
let(:request) { double('request') }
subject { entity.as_json }
before do
stub_container_registry_config(enabled: true)
allow(request).to receive(:project).and_return(project)
allow(request).to receive(:current_user).and_return(user)
end
it 'exposes required informations' do
expect(subject).to include(:id, :path, :location, :tags_path)
end
context 'when user can manage repositories' do
before do
project.add_developer(user)
end
it 'exposes destroy_path' do
expect(subject).to include(:destroy_path)
end
end
context 'when user cannot manage repositories' do
it 'does not expose destroy_path' do
expect(subject).not_to include(:destroy_path)
end
end
end
require 'spec_helper'
describe ContainerTagEntity do
let(:entity) do
described_class.new(tag, request: request)
end
set(:project) { create(:project) }
set(:user) { create(:user) }
set(:repository) { create(:container_repository, name: 'image', project: project) }
let(:request) { double('request') }
let(:tag) { repository.tag('test') }
subject { entity.as_json }
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: /image/, tags: %w[test])
allow(request).to receive(:project).and_return(project)
allow(request).to receive(:current_user).and_return(user)
end
it 'exposes required informations' do
expect(subject).to include(:name, :location, :revision, :total_size, :created_at)
end
context 'when user can manage repositories' do
before do
project.add_developer(user)
end
it 'exposes destroy_path' do
expect(subject).to include(:destroy_path)
end
end
context 'when user cannot manage repositories' do
it 'does not expose destroy_path' do
expect(subject).not_to include(:destroy_path)
end
end
end
......@@ -11,16 +11,6 @@ describe MergeRequestEntity do
described_class.new(resource, request: request).as_json
end
it 'includes author' do
req = double('request')
author_payload = UserEntity
.represent(resource.author, request: req)
.as_json
expect(subject[:author]).to eq(author_payload)
end
it 'includes pipeline' do
req = double('request', current_user: user)
pipeline = build_stubbed(:ci_pipeline)
......
......@@ -39,11 +39,11 @@ module StubGitlabCalls
.and_return({ 'tags' => tags })
allow_any_instance_of(ContainerRegistry::Client)
.to receive(:repository_manifest).with(repository)
.to receive(:repository_manifest).with(repository, anything)
.and_return(stub_container_registry_tag_manifest)
allow_any_instance_of(ContainerRegistry::Client)
.to receive(:blob).with(repository)
.to receive(:blob).with(repository, anything, 'application/octet-stream')
.and_return(stub_container_registry_blob)
end
......
require 'spec_helper'
describe 'projects/registry/repositories/index' do
let(:group) { create(:group, path: 'group') }
let(:project) { create(:project, group: group, path: 'test') }
let(:repository) do
create(:container_repository, project: project, name: 'image')
end
before do
stub_container_registry_config(enabled: true,
host_port: 'registry.gitlab',
api_url: 'http://registry.gitlab')
stub_container_registry_tags(repository: :any, tags: [:latest])
assign(:project, project)
assign(:images, [repository])
allow(view).to receive(:can?).and_return(true)
end
it 'contains container repository path' do
render
expect(rendered).to have_content 'group/test/image'
end
it 'contains attribute for copying tag location into clipboard' do
render
expect(rendered).to have_css 'button[data-clipboard-text="docker pull ' \
'registry.gitlab/group/test/image:latest"]'
end
end
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