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 ...@@ -5,3 +5,4 @@ app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb
...@@ -195,6 +195,10 @@ entry. ...@@ -195,6 +195,10 @@ entry.
- Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi) - Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi)
- [BUGIFX] Improves subgroup creation permissions. !13418 - [BUGIFX] Improves subgroup creation permissions. !13418
## 9.5.7 (2017-10-03)
- Fix gitlab rake:import:repos task.
## 9.5.6 (2017-09-29) ## 9.5.6 (2017-09-29)
- [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242 - [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242
......
...@@ -910,7 +910,7 @@ GEM ...@@ -910,7 +910,7 @@ GEM
json (>= 1.8.0) json (>= 1.8.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.2) unf_ext (0.0.7.4)
unicode-display_width (1.3.0) unicode-display_width (1.3.0)
unicorn (5.1.0) unicorn (5.1.0)
kgio (~> 2.6) kgio (~> 2.6)
......
import Jed from 'jed'; import Jed from 'jed';
import sprintf from './sprintf'; 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 langAttribute = document.querySelector('html').getAttribute('lang');
const lang = (langAttribute || 'en').replace(/-/g, '_'); const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(locales[lang]); const locale = new Jed(window.translations || {});
/** /**
Translates `text` Translates `text`
@param text The text to be translated @param text The text to be translated
@returns {String} The translated text @returns {String} The translated text
**/ **/
......
...@@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper'; ...@@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper';
$el.text(gl.text.addDelimiter(count)); $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; return MergeRequest;
})(); })();
}).call(window); }).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> <script>
import CodeCell from './code/index.vue'; import CodeCell from './code/index.vue';
import OutputCell from './output/index.vue'; import OutputCell from './output/index.vue';
...@@ -51,6 +36,21 @@ export default { ...@@ -51,6 +36,21 @@ export default {
}; };
</script> </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> <style scoped>
.cell { .cell {
flex-direction: column; 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> <script>
import Prism from '../../lib/highlight'; import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
...@@ -55,3 +41,17 @@ ...@@ -55,3 +41,17 @@
}, },
}; };
</script> </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> <script>
/* global katex */ /* global katex */
import marked from 'marked'; import marked from 'marked';
...@@ -95,6 +88,13 @@ ...@@ -95,6 +88,13 @@
}; };
</script> </script>
<template>
<div class="cell text-cell">
<prompt />
<div class="markdown" v-html="markdown"></div>
</div>
</template>
<style> <style>
.markdown .katex { .markdown .katex {
display: block; display: block;
......
<template>
<div class="output">
<prompt />
<div v-html="rawCode"></div>
</div>
</template>
<script> <script>
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
...@@ -20,3 +13,10 @@ export default { ...@@ -20,3 +13,10 @@ export default {
}, },
}; };
</script> </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> <script>
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
...@@ -25,3 +17,11 @@ export default { ...@@ -25,3 +17,11 @@ export default {
}, },
}; };
</script> </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> <script>
import CodeCell from '../code/index.vue'; import CodeCell from '../code/index.vue';
import Html from './html.vue'; import Html from './html.vue';
...@@ -81,3 +72,12 @@ export default { ...@@ -81,3 +72,12 @@ export default {
}, },
}; };
</script> </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> <script>
export default { export default {
props: { props: {
...@@ -21,6 +13,14 @@ ...@@ -21,6 +13,14 @@
}; };
</script> </script>
<template>
<div class="prompt">
<span v-if="type && count">
{{ type }} [{{ count }}]:
</span>
</div>
</template>
<style scoped> <style scoped>
.prompt { .prompt {
padding: 0 10px; 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> <script>
import { import {
MarkdownCell, MarkdownCell,
...@@ -59,6 +48,17 @@ ...@@ -59,6 +48,17 @@
}; };
</script> </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> <style>
.cell, .cell,
.input, .input,
......
...@@ -272,6 +272,7 @@ ...@@ -272,6 +272,7 @@
v-model="note" v-model="note"
ref="textarea" ref="textarea"
slot="textarea" slot="textarea"
:disabled="isSubmitting"
placeholder="Write a comment or drag your files here..." placeholder="Write a comment or drag your files here..."
@keydown.up="editCurrentUserLastNote()" @keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()"> @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> <script>
import pdfjsLib from 'vendor/pdf'; import pdfjsLib from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min'; import workerSrc from 'vendor/pdf.worker.min';
...@@ -64,6 +54,16 @@ ...@@ -64,6 +54,16 @@
}; };
</script> </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> <style>
.pdf-viewer { .pdf-viewer {
background: url('./assets/img/bg.gif'); background: url('./assets/img/bg.gif');
......
<template>
<canvas
class="pdf-page"
ref="canvas"
:data-page="number" />
</template>
<script> <script>
export default { export default {
props: { props: {
...@@ -48,6 +41,13 @@ ...@@ -48,6 +41,13 @@
}; };
</script> </script>
<template>
<canvas
class="pdf-page"
ref="canvas"
:data-page="number" />
</template>
<style> <style>
.pdf-page { .pdf-page {
margin: 8px auto 0 auto; margin: 8px auto 0 auto;
......
export default () => { export default () => {
$('.fork-thumbnail a').on('click', function forkThumbnailClicked() { $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
if ($(this).hasClass('disabled')) return false; if ($(this).hasClass('disabled')) return false;
$('.fork-namespaces').hide(); return $('.js-fork-content').toggle();
return $('.save-project-loader').show();
}); });
}; };
<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 { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <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"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
Checking ability to merge automatically Checking ability to merge automatically
......
...@@ -12,7 +12,7 @@ export default { ...@@ -12,7 +12,7 @@ export default {
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon <status-icon
status="failed" status="failed"
showDisabledButton /> :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span <span
v-if="mr.shouldBeRebased" v-if="mr.shouldBeRebased"
......
...@@ -51,7 +51,7 @@ export default { ...@@ -51,7 +51,7 @@ export default {
</span> </span>
</template> </template>
<template v-else> <template v-else>
<status-icon status="failed" showDisabledButton /> <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
<span <span
......
...@@ -24,7 +24,7 @@ export default { ...@@ -24,7 +24,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <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"> <div class="media-body space-children">
<span class="bold js-branch-text"> <span class="bold js-branch-text">
<span class="capitalize"> <span class="capitalize">
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <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"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
Ready to be merged automatically. Ready to be merged automatically.
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <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"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <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"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure 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 { ...@@ -38,24 +38,40 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc; return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
}, },
mergeButtonClass() { status() {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) { if (hasCI && !ciStatus) {
return failedClass; return 'failed';
} else if (!pipeline) { } else if (!pipeline) {
return defaultClass; return 'success';
} else if (isPipelineActive) { } else if (isPipelineActive) {
return inActionClass; return 'pending';
} else if (isPipelineFailed) { } 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; return failedClass;
} else if (this.status === 'pending') {
return inActionClass;
} }
return defaultClass; return defaultClass;
}, },
iconClass() {
if (this.status === 'failed' || !this.commitMessage.length || !this.isMergeAllowed() || this.mr.preventMerge) {
return 'failed';
}
return 'success';
},
mergeButtonText() { mergeButtonText() {
if (this.isMergingImmediately) { if (this.isMergingImmediately) {
return 'Merge in progress'; return 'Merge in progress';
...@@ -156,6 +172,7 @@ export default { ...@@ -156,6 +172,7 @@ export default {
eventHub.$emit('FetchActionsContent'); eventHub.$emit('FetchActionsContent');
if (window.mergeRequest) { if (window.mergeRequest) {
window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged'); window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged');
window.mergeRequest.hideCloseButton();
window.mergeRequest.decreaseCounter(); window.mergeRequest.decreaseCounter();
} }
stopPolling(); stopPolling();
...@@ -208,7 +225,7 @@ export default { ...@@ -208,7 +225,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="success" /> <status-icon :status="iconClass" />
<div class="media-body"> <div class="media-body">
<div class="mr-widget-body-controls media space-children"> <div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5"> <span class="btn-group append-bottom-5">
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <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"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging The source branch HEAD has recently changed. Please reload the page and review the changes before merging
......
...@@ -10,7 +10,7 @@ export default { ...@@ -10,7 +10,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <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"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
There are unresolved discussions. Please resolve these discussions There are unresolved discussions. Please resolve these discussions
......
...@@ -38,7 +38,7 @@ export default { ...@@ -38,7 +38,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <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"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
This is a Work in Progress 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 @@ ...@@ -23,6 +23,7 @@
&.s60 { @include avatar-size(60px, 12px); } &.s60 { @include avatar-size(60px, 12px); }
&.s70 { @include avatar-size(70px, 14px); } &.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); } &.s90 { @include avatar-size(90px, 15px); }
&.s100 { @include avatar-size(100px, 15px); }
&.s110 { @include avatar-size(110px, 15px); } &.s110 { @include avatar-size(110px, 15px); }
&.s140 { @include avatar-size(140px, 15px); } &.s140 { @include avatar-size(140px, 15px); }
&.s160 { @include avatar-size(160px, 20px); } &.s160 { @include avatar-size(160px, 20px); }
...@@ -78,6 +79,7 @@ ...@@ -78,6 +79,7 @@
&.s60 { font-size: 32px; line-height: 58px; } &.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 70px; } &.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; } &.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; } &.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
&.s140 { font-size: 72px; line-height: 138px; } &.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; } &.s160 { font-size: 96px; line-height: 158px; }
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
.prepend-top-10 { margin-top: 10px; } .prepend-top-10 { margin-top: 10px; }
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px; } .prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; } .prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; } .prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; } .prepend-left-default { margin-left: $gl-padding; }
...@@ -129,11 +130,6 @@ span.update-author { ...@@ -129,11 +130,6 @@ span.update-author {
} }
} }
.user-mention {
color: $user-mention-color;
font-weight: $gl-font-weight-bold;
}
.field_with_errors { .field_with_errors {
display: inline; display: inline;
} }
......
...@@ -6,3 +6,14 @@ ...@@ -6,3 +6,14 @@
.gfm-commit_range { .gfm-commit_range {
@extend .commit-sha; @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 @@ ...@@ -48,31 +48,24 @@
} }
&:hover { &:hover {
background-color: $white-normal; border-color: $gray-darkest;
border-color: $border-white-normal;
color: $gl-text-color; color: $gl-text-color;
} }
} }
} }
.select2-drop { .select2-drop,
box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0; .select2-drop.select2-drop-above {
border-radius: $border-radius-default; box-shadow: 0 2px 4px $dropdown-shadow-color;
border: none; border-radius: $border-radius-base;
border: 1px solid $dropdown-border-color;
min-width: 175px; min-width: 175px;
color: $gl-text-color;
} }
.select2-results .select2-result-label, .select2-drop.select2-drop-above.select2-drop-active {
.select2-more-results { border-top: 1px solid $dropdown-border-color;
padding: 10px 15px; margin-top: -6px;
}
.select2-drop {
color: $gl-grayish-blue;
}
.select2-highlighted {
background: $gl-link-color !important;
} }
.select2-results li.select2-result-with-children > .select2-result-label { .select2-results li.select2-result-with-children > .select2-result-label {
...@@ -87,13 +80,11 @@ ...@@ -87,13 +80,11 @@
} }
} }
.select2-dropdown-open { .select2-dropdown-open,
.select2-dropdown-open.select2-drop-above {
.select2-choice { .select2-choice {
border-color: $border-white-normal; border-color: $gray-darkest;
outline: 0; outline: 0;
background-image: none;
background-color: $white-dark;
box-shadow: $gl-btn-active-gradient;
} }
} }
...@@ -131,28 +122,14 @@ ...@@ -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 { .select2-drop-active {
margin-top: 6px; margin-top: $dropdown-vertical-offset;
font-size: 14px; font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
.select2-results { .select2-results {
max-height: 350px; max-height: 350px;
.select2-highlighted {
background: $gl-primary;
}
} }
} }
...@@ -186,19 +163,35 @@ ...@@ -186,19 +163,35 @@
background-size: 16px 16px !important; 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 { .select2-results {
margin: 0; 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 { .ajax-users-select {
...@@ -265,56 +258,10 @@ ...@@ -265,56 +258,10 @@
min-width: 250px !important; min-width: 250px !important;
} }
// TODO: change global style .select2-result-selectable,
.ajax-project-dropdown, .select2-result-unselectable {
.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 { .select2-match {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
text-decoration: none; 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;
}
}
}
}
} }
...@@ -262,7 +262,8 @@ $well-pre-bg: #eee; ...@@ -262,7 +262,8 @@ $well-pre-bg: #eee;
$well-pre-color: #555; $well-pre-color: #555;
$loading-color: #555; $loading-color: #555;
$update-author-color: #999; $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; $time-color: #999;
$project-member-show-color: #aaa; $project-member-show-color: #aaa;
$gl-promo-color: #aaa; $gl-promo-color: #aaa;
......
...@@ -9,6 +9,14 @@ ...@@ -9,6 +9,14 @@
.container-image-head { .container-image-head {
padding: 0 16px; padding: 0 16px;
line-height: 4em; line-height: 4em;
.btn-link {
padding: 0;
&:focus {
outline: none;
}
}
} }
.table.tags { .table.tags {
......
...@@ -499,22 +499,17 @@ a.deploy-project-label { ...@@ -499,22 +499,17 @@ a.deploy-project-label {
} }
} }
.fork-namespaces { .fork-thumbnail {
.row { height: 200px;
-webkit-flex-wrap: wrap; width: calc((100% / 2) - #{$gl-padding * 2});
display: -webkit-flex;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.fork-thumbnail { @media (min-width: $screen-md-min) {
border-radius: $border-radius-base; width: calc((100% / 4) - #{$gl-padding * 2});
background-color: $white-light; }
border: 1px solid $border-white-light;
height: 202px; @media (min-width: $screen-lg-min) {
margin: $gl-padding; width: calc((100% / 5) - #{$gl-padding * 2});
text-align: center; }
width: 169px;
&:hover:not(.disabled), &:hover:not(.disabled),
&.forked { &.forked {
...@@ -522,18 +517,11 @@ a.deploy-project-label { ...@@ -522,18 +517,11 @@ a.deploy-project-label {
border-color: $row-hover-border; border-color: $row-hover-border;
} }
.no-avatar { .avatar-container,
width: 100px; .identicon {
height: 100px; float: none;
background-color: $gray-light; margin-left: auto;
border: 1px solid $white-normal; margin-right: auto;
margin: 0 auto;
border-radius: 50%;
i {
font-size: 100px;
color: $white-normal;
}
} }
a { a {
...@@ -541,28 +529,23 @@ a.deploy-project-label { ...@@ -541,28 +529,23 @@ a.deploy-project-label {
width: 100%; width: 100%;
height: 100%; height: 100%;
padding-top: $gl-padding; padding-top: $gl-padding;
color: $gl-text-color; text-decoration: none;
&.disabled { &.disabled {
opacity: .3; opacity: .3;
cursor: not-allowed; cursor: not-allowed;
&:hover {
text-decoration: none;
} }
} }
}
.caption { .fork-thumbnail-container {
min-height: 30px; display: flex;
padding: $gl-padding 0; flex-wrap: wrap;
} margin-left: -$gl-padding;
} margin-right: -$gl-padding;
img { > h5 {
border-radius: 50%; width: 100%;
max-width: 100px;
}
}
} }
} }
......
...@@ -12,3 +12,7 @@ ...@@ -12,3 +12,7 @@
margin-left: 10px; margin-left: 10px;
} }
} }
.registry-placeholder {
min-height: 60px;
}
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def index def index
set_index_vars set_index_vars
@personal_access_token = finder.build
end end
def create def create
...@@ -40,7 +41,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -40,7 +41,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth.available_scopes @scopes = Gitlab::Auth.available_scopes
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
end end
......
...@@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController
def index def index
@sort = params[:sort].presence || sort_value_recently_updated @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]) @branches = Kaminari.paginate_array(@branches).page(params[:page])
respond_to do |format| respond_to do |format|
......
...@@ -6,17 +6,26 @@ module Projects ...@@ -6,17 +6,26 @@ module Projects
def index def index
@images = project.container_repositories @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 end
def destroy def destroy
if image.destroy if image.destroy
redirect_to project_container_registry_index_path(@project), respond_to do |format|
status: 302, format.json { head :no_content }
notice: 'Image repository has been removed successfully!' end
else else
redirect_to project_container_registry_index_path(@project), respond_to do |format|
status: 302, format.json { head :bad_request }
alert: 'Failed to remove image repository!' end
end end
end end
......
...@@ -3,20 +3,35 @@ module Projects ...@@ -3,20 +3,35 @@ module Projects
class TagsController < ::Projects::Registry::ApplicationController class TagsController < ::Projects::Registry::ApplicationController
before_action :authorize_update_container_image!, only: [:destroy] 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 def destroy
if tag.delete if tag.delete
redirect_to project_container_registry_index_path(@project), respond_to do |format|
status: 302, format.json { head :no_content }
notice: 'Registry tag has been removed successfully!' end
else else
redirect_to project_container_registry_index_path(@project), respond_to do |format|
status: 302, format.json { head :bad_request }
alert: 'Failed to remove registry tag!' end
end end
end end
private private
def tags
Kaminari::PaginatableArray.new(image.tags, limit: 15)
end
def image def image
@image ||= project.container_repositories @image ||= project.container_repositories
.find(params[:repository_id]) .find(params[:repository_id])
......
module EventsHelper module EventsHelper
ICON_NAMES_BY_EVENT_TYPE = { ICON_NAMES_BY_EVENT_TYPE = {
'pushed to' => 'icon_commit', 'pushed to' => 'commit',
'pushed new' => 'icon_commit', 'pushed new' => 'commit',
'created' => 'icon_status_open', 'created' => 'status_open',
'opened' => 'icon_status_open', 'opened' => 'status_open',
'closed' => 'icon_status_closed', 'closed' => 'status_closed',
'accepted' => 'icon_code_fork', 'accepted' => 'fork',
'commented on' => 'icon_comment_o', 'commented on' => 'comment',
'deleted' => 'icon_trash_o' 'deleted' => 'remove',
'imported' => 'import',
'joined' => 'users'
}.freeze }.freeze
def link_to_author(event, self_added: false) def link_to_author(event, self_added: false)
...@@ -197,7 +199,7 @@ module EventsHelper ...@@ -197,7 +199,7 @@ module EventsHelper
def icon_for_event(note) def icon_for_event(note)
icon_name = ICON_NAMES_BY_EVENT_TYPE[note] icon_name = ICON_NAMES_BY_EVENT_TYPE[note]
custom_icon(icon_name) if icon_name sprite_icon(icon_name) if icon_name
end end
def icon_for_profile_event(event) def icon_for_profile_event(event)
......
...@@ -34,6 +34,7 @@ class Key < ActiveRecord::Base ...@@ -34,6 +34,7 @@ class Key < ActiveRecord::Base
value&.delete!("\n\r") value&.delete!("\n\r")
value.strip! unless value.blank? value.strip! unless value.blank?
write_attribute(:key, value) write_attribute(:key, value)
@public_key = nil
end end
def publishable_key def publishable_key
......
...@@ -560,14 +560,20 @@ class MergeRequest < ActiveRecord::Base ...@@ -560,14 +560,20 @@ class MergeRequest < ActiveRecord::Base
commits_for_notes_limit = 100 commits_for_notes_limit = 100
commit_ids = commit_shas.take(commits_for_notes_limit) commit_ids = commit_shas.take(commits_for_notes_limit)
Note.where( commit_notes = Note
"(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" + .except(:order)
"((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", .where(project_id: [source_project_id, target_project_id])
mr_id: id, .where(noteable_type: 'Commit', commit_id: commit_ids)
commit_ids: commit_ids,
target_project_id: target_project_id, # We're using a UNION ALL here since this results in better performance
source_project_id: source_project_id # 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 end
alias_method :discussion_notes, :related_notes alias_method :discussion_notes, :related_notes
...@@ -742,10 +748,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -742,10 +748,9 @@ class MergeRequest < ActiveRecord::Base
end end
def has_ci? def has_ci?
has_ci_integration = source_project.try(:ci_service) return false if has_no_commits?
uses_gitlab_ci = all_pipelines.any?
(has_ci_integration || uses_gitlab_ci) && commits.any? !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
end end
def branch_missing? def branch_missing?
......
...@@ -17,6 +17,8 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -17,6 +17,8 @@ class PersonalAccessToken < ActiveRecord::Base
validates :scopes, presence: true validates :scopes, presence: true
validate :validate_scopes validate :validate_scopes
after_initialize :set_default_scopes, if: :persisted?
def revoke! def revoke!
update!(revoked: true) update!(revoked: true)
end end
...@@ -32,4 +34,8 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -32,4 +34,8 @@ class PersonalAccessToken < ActiveRecord::Base
errors.add :scopes, "can only contain available scopes" errors.add :scopes, "can only contain available scopes"
end end
end end
def set_default_scopes
self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
end
end end
...@@ -989,7 +989,7 @@ class Repository ...@@ -989,7 +989,7 @@ class Repository
end end
def create_ref(ref, ref_path) def create_ref(ref, ref_path)
fetch_ref(path_to_repo, ref, ref_path) raw_repository.write_ref(ref_path, ref)
end end
def ls_files(ref) def ls_files(ref)
......
...@@ -31,7 +31,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -31,7 +31,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def remove_wip_path 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) remove_wip_project_merge_request_path(project, merge_request)
end end
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 ...@@ -23,7 +23,6 @@ class MergeRequestEntity < IssuableEntity
expose :closed_event, using: EventEntity expose :closed_event, using: EventEntity
# User entities # User entities
expose :author, using: UserEntity
expose :merge_user, using: UserEntity expose :merge_user, using: UserEntity
# Diff sha's # Diff sha's
...@@ -31,7 +30,6 @@ class MergeRequestEntity < IssuableEntity ...@@ -31,7 +30,6 @@ class MergeRequestEntity < IssuableEntity
merge_request.diff_head_sha if merge_request.diff_head_commit merge_request.diff_head_sha if merge_request.diff_head_commit
end end
expose :merge_commit_sha
expose :merge_commit_message expose :merge_commit_message
expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline
......
...@@ -37,9 +37,9 @@ ...@@ -37,9 +37,9 @@
- if content_for?(:library_javascripts) - if content_for?(:library_javascripts)
= yield :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 "webpack_runtime"
= webpack_bundle_tag "common" = webpack_bundle_tag "common"
= webpack_bundle_tag "locale"
= webpack_bundle_tag "main" = webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test? = webpack_bundle_tag "test" if Rails.env.test?
......
...@@ -9,50 +9,36 @@ ...@@ -9,50 +9,36 @@
%br %br
Forking a repository allows you to make changes without affecting the original project. Forking a repository allows you to make changes without affecting the original project.
.col-lg-9 .col-lg-9
.fork-namespaces
- if @namespaces.present? - if @namespaces.present?
%label.label-light .fork-thumbnail-container.js-fork-content
%span %h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
Click to fork the project Click to fork the project
- @namespaces.in_groups_of(6, false) do |group| - @namespaces.each do |namespace|
.row
- group.each do |namespace|
- avatar = namespace_icon(namespace, 100) - 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) - can_create_project = current_user.can?(:create_projects, namespace)
.fork-thumbnail{ class: ("disabled" unless can_create_project) } - forked_project = namespace.find_fork_of(@project)
= link_to project_forks_path(@project, namespace_key: namespace.id), - 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", method: "POST",
class: ("disabled has-tooltip" unless can_create_project), 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 title: (_('You have reached your project limit') unless can_create_project) do
- if /no_((\w*)_)*avatar/.match(avatar) - if /no_((\w*)_)*avatar/.match(avatar)
.no-avatar = project_identicon(namespace, class: "avatar s100 identicon")
= icon 'question'
- else - else
= image_tag avatar .avatar-container.s100
.caption = image_tag(avatar, class: "avatar s100")
%h5.prepend-top-default
= namespace.human_name = namespace.human_name
- else - else
%label.label-light %strong
%span
No available namespaces to fork the project. No available namespaces to fork the project.
%br %p.prepend-top-default
%small
You must have permission to create a project in a namespace before forking. You must have permission to create a project in a namespace before forking.
.save-project-loader.hide .save-project-loader.hide.js-fork-content
.center %h2.text-center
%h2 = icon('spinner spin')
%i.fa.fa-spinner.fa-spin
Forking repository Forking repository
%p Please wait a moment, this page will automatically refresh when ready. %p.text-center
Please wait a moment, this page will automatically refresh when ready.
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
%h2.merge-requests-title %h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request') = pluralize(@merge_requests.count, 'Related Merge Request')
%ul.unstyled-list.related-merge-requests %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| - @merge_requests.each do |merge_request|
%li %li
%span.merge-request-ci-status %span.merge-request-ci-status
- if merge_request.head_pipeline - if merge_request.head_pipeline
= render_pipeline_status(merge_request.head_pipeline) = render_pipeline_status(merge_request.head_pipeline)
- elsif has_any_ci - elsif has_any_head_pipeline
= icon('blank fw') = icon('blank fw')
%span.merge-request-id %span.merge-request-id
= merge_request.to_reference = merge_request.to_reference
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
- unless current_user == @merge_request.author - 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)) %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 - 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' = 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) } %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' = 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" - page_title "Container Registry"
.row.prepend-top-default.append-bottom-default %section
.col-lg-3 .settings-header
%h4.prepend-top-0 %h4
= page_title = page_title
%p %p
With the Docker Container Registry integrated into GitLab, every project = s_('ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.')
can have its own space to store its Docker images.
%p.append-bottom-0 %p.append-bottom-0
= succeed '.' do = succeed '.' do
Learn more about = s_('ContainerRegistry|Learn more about')
= link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank' = 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 = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('registry_list')
.row.prepend-top-10
.col-lg-12
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
%h4.panel-title %h4.panel-title
How to use the Container Registry = s_('ContainerRegistry|How to use the Container Registry')
.panel-body .panel-body
%p %p
First log in to GitLab&rsquo;s Container Registry using your GitLab username - link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank')
and password. If you have - link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank')
= 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 }
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 %pre
docker login #{Gitlab.config.registry.host_port} docker login #{Gitlab.config.registry.host_port}
%br %br
%p %p
Once you log in, you&rsquo;re free to create and upload a container image = 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 }
using the common
%code build
and
%code push
commands:
%pre %pre
:plain :plain
docker build -t #{escape_once(@project.container_registry_url)} . docker build -t #{escape_once(@project.container_registry_url)} .
docker push #{escape_once(@project.container_registry_url)} docker push #{escape_once(@project.container_registry_url)}
%hr %hr
%h5.prepend-top-default %h5.prepend-top-default
Use different image names = s_('ContainerRegistry|Use different image names')
%p.light %p.light
GitLab supports up to 3 levels of image names. The following = s_('ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:')
examples of images are valid for your project:
%pre %pre
:plain :plain
#{escape_once(@project.container_registry_url)}:tag #{escape_once(@project.container_registry_url)}:tag
#{escape_once(@project.container_registry_url)}/optional-image-name:tag #{escape_once(@project.container_registry_url)}/optional-image-name:tag
#{escape_once(@project.container_registry_url)}/optional-name/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
...@@ -2,12 +2,11 @@ ...@@ -2,12 +2,11 @@
- release = @releases.find { |release| release.tag == tag.name } - release = @releases.find { |release| release.tag == tag.name }
%li.flex-row %li.flex-row
.row-main-content.str-truncated .row-main-content.str-truncated
= link_to project_tag_path(@project, tag.name), class: 'item-title ref-name' do
= icon('tag') = icon('tag')
= tag.name = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name prepend-left-4'
- if protected_tag?(@project, tag) - if protected_tag?(@project, tag)
%span.label.label-success %span.label.label-success.prepend-left-4
protected protected
- if tag.message.present? - if tag.message.present?
......
- type = impersonation ? "impersonation" : "personal access" - type = impersonation ? "impersonation" : "personal access"
%h5.prepend-top-0 %h5.prepend-top-0
Add a #{type} Token Add a #{type} token
%p.profile-settings-content %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| = 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 ...@@ -105,6 +105,7 @@ module Gitlab
config.assets.precompile << "lib/ace.js" config.assets.precompile << "lib/ace.js"
config.assets.precompile << "vendor/assets/fonts/*" config.assets.precompile << "vendor/assets/fonts/*"
config.assets.precompile << "test.css" 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 # Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0' config.assets.version = '1.0'
......
...@@ -499,6 +499,8 @@ production: &base ...@@ -499,6 +499,8 @@ production: &base
# Gitaly settings # Gitaly settings
gitaly: gitaly:
# Path to the directory containing Gitaly client executables.
client_path: /home/git/gitaly
# Default Gitaly authentication token. Can be overriden per storage. Can # Default Gitaly authentication token. Can be overriden per storage. Can
# be left blank when Gitaly is running locally on a Unix socket, which # be left blank when Gitaly is running locally on a Unix socket, which
# is the normal way to deploy Gitaly. # is the normal way to deploy Gitaly.
...@@ -664,7 +666,7 @@ test: ...@@ -664,7 +666,7 @@ test:
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
gitaly: gitaly:
enabled: true client_path: tmp/tests/gitaly
token: secret token: secret
backup: backup:
path: tmp/tests/backups path: tmp/tests/backups
......
...@@ -39,3 +39,17 @@ module GettextI18nRailsJs ...@@ -39,3 +39,17 @@ module GettextI18nRailsJs
end end
end 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 ...@@ -281,7 +281,7 @@ constraints(ProjectUrlConstrainer.new) do
namespace :registry do namespace :registry do
resources :repository, only: [] do resources :repository, only: [] do
resources :tags, only: [:destroy], resources :tags, only: [:index, :destroy],
constraints: { id: Gitlab::Regex.container_registry_tag_regex } constraints: { id: Gitlab::Regex.container_registry_tag_regex }
end end
end end
......
...@@ -68,6 +68,7 @@ var config = { ...@@ -68,6 +68,7 @@ var config = {
prometheus_metrics: './prometheus_metrics', prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches', protected_branches: './protected_branches',
protected_tags: './protected_tags', protected_tags: './protected_tags',
registry_list: './registry/index.js',
repo: './repo/index.js', repo: './repo/index.js',
sidebar: './sidebar/sidebar_bundle.js', sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js', schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
...@@ -121,10 +122,6 @@ var config = { ...@@ -121,10 +122,6 @@ var config = {
name: '[name].[hash].[ext]', name: '[name].[hash].[ext]',
} }
}, },
{
test: /locale\/\w+\/(.*)\.js$/,
loader: 'exports-loader?locales',
},
{ {
test: /monaco-editor\/\w+\/vs\/loader\.js$/, test: /monaco-editor\/\w+\/vs\/loader\.js$/,
use: [ use: [
...@@ -200,6 +197,7 @@ var config = { ...@@ -200,6 +197,7 @@ var config = {
'pdf_viewer', 'pdf_viewer',
'pipelines', 'pipelines',
'pipelines_details', 'pipelines_details',
'registry_list',
'repo', 'repo',
'schedule_form', 'schedule_form',
'schedules_index', 'schedules_index',
...@@ -222,7 +220,7 @@ var config = { ...@@ -222,7 +220,7 @@ var config = {
// create cacheable common library bundles // create cacheable common library bundles
new webpack.optimize.CommonsChunkPlugin({ new webpack.optimize.CommonsChunkPlugin({
names: ['main', 'locale', 'common', 'webpack_runtime'], names: ['main', 'common', 'webpack_runtime'],
}), }),
// enable scope hoisting // enable scope hoisting
......
...@@ -7,11 +7,13 @@ class AddFastForwardOptionToProject < ActiveRecord::Migration ...@@ -7,11 +7,13 @@ class AddFastForwardOptionToProject < ActiveRecord::Migration
disable_ddl_transaction! disable_ddl_transaction!
def add def up
add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false) add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false)
end end
def down def down
if column_exists?(:projects, :merge_requests_ff_only_enabled)
remove_column(:projects, :merge_requests_ff_only_enabled) remove_column(:projects, :merge_requests_ff_only_enabled)
end end
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 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170928100231) do ActiveRecord::Schema.define(version: 20171004121444) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -121,6 +121,15 @@ GET /projects/:id/merge_requests?labels=bug,reproduced ...@@ -121,6 +121,15 @@ GET /projects/:id/merge_requests?labels=bug,reproduced
GET /projects/:id/merge_requests?my_reaction_emoji=star 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: Parameters:
| Attribute | Type | Required | Description | | 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 file present at the root directory of your project and a
[runner](runners/README.md) properly set up. You can read our [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 If you are using an external CI/CD server like Jenkins or Drone CI, it is advised
to disable GitLab CI in order to not have any conflicts with the commits status to disable GitLab CI/CD in order to not have any conflicts with the commits status
API. API.
--- ---
GitLab CI is exposed via the `/pipelines` and `/builds` pages of a project. GitLab CI/CD is exposed via the `/pipelines` and `/jobs` pages of a project.
Disabling GitLab CI in a project does not delete any previous jobs. Disabling GitLab CI/CD in a project does not delete any previous jobs.
In fact, the `/pipelines` and `/builds` pages can still be accessed, although In fact, the `/pipelines` and `/jobs` pages can still be accessed, although
it's hidden from the left sidebar menu. 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 individually under each project's settings, or site-wide by modifying the
settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
respectively. respectively.
### Per-project user setting ### Per-project user setting
The setting to enable or disable GitLab CI can be found with the name **Pipelines** The setting to enable or disable GitLab CI/CD can be found under your project's
under the **Sharing & Permissions** area of a project's settings along with **Settings > General > Permissions**. Choose one of "Disabled", "Only team members"
**Merge Requests**. Choose one of **Disabled**, **Only team members** and or "Everyone with access" and hit **Save changes** for the settings to take effect.
**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 admin setting
### Site-wide administrator 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. and `gitlab.rb` for source and Omnibus installations respectively.
Two things to note: 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. 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. project's settings.
---
For installations from source, open `gitlab.yml` with your editor and set For installations from source, open `gitlab.yml` with your editor and set
`builds` to `false`: `builds` to `false`:
......
...@@ -26,7 +26,7 @@ so every environment can have one or more deployments. GitLab keeps track of ...@@ -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 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] 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 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 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 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. ...@@ -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. 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: 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. ...@@ -229,7 +229,7 @@ You can find it in the pipeline, job, environment, and deployment views.
| Pipelines | Single pipeline | Environments | Deployments | jobs | | 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 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 `deploy_prod` job, and the deployment will be recorded under a new
...@@ -402,7 +402,7 @@ places within GitLab. ...@@ -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 | | 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 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` `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] ...@@ -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 environment which has had a successful deployment. If monitoring data was
successfully retrieved, a Monitoring button will appear for each environment. 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 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 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 ...@@ -593,10 +593,11 @@ Web terminals were added in GitLab 8.15 and are only available to project
masters and owners. masters and owners.
If you deploy to your environments with the help of a deployment service (e.g., 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 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 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: 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: ...@@ -149,14 +149,15 @@ script:
## Secret variables ## Secret variables
>**Notes:** NOTE: **Note:**
- This feature requires GitLab Runner 0.4.0 or higher. Group-level secret variables were added in GitLab 9.4.
- Group-level secret variables added in GitLab 9.4.
- Be aware that secret variables are not masked, and their values can be shown CAUTION: **Important:**
in the job logs if explicitly asked to do so. If your project is public or Be aware that secret variables are not masked, and their values can be shown
internal, you can set the pipelines private from your project's Pipelines in the job logs if explicitly asked to do so. If your project is public or
settings. Follow the discussion in issue [#13784][ce-13784] for masking the internal, you can set the pipelines private from your [project's Pipelines
secret variables. 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 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 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 ...@@ -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**. **Settings > CI/CD**, then finding the section called **Secret variables**.
Any variables of [subgroups] will be inherited recursively. 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 Once you set them, they will be available for all subsequent pipelines. You can also
[protect your variables](#protected-secret-variables). [protect your variables](#protected-secret-variables).
...@@ -202,7 +205,7 @@ are set in the build environment. These variables are only defined for ...@@ -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. the project services that you are using to learn which variables they define.
An example project service that defines deployment variables is 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 ## Debug tracing
...@@ -439,7 +442,7 @@ export CI_REGISTRY_USER="gitlab-ci-token" ...@@ -439,7 +442,7 @@ export CI_REGISTRY_USER="gitlab-ci-token"
export CI_REGISTRY_PASSWORD="longalfanumstring" 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" [eep]: https://about.gitlab.com/gitlab-ee/ "Available only in GitLab Enterprise Edition Premium"
[envs]: ../environments.md [envs]: ../environments.md
[protected branches]: ../../user/project/protected_branches.md [protected branches]: ../../user/project/protected_branches.md
......
...@@ -470,7 +470,25 @@ On those a default key should not be provided. ...@@ -470,7 +470,25 @@ On those a default key should not be provided.
``` ```
#### Ordering #### 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. `name`
1. `props` 1. `props`
1. `mixins` 1. `mixins`
...@@ -490,6 +508,7 @@ On those a default key should not be provided. ...@@ -490,6 +508,7 @@ On those a default key should not be provided.
1. `beforeDestroy` 1. `beforeDestroy`
1. `destroyed` 1. `destroyed`
#### Vue and Bootstrap #### Vue and Bootstrap
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components 1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
......
...@@ -428,7 +428,7 @@ is a good example of this pattern. ...@@ -428,7 +428,7 @@ is a good example of this pattern.
## Style guide ## 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. for best practices while writing your Vue components and templates.
## Testing Vue Components ## Testing Vue Components
......
...@@ -302,7 +302,7 @@ range of inputs, might look like this: ...@@ -302,7 +302,7 @@ range of inputs, might look like this:
```ruby ```ruby
describe "#==" do describe "#==" do
using Rspec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:project1) { create(:project) } let(:project1) { create(:project) }
let(:project2) { create(:project) } let(:project2) { create(:project) }
......
...@@ -11,6 +11,7 @@ This page gathers all the resources for the topic **Authentication** within GitL ...@@ -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/) - [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/)
- **Integrations:** - **Integrations:**
- [GitLab as OAuth2 authentication service provider](../../integration/oauth_provider.md#introduction-to-oauth) - [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 ## GitLab administrators
......
...@@ -17,25 +17,25 @@ have its own space to store its Docker images. ...@@ -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/. You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
---
## Enable the Container Registry for your project ## 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 1. First, ask your system administrator to enable GitLab Container Registry
following the [administration documentation](../../administration/container_registry.md). following the [administration documentation](../../administration/container_registry.md).
If you are using GitLab.com, this is enabled by default so you can start using If you are using GitLab.com, this is enabled by default so you can start using
the Registry immediately. the Registry immediately.
1. Go to your [project's General settings](settings/index.md#sharing-and-permissions)
1. Go to your project's settings and enable the **Container Registry** feature and enable the **Container Registry** feature on your project. For new
on your project. For new projects this might be enabled by default. For projects this might be enabled by default. For existing projects
existing projects (prior GitLab 8.8), you will have to explicitly enable it. (prior GitLab 8.8), you will have to explicitly enable it.
![Enable Container Registry](img/container_registry_enable.png)
1. Hit **Save changes** for the changes to take effect. You should now be able 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 ## 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: ...@@ -12,6 +12,8 @@ Other interesting links:
- [GitLab Issue Board landing page on about.gitlab.com][landing] - [GitLab Issue Board landing page on about.gitlab.com][landing]
- [YouTube video introduction to Issue Boards][youtube] - [YouTube video introduction to Issue Boards][youtube]
![GitLab Issue Board](img/issue_board.png)
## Overview ## Overview
The Issue Board builds on GitLab's existing The Issue Board builds on GitLab's existing
...@@ -89,10 +91,6 @@ two defaults: ...@@ -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. - **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. - **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: In short, here's a list of actions you can take in an Issue Board:
- [Create a new list](#creating-a-new-list). - [Create a new list](#creating-a-new-list).
......
...@@ -20,8 +20,6 @@ Head over a single project and navigate to **Issues > Labels**. ...@@ -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 The first time you visit this page, you'll notice that there are no labels
created yet. created yet.
![Generate new labels](img/labels_generate.png)
Creating a new label from scratch is as easy as pressing the **New label** 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, button. From there on you can choose the name, give it an optional description,
a color and you are set. a color and you are set.
...@@ -32,21 +30,23 @@ When you are ready press the **Create label** button to create the new label. ...@@ -32,21 +30,23 @@ When you are ready press the **Create label** button to create the new label.
--- ---
## Default Labels ## Default labels
It's possible to populate the labels for your project from a set of predefined labels.
### Generate GitLab's predefined label set
![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 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 generate them for you. There are 8 default generated labels in total:
in total and you can see them in the screenshot below.
![Default generated labels](img/labels_default.png)
--- - bug
- confirmed
- critical
- discussion
- documentation
- enhancement
- suggestion
- support
## Labels Overview ## Labels Overview
...@@ -102,30 +102,25 @@ If you work on a large or popular project, try subscribing only to the labels ...@@ -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 that are relevant to you. You’ll notice it’ll be much easier to focus on what’s
important. important.
## Create a new label right from the issue tracker ## Create a new label when inside an issue
> Introduced in GitLab 8.6.
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** 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 page and being distracted from your original purpose, you can create new
labels on the fly. labels on the fly.
Select **Create new** from the labels dropdown list, provide a name, pick a Expand the issue sidebar and select **Create new label** from the labels dropdown
color and hit **Create**. 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) ![New label on the fly](img/labels_new_label_on_the_fly.png)
## Assigning labels to issues and merge requests ## Assigning labels to issues and merge requests
There are generally two ways to assign a label to an issue or merge request. 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. The first one is to 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 second way is by using the right sidebar when inside an issue or merge 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 request. Expand it and hit **Edit** in the labels area. Start typing the name
......
...@@ -2,24 +2,19 @@ ...@@ -2,24 +2,19 @@
> [Introduced][ce-3514] in GitLab 8.7. > [Introduced][ce-3514] in GitLab 8.7.
---
GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick] 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 After the merge request has been merged, a **Cherry-pick** button will be available
to cherry-pick the changes introduced by that Merge Request: to cherry-pick the changes introduced by that merge request.
![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png) ![Cherry-pick Merge Request](img/cherry_pick_changes_mr.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
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
create a new Merge Request with the cherry-pick changes:
![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
## Cherry-picking a Commit ## Cherry-picking a Commit
...@@ -27,15 +22,9 @@ You can cherry-pick a Commit from the Commit details page: ...@@ -27,15 +22,9 @@ You can cherry-pick a Commit from the Commit details page:
![Cherry-pick commit](img/cherry_pick_changes_commit.png) ![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
Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes 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)
---
Please note that when cherry-picking merge commits, the mainline will always be the 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 first parent. If you want to use a different mainline then you need to do that
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
Merge requests allow you to exchange changes you made to source code and Merge requests allow you to exchange changes you made to source code and
collaborate with other people on the same project. collaborate with other people on the same project.
![Merge request view](img/merge_request.png)
## Overview ## Overview
A Merge Request (**MR**) is the basis of GitLab as a code collaboration A Merge Request (**MR**) is the basis of GitLab as a code collaboration
......
...@@ -2,51 +2,39 @@ ...@@ -2,51 +2,39 @@
> [Introduced][ce-1990] in GitLab 8.5. > [Introduced][ce-1990] in GitLab 8.5.
---
GitLab implements Git's powerful feature to [revert any commit][git-revert] 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 ## Reverting a Merge Request
_**Note:** The **Revert** button will only be available for Merge Requests NOTE: **Note:**
created since GitLab 8.5. However, you can still revert a Merge Request The **Revert** button will only be available for merge requests
by reverting the merge commit from the list of Commits page._ 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 After the Merge Request has been merged, a **Revert** button will be available
to revert the changes introduced by that Merge Request: 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:
![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. available anymore.
## Reverting a Commit ## Reverting a Commit
You can revert a Commit from the Commit details page: You can revert a Commit from the Commit details page:
![Revert commit](img/revert_changes_commit.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:
![Revert commit modal](img/revert_changes_commit_modal.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. anymore.
Please note that when reverting merge commits, the mainline will always be the Please note that when reverting merge commits, the mainline will always be the
......
...@@ -26,7 +26,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps ...@@ -26,7 +26,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end end
step 'I fork to my namespace' do step 'I fork to my namespace' do
page.within '.fork-namespaces' do page.within '.fork-thumbnail-container' do
click_link current_user.name click_link current_user.name
end end
end end
......
...@@ -53,14 +53,15 @@ module Gitlab ...@@ -53,14 +53,15 @@ module Gitlab
# Rugged repo object # Rugged repo object
attr_reader :rugged 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. # This initializer method is only used on the client side (gitlab-ce).
# /path/to/my-repo.git # Gitaly-ruby uses a different initializer.
def initialize(storage, relative_path, gl_repository) def initialize(storage, relative_path, gl_repository)
@storage = storage @storage = storage
@relative_path = relative_path @relative_path = relative_path
@gl_repository = gl_repository @gl_repository = gl_repository
@gitaly_resolver = Gitlab::GitalyClient
storage_path = Gitlab.config.repositories.storages[@storage]['path'] storage_path = Gitlab.config.repositories.storages[@storage]['path']
@path = File.join(storage_path, @relative_path) @path = File.join(storage_path, @relative_path)
...@@ -676,8 +677,14 @@ module Gitlab ...@@ -676,8 +677,14 @@ module Gitlab
end end
def rm_branch(branch_name, user:) def rm_branch(branch_name, user:)
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)) OperationService.new(user, self).rm_branch(find_branch(branch_name))
end end
end
end
def rm_tag(tag_name, user:) def rm_tag(tag_name, user:)
gitaly_migrate(:operation_user_delete_tag) do |is_enabled| gitaly_migrate(:operation_user_delete_tag) do |is_enabled|
...@@ -981,9 +988,9 @@ module Gitlab ...@@ -981,9 +988,9 @@ module Gitlab
def with_repo_tmp_commit(start_repository, start_branch_name, sha) def with_repo_tmp_commit(start_repository, start_branch_name, sha)
tmp_ref = fetch_ref( tmp_ref = fetch_ref(
start_repository.path, start_repository,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
"refs/tmp/#{SecureRandom.hex}/head" target_ref: "refs/tmp/#{SecureRandom.hex}/head"
) )
yield commit(sha) yield commit(sha)
...@@ -1015,13 +1022,27 @@ module Gitlab ...@@ -1015,13 +1022,27 @@ module Gitlab
end end
end end
def write_ref(ref_path, sha) def write_ref(ref_path, ref)
rugged.references.create(ref_path, sha, force: true) 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 end
def fetch_ref(source_path, source_ref, target_ref) raise GitError, output unless status.zero?
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) end
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 # Make sure ref was created, and raise Rugged::ReferenceError when not
raise Rugged::ReferenceError, message if status != 0 raise Rugged::ReferenceError, message if status != 0
...@@ -1030,9 +1051,9 @@ module Gitlab ...@@ -1030,9 +1051,9 @@ module Gitlab
end end
# Refactoring aid; allows us to copy code from app/models/repository.rb # 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 circuit_breaker.perform do
popen([Gitlab.config.git.bin_path, *args], path) popen([Gitlab.config.git.bin_path, *args], path, env)
end end
end end
...@@ -1489,9 +1510,33 @@ module Gitlab ...@@ -1489,9 +1510,33 @@ module Gitlab
OperationService.new(user, self).add_branch(branch_name, target_object.oid) OperationService.new(user, self).add_branch(branch_name, target_object.oid)
find_branch(branch_name) find_branch(branch_name)
rescue Rugged::ReferenceError rescue Rugged::ReferenceError => ex
raise InvalidRef, ex raise InvalidRef, ex
end 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 end
end end
...@@ -31,7 +31,7 @@ module Gitlab ...@@ -31,7 +31,7 @@ module Gitlab
output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys) output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
unless status.zero? 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 end
output.split("\n") output.split("\n")
......
...@@ -60,6 +60,20 @@ module Gitlab ...@@ -60,6 +60,20 @@ module Gitlab
target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit) target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit) Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit)
end 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 end
end end
...@@ -25,7 +25,7 @@ module Gitlab ...@@ -25,7 +25,7 @@ module Gitlab
Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\ Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\
"#{MAX_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" "in #{GRACE_TIME} seconds"
sleep(GRACE_TIME) sleep(GRACE_TIME)
......
...@@ -12,8 +12,9 @@ module Gitlab ...@@ -12,8 +12,9 @@ module Gitlab
# #
# Project.where("id IN (#{sql})") # Project.where("id IN (#{sql})")
class Union class Union
def initialize(relations) def initialize(relations, remove_duplicates: true)
@relations = relations @relations = relations
@remove_duplicates = remove_duplicates
end end
def to_sql def to_sql
...@@ -25,7 +26,11 @@ module Gitlab ...@@ -25,7 +26,11 @@ module Gitlab
@relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
end end
fragments.join("\nUNION\n") fragments.join("\n#{union_keyword}\n")
end
def union_keyword
@remove_duplicates ? 'UNION' : 'UNION ALL'
end end
end end
end end
......
...@@ -3,8 +3,8 @@ namespace :gitlab do ...@@ -3,8 +3,8 @@ namespace :gitlab do
desc 'GitLab | Assets | Compile all frontend assets' desc 'GitLab | Assets | Compile all frontend assets'
task compile: [ task compile: [
'yarn:check', 'yarn:check',
'rake:assets:precompile',
'gettext:po_to_json', 'gettext:po_to_json',
'rake:assets:precompile',
'webpack:compile', 'webpack:compile',
'fix_urls' 'fix_urls'
] ]
......
source 'https://rubygems.org' source 'https://rubygems.org'
gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'capybara', '~> 2.12.1' gem 'capybara', '~> 2.12.1'
gem 'capybara-screenshot', '~> 1.0.14' gem 'capybara-screenshot', '~> 1.0.14'
gem 'rake', '~> 12.0.0' gem 'rake', '~> 12.0.0'
......
...@@ -3,6 +3,7 @@ GEM ...@@ -3,6 +3,7 @@ GEM
specs: specs:
addressable (2.5.0) addressable (2.5.0)
public_suffix (~> 2.0, >= 2.0.2) public_suffix (~> 2.0, >= 2.0.2)
byebug (9.0.6)
capybara (2.12.1) capybara (2.12.1)
addressable addressable
mime-types (>= 1.16) mime-types (>= 1.16)
...@@ -13,22 +14,27 @@ GEM ...@@ -13,22 +14,27 @@ GEM
capybara-screenshot (1.0.14) capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3) capybara (>= 1.0, < 3)
launchy launchy
capybara-webkit (1.12.0)
capybara (>= 2.3.0, < 2.13.0)
json
childprocess (0.7.0) childprocess (0.7.0)
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
coderay (1.1.1)
diff-lcs (1.3) diff-lcs (1.3)
ffi (1.9.18) ffi (1.9.18)
json (2.0.3)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
method_source (0.8.2)
mime-types (3.1) mime-types (3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mini_portile2 (2.1.0) mini_portile2 (2.1.0)
nokogiri (1.7.0.1) nokogiri (1.7.0.1)
mini_portile2 (~> 2.1.0) 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) public_suffix (2.0.5)
rack (2.0.1) rack (2.0.1)
rack-test (0.6.3) rack-test (0.6.3)
...@@ -52,6 +58,7 @@ GEM ...@@ -52,6 +58,7 @@ GEM
childprocess (~> 0.5) childprocess (~> 0.5)
rubyzip (~> 1.0) rubyzip (~> 1.0)
websocket (~> 1.0) websocket (~> 1.0)
slop (3.6.0)
websocket (1.2.4) websocket (1.2.4)
xpath (2.0.0) xpath (2.0.0)
nokogiri (~> 1.3) nokogiri (~> 1.3)
...@@ -62,7 +69,7 @@ PLATFORMS ...@@ -62,7 +69,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
capybara (~> 2.12.1) capybara (~> 2.12.1)
capybara-screenshot (~> 1.0.14) capybara-screenshot (~> 1.0.14)
capybara-webkit (~> 1.12.0) pry-byebug (~> 3.4.1)
rake (~> 12.0.0) rake (~> 12.0.0)
rspec (~> 3.5) rspec (~> 3.5)
selenium-webdriver (~> 2.53) selenium-webdriver (~> 2.53)
......
...@@ -4,8 +4,6 @@ module QA ...@@ -4,8 +4,6 @@ module QA
class Menu < Page::Base class Menu < Page::Base
def go_to_license def go_to_license
link = find_link 'License' link = find_link 'License'
# Click space to scroll this link into the view
link.send_keys(:space)
link.click link.click
end end
end end
......
...@@ -43,8 +43,7 @@ module QA ...@@ -43,8 +43,7 @@ module QA
Capybara.register_driver :chrome do |app| Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
'chromeOptions' => { 'chromeOptions' => {
'binary' => '/usr/bin/google-chrome-stable', 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680]
'args' => %w[headless no-sandbox disable-gpu window-size=1280,1024]
} }
) )
......
...@@ -96,18 +96,6 @@ describe Projects::MergeRequestsController do ...@@ -96,18 +96,6 @@ describe Projects::MergeRequestsController do
expect(response).to match_response_schema('entities/merge_request') expect(response).to match_response_schema('entities/merge_request')
end end
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 end
describe "as diff" do describe "as diff" do
......
...@@ -42,6 +42,13 @@ describe Projects::Registry::RepositoriesController do ...@@ -42,6 +42,13 @@ describe Projects::Registry::RepositoriesController do
expect { go_to_index }.to change { ContainerRepository.all.count }.by(1) expect { go_to_index }.to change { ContainerRepository.all.count }.by(1)
expect(ContainerRepository.first).to be_root_repository expect(ContainerRepository.first).to be_root_repository
end 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 end
context 'when there are no tags for this repository' do context 'when there are no tags for this repository' do
...@@ -58,6 +65,31 @@ describe Projects::Registry::RepositoriesController do ...@@ -58,6 +65,31 @@ describe Projects::Registry::RepositoriesController do
it 'does not ensure root container repository' do it 'does not ensure root container repository' do
expect { go_to_index }.not_to change { ContainerRepository.all.count } expect { go_to_index }.not_to change { ContainerRepository.all.count }
end 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 end
end end
...@@ -77,8 +109,16 @@ describe Projects::Registry::RepositoriesController do ...@@ -77,8 +109,16 @@ describe Projects::Registry::RepositoriesController do
end end
end end
def go_to_index def go_to_index(format: :html)
get :index, namespace_id: project.namespace, 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
end end
...@@ -4,24 +4,83 @@ describe Projects::Registry::TagsController do ...@@ -4,24 +4,83 @@ describe Projects::Registry::TagsController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :private) } let(:project) { create(:project, :private) }
let(:repository) do
create(:container_repository, name: 'image', project: project)
end
before do before do
sign_in(user) sign_in(user)
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
end 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
stub_container_registry_tags(repository: /image/, tags: tags)
end
context 'when user can control the registry' do
before do before do
project.add_developer(user) project.add_developer(user)
end 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 describe 'POST destroy' do
context 'when there is matching tag present' do context 'when user has access to registry' do
before do before do
stub_container_registry_tags(repository: /image/, tags: %w[rc1 test.]) project.add_developer(user)
end end
let(:repository) do context 'when there is matching tag present' do
create(:container_repository, name: 'image', project: project) before do
stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.])
end end
it 'makes it possible to delete regular tag' do it 'makes it possible to delete regular tag' do
...@@ -37,12 +96,15 @@ describe Projects::Registry::TagsController do ...@@ -37,12 +96,15 @@ describe Projects::Registry::TagsController do
end end
end end
end end
end
private
def destroy_tag(name) def destroy_tag(name)
post :destroy, namespace_id: project.namespace, post :destroy, namespace_id: project.namespace,
project_id: project, project_id: project,
repository_id: repository, repository_id: repository,
id: name id: name,
format: :json
end
end end
end end
...@@ -12,7 +12,7 @@ FactoryGirl.define do ...@@ -12,7 +12,7 @@ FactoryGirl.define do
deployment.project ||= deployment.environment.project deployment.project ||= deployment.environment.project
unless deployment.project.repository_exists? unless deployment.project.repository_exists?
allow(deployment.project.repository).to receive(:fetch_ref) allow(deployment.project.repository).to receive(:create_ref)
end end
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe "Container Registry" do describe "Container Registry", js: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
...@@ -41,16 +41,19 @@ describe "Container Registry" do ...@@ -41,16 +41,19 @@ describe "Container Registry" do
expect_any_instance_of(ContainerRepository) expect_any_instance_of(ContainerRepository)
.to receive(:delete_tags!).and_return(true) .to receive(:delete_tags!).and_return(true)
click_on 'Remove repository' click_on(class: 'js-remove-repo')
end end
scenario 'user removes a specific tag from container repository' do scenario 'user removes a specific tag from container repository' do
visit_container_registry visit_container_registry
find('.js-toggle-repo').trigger('click')
wait_for_requests
expect_any_instance_of(ContainerRegistry::Tag) expect_any_instance_of(ContainerRegistry::Tag)
.to receive(:delete).and_return(true) .to receive(:delete).and_return(true)
click_on 'Remove tag' click_on(class: 'js-delete-registry')
end end
end end
......
...@@ -29,7 +29,7 @@ feature 'Download buttons in branches page' do ...@@ -29,7 +29,7 @@ feature 'Download buttons in branches page' do
describe 'when checking branches' do describe 'when checking branches' do
context 'with artifacts' do context 'with artifacts' do
before do before do
visit project_branches_path(project) visit project_branches_path(project, search: 'binary-encoding')
end end
scenario 'shows download artifacts button' do scenario 'shows download artifacts button' do
......
...@@ -5,12 +5,6 @@ describe 'Branches' do ...@@ -5,12 +5,6 @@ describe 'Branches' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:repository) { project.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 context 'logged in as developer' do
before do before do
sign_in(user) sign_in(user)
...@@ -18,12 +12,10 @@ describe 'Branches' do ...@@ -18,12 +12,10 @@ describe 'Branches' do
end end
describe 'Initial branches page' do 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) visit project_branches_path(project)
repository.branches_sorted_by(:name).first(20).each do |branch| expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc))
expect(page).to have_content("#{branch.name}")
end
end end
it 'sorts the branches by name' do it 'sorts the branches by name' do
...@@ -32,22 +24,7 @@ describe 'Branches' do ...@@ -32,22 +24,7 @@ describe 'Branches' do
click_button "Last updated" # Open sorting dropdown click_button "Last updated" # Open sorting dropdown
click_link "Name" click_link "Name"
sorted = repository.branches_sorted_by(:name).first(20).map do |branch| expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name))
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(".*")}/)
end end
it 'sorts the branches by oldest updated' do it 'sorts the branches by oldest updated' do
...@@ -56,10 +33,7 @@ describe 'Branches' do ...@@ -56,10 +33,7 @@ describe 'Branches' do
click_button "Last updated" # Open sorting dropdown click_button "Last updated" # Open sorting dropdown
click_link "Oldest updated" click_link "Oldest updated"
sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch| expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc))
Regexp.escape(branch.name)
end
expect(page).to have_content(/#{sorted.join(".*")}/)
end end
it 'avoids a N+1 query in branches index' do it 'avoids a N+1 query in branches index' do
...@@ -99,28 +73,6 @@ describe 'Branches' do ...@@ -99,28 +73,6 @@ describe 'Branches' do
expect(find('.all-branches')).to have_selector('li', count: 0) expect(find('.all-branches')).to have_selector('li', count: 0)
end end
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 end
context 'logged in as master' do context 'logged in as master' do
...@@ -136,37 +88,6 @@ describe 'Branches' do ...@@ -136,37 +88,6 @@ describe 'Branches' do
expect(page).to have_content("Protected branches can be managed in project settings") expect(page).to have_content("Protected branches can be managed in project settings")
end end
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 end
context 'logged out' do context 'logged out' do
...@@ -180,4 +101,13 @@ describe 'Branches' do ...@@ -180,4 +101,13 @@ describe 'Branches' do
end end
end 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 end
require 'spec_helper' require 'spec_helper'
feature 'Protected Branches', js: true do feature 'Protected Branches', :js do
let(:user) { create(:user, :admin) } let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
context 'logged in as developer' do
before do before do
project.add_developer(user)
sign_in(user) sign_in(user)
end end
def set_protected_branch_name(branch_name) describe 'Delete protected branch' do
find(".js-protected-branch-select").trigger('click') before do
find(".dropdown-input-field").set(branch_name) create(:protected_branch, project: project, name: 'fix')
click_on("Create wildcard #{branch_name}") 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)
expect(page).to have_css('.btn-remove.disabled')
end
end
end
context 'logged in as master' do
before do
project.add_master(user)
sign_in(user)
end
describe 'Delete protected branch' do
before do
create(:protected_branch, project: project, name: 'fix')
expect(ProtectedBranch.count).to eq(1)
end
it 'removes branch after modal confirmation' 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 in as admin' do
before do
sign_in(admin)
end end
describe "explicit protected branches" do describe "explicit protected branches" do
...@@ -27,7 +80,7 @@ feature 'Protected Branches', js: true do ...@@ -27,7 +80,7 @@ feature 'Protected Branches', js: true do
it "displays the last commit on the matching branch if it exists" do it "displays the last commit on the matching branch if it exists" do
commit = create(:commit, project: project) commit = create(:commit, project: project)
project.repository.add_branch(user, 'some-branch', commit.id) project.repository.add_branch(admin, 'some-branch', commit.id)
visit project_protected_branches_path(project) visit project_protected_branches_path(project)
set_protected_branch_name('some-branch') set_protected_branch_name('some-branch')
...@@ -57,8 +110,8 @@ feature 'Protected Branches', js: true do ...@@ -57,8 +110,8 @@ feature 'Protected Branches', js: true do
end end
it "displays the number of matching branches" do it "displays the number of matching branches" do
project.repository.add_branch(user, 'production-stable', 'master') project.repository.add_branch(admin, 'production-stable', 'master')
project.repository.add_branch(user, 'staging-stable', 'master') project.repository.add_branch(admin, 'staging-stable', 'master')
visit project_protected_branches_path(project) visit project_protected_branches_path(project)
set_protected_branch_name('*-stable') set_protected_branch_name('*-stable')
...@@ -68,9 +121,9 @@ feature 'Protected Branches', js: true do ...@@ -68,9 +121,9 @@ feature 'Protected Branches', js: true do
end end
it "displays all the branches matching the wildcard" do it "displays all the branches matching the wildcard" do
project.repository.add_branch(user, 'production-stable', 'master') project.repository.add_branch(admin, 'production-stable', 'master')
project.repository.add_branch(user, 'staging-stable', 'master') project.repository.add_branch(admin, 'staging-stable', 'master')
project.repository.add_branch(user, 'development', 'master') project.repository.add_branch(admin, 'development', 'master')
visit project_protected_branches_path(project) visit project_protected_branches_path(project)
set_protected_branch_name('*-stable') set_protected_branch_name('*-stable')
...@@ -90,4 +143,11 @@ feature 'Protected Branches', js: true do ...@@ -90,4 +143,11 @@ feature 'Protected Branches', js: true do
describe "access control" do describe "access control" do
include_examples "protected branches > access control > CE" include_examples "protected branches > access control > CE"
end end
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
end end
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
"merge_commit_message_with_description": { "type": "string" }, "merge_commit_message_with_description": { "type": "string" },
"diverged_commits_count": { "type": "integer" }, "diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" }, "commit_change_content_path": { "type": "string" },
"remove_wip_path": { "type": "string" }, "remove_wip_path": { "type": ["string", "null"] },
"commits_count": { "type": "integer" }, "commits_count": { "type": "integer" },
"remove_source_branch": { "type": ["boolean", "null"] }, "remove_source_branch": { "type": ["boolean", "null"] },
"merge_ongoing": { "type": "boolean" }, "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 ...@@ -41,6 +41,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
remove_repository(project) remove_repository(project)
end 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| it 'merge_requests/merge_request_with_task_list.html.raw' do |example|
create(:ci_build, :pending, pipeline: pipeline) create(:ci_build, :pending, pipeline: pipeline)
......
...@@ -58,5 +58,44 @@ import IssuablesHelper from '~/helpers/issuables_helper'; ...@@ -58,5 +58,44 @@ import IssuablesHelper from '~/helpers/issuables_helper';
expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled(); 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); }).call(window);
...@@ -33,6 +33,30 @@ describe('issue_comment_form component', () => { ...@@ -33,6 +33,30 @@ describe('issue_comment_form component', () => {
expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); 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', () => { describe('textarea', () => {
it('should render textarea with placeholder', () => { it('should render textarea with placeholder', () => {
expect( expect(
...@@ -40,6 +64,22 @@ describe('issue_comment_form component', () => { ...@@ -40,6 +64,22 @@ describe('issue_comment_form component', () => {
).toEqual('Write a comment or drag your files here...'); ).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', () => { it('should support quick actions', () => {
expect( expect(
vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'),
......
import * as actions from '~/notes/stores/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'; import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
describe('Actions Notes Store', () => { 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', () => { ...@@ -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', () => { describe('mergeButtonClass', () => {
const defaultClass = 'btn btn-sm btn-success accept-merge-request'; const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`; const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`; 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; vm.mr.pipeline = true;
expect(vm.mergeButtonClass).toEqual(defaultClass); 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; vm.mr.hasCI = true;
expect(vm.mergeButtonClass).toEqual(failedClass); expect(vm.mergeButtonClass).toEqual(failedClass);
}); });
});
it('should return default class when MR has no pipeline', () => { describe('status icon', () => {
expect(vm.mergeButtonClass).toEqual(defaultClass); 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.pipeline = {};
vm.mr.isPipelineActive = true; vm.mr.isPipelineActive = true;
expect(vm.mergeButtonClass).toEqual(inActionClass); expect(vm.iconClass).toEqual('success');
}); });
it('should return failed class when pipeline is failed', () => { it('shows x for failed status', () => {
vm.mr.pipeline = {}; vm.mr.hasCI = true;
vm.mr.isPipelineFailed = true; expect(vm.iconClass).toEqual('failed');
expect(vm.mergeButtonClass).toEqual(failedClass); });
it('shows x for merge not allowed', () => {
vm.mr.hasCI = true;
expect(vm.iconClass).toEqual('failed');
}); });
}); });
...@@ -177,7 +226,7 @@ describe('MRWidgetReadyToMerge', () => { ...@@ -177,7 +226,7 @@ describe('MRWidgetReadyToMerge', () => {
expect(vm.isMergeButtonDisabled).toBeTruthy(); 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; vm.isMakingRequest = true;
expect(vm.isMergeButtonDisabled).toBeTruthy(); expect(vm.isMergeButtonDisabled).toBeTruthy();
}); });
......
...@@ -1444,6 +1444,51 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1444,6 +1444,51 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
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) def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name } source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged rugged = repository.rugged
......
...@@ -4,10 +4,10 @@ describe Gitlab::GitalyClient::OperationService do ...@@ -4,10 +4,10 @@ describe Gitlab::GitalyClient::OperationService do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:repository) { project.repository.raw } let(:repository) { project.repository.raw }
let(:client) { described_class.new(repository) } let(:client) { described_class.new(repository) }
describe '#user_create_branch' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) } let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) }
describe '#user_create_branch' do
let(:branch_name) { 'new' } let(:branch_name) { 'new' }
let(:start_point) { 'master' } let(:start_point) { 'master' }
let(:request) do let(:request) do
...@@ -52,4 +52,41 @@ describe Gitlab::GitalyClient::OperationService do ...@@ -52,4 +52,41 @@ describe Gitlab::GitalyClient::OperationService do
end end
end 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 end
...@@ -22,5 +22,12 @@ describe Gitlab::SQL::Union do ...@@ -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 {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)}") expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
end 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
end end
...@@ -155,5 +155,15 @@ describe Key, :mailer do ...@@ -155,5 +155,15 @@ describe Key, :mailer do
it 'strips white spaces' do it 'strips white spaces' do
expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end 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
end end
...@@ -791,6 +791,49 @@ describe MergeRequest do ...@@ -791,6 +791,49 @@ describe MergeRequest do
end end
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 describe '#all_pipelines' do
shared_examples 'returning pipelines with proper ordering' do shared_examples 'returning pipelines with proper ordering' do
let!(:all_pipelines) do let!(:all_pipelines) do
......
...@@ -636,18 +636,18 @@ describe Repository do ...@@ -636,18 +636,18 @@ describe Repository do
describe '#fetch_ref' do describe '#fetch_ref' do
describe 'when storage is broken', broken_storage: true do describe 'when storage is broken', broken_storage: true do
it 'should raise a storage error' do it 'should raise a storage error' do
path = broken_repository.path_to_repo expect_to_raise_storage_error do
broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
expect_to_raise_storage_error { broken_repository.fetch_ref(path, '1', '2') } end
end end
end end
end end
describe '#create_ref' do 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' 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) repository.create_ref(ref, ref_path)
end end
...@@ -901,47 +901,6 @@ describe Repository do ...@@ -901,47 +901,6 @@ describe Repository do
end end
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 describe '#update_branch_with_hooks' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
...@@ -1744,8 +1703,7 @@ describe Repository do ...@@ -1744,8 +1703,7 @@ describe Repository do
end end
describe '#rm_branch' do describe '#rm_branch' do
let(:user) { create(:user) } shared_examples "user deleting a branch" do
it 'removes a branch' do it 'removes a branch' do
expect(repository).to receive(:before_remove_branch) expect(repository).to receive(:before_remove_branch)
expect(repository).to receive(:after_remove_branch) expect(repository).to receive(:after_remove_branch)
...@@ -1754,6 +1712,69 @@ describe Repository do ...@@ -1754,6 +1712,69 @@ describe Repository do
end end
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')
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
end
describe '#rm_tag' do describe '#rm_tag' do
shared_examples 'removing tag' do shared_examples 'removing tag' do
it 'removes a tag' do it 'removes a tag' do
......
...@@ -300,6 +300,10 @@ describe MergeRequestPresenter do ...@@ -300,6 +300,10 @@ describe MergeRequestPresenter do
described_class.new(resource, current_user: user).remove_wip_path described_class.new(resource, current_user: user).remove_wip_path
end end
before do
allow(resource).to receive(:work_in_progress?).and_return(true)
end
context 'when merge request enabled and has permission' do context 'when merge request enabled and has permission' do
it 'has remove_wip_path' do it 'has remove_wip_path' do
allow(project).to receive(:merge_requests_enabled?) { true } 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 ...@@ -11,16 +11,6 @@ describe MergeRequestEntity do
described_class.new(resource, request: request).as_json described_class.new(resource, request: request).as_json
end 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 it 'includes pipeline' do
req = double('request', current_user: user) req = double('request', current_user: user)
pipeline = build_stubbed(:ci_pipeline) pipeline = build_stubbed(:ci_pipeline)
......
...@@ -39,11 +39,11 @@ module StubGitlabCalls ...@@ -39,11 +39,11 @@ module StubGitlabCalls
.and_return({ 'tags' => tags }) .and_return({ 'tags' => tags })
allow_any_instance_of(ContainerRegistry::Client) 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) .and_return(stub_container_registry_tag_manifest)
allow_any_instance_of(ContainerRegistry::Client) 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) .and_return(stub_container_registry_blob)
end 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