diff options
author | Shinya Maeda <shinya@gitlab.com> | 2017-10-05 14:49:16 +0900 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2017-10-05 14:49:16 +0900 |
commit | 88cc9d5294198cfa748d602236729abcd73f56a6 (patch) | |
tree | 6eb5be9ed46ec720ba195702d80e166980868146 | |
parent | d6e22e83d1fe8ffaa71c3bcb7906731733a89f0e (diff) | |
parent | 8921af39e74976e37e92c786bd957883110f6522 (diff) | |
download | gitlab-ce-88cc9d5294198cfa748d602236729abcd73f56a6.tar.gz |
Merge branch 'master' into feature/sm/35954-create-kubernetes-cluster-on-gke-from-k8s-service
215 files changed, 2722 insertions, 942 deletions
diff --git a/.flayignore b/.flayignore index b63ce4c4df0..acac0ce14c9 100644 --- a/.flayignore +++ b/.flayignore @@ -5,3 +5,4 @@ app/policies/project_policy.rb app/models/concerns/relative_positioning.rb app/workers/stuck_merge_jobs_worker.rb lib/gitlab/redis/*.rb +lib/gitlab/gitaly_client/operation_service.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index a31817e2b8d..15d9117976a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -195,6 +195,10 @@ entry. - Added type to CHANGELOG entries. (Jacopo Beschi @jacopo-beschi) - [BUGIFX] Improves subgroup creation permissions. !13418 +## 9.5.7 (2017-10-03) + +- Fix gitlab rake:import:repos task. + ## 9.5.6 (2017-09-29) - [FIXED] Fix MR ready to merge buttons/controls at mobile breakpoint. !14242 diff --git a/Gemfile.lock b/Gemfile.lock index a0ad2716c01..4622fd141ba 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -910,7 +910,7 @@ GEM json (>= 1.8.0) unf (0.1.4) unf_ext - unf_ext (0.0.7.2) + unf_ext (0.0.7.4) unicode-display_width (1.3.0) unicorn (5.1.0) kgio (~> 2.6) diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index af718e894cf..ce05b3eabec 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -1,30 +1,12 @@ import Jed from 'jed'; - import sprintf from './sprintf'; -/** - This is required to require all the translation folders in the current directory - this saves us having to do this manually & keep up to date with new languages -**/ -function requireAll(requireContext) { return requireContext.keys().map(requireContext); } - -const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/)); -const locales = allLocales.reduce((d, obj) => { - const data = d; - const localeKey = Object.keys(obj)[0]; - - data[localeKey] = obj[localeKey]; - - return data; -}, {}); - const langAttribute = document.querySelector('html').getAttribute('lang'); const lang = (langAttribute || 'en').replace(/-/g, '_'); -const locale = new Jed(locales[lang]); +const locale = new Jed(window.translations || {}); /** Translates `text` - @param text The text to be translated @returns {String} The translated text **/ diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 0db2abe507d..af0658eb668 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper'; $el.text(gl.text.addDelimiter(count)); }; + MergeRequest.prototype.hideCloseButton = function() { + const el = document.querySelector('.merge-request .issuable-actions'); + const closeDropdownItem = el.querySelector('li.close-item'); + if (closeDropdownItem) { + closeDropdownItem.classList.add('hidden'); + // Selects the next dropdown item + el.querySelector('li.report-item').click(); + } else { + // No dropdown just hide the Close button + el.querySelector('.btn-close').classList.add('hidden'); + } + // Dropdown for mobile screen + el.querySelector('li.js-close-item').classList.add('hidden'); + }; + return MergeRequest; })(); }).call(window); diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue index b8a16356576..b4067d229aa 100644 --- a/app/assets/javascripts/notebook/cells/code.vue +++ b/app/assets/javascripts/notebook/cells/code.vue @@ -1,18 +1,3 @@ -<template> - <div class="cell"> - <code-cell - type="input" - :raw-code="rawInputCode" - :count="cell.execution_count" - :code-css-class="codeCssClass" /> - <output-cell - v-if="hasOutput" - :count="cell.execution_count" - :output="output" - :code-css-class="codeCssClass" /> - </div> -</template> - <script> import CodeCell from './code/index.vue'; import OutputCell from './output/index.vue'; @@ -51,6 +36,21 @@ export default { }; </script> +<template> + <div class="cell"> + <code-cell + type="input" + :raw-code="rawInputCode" + :count="cell.execution_count" + :code-css-class="codeCssClass" /> + <output-cell + v-if="hasOutput" + :count="cell.execution_count" + :output="output" + :code-css-class="codeCssClass" /> + </div> +</template> + <style scoped> .cell { flex-direction: column; diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue index 31b30f601e2..0f3083f05b2 100644 --- a/app/assets/javascripts/notebook/cells/code/index.vue +++ b/app/assets/javascripts/notebook/cells/code/index.vue @@ -1,17 +1,3 @@ -<template> - <div :class="type"> - <prompt - :type="promptType" - :count="count" /> - <pre - class="language-python" - :class="codeCssClass" - ref="code" - v-text="code"> - </pre> - </div> -</template> - <script> import Prism from '../../lib/highlight'; import Prompt from '../prompt.vue'; @@ -55,3 +41,17 @@ }, }; </script> + +<template> + <div :class="type"> + <prompt + :type="promptType" + :count="count" /> + <pre + class="language-python" + :class="codeCssClass" + ref="code" + v-text="code"> + </pre> + </div> +</template> diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index 814d2ea92b4..82c51a1068c 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -1,10 +1,3 @@ -<template> - <div class="cell text-cell"> - <prompt /> - <div class="markdown" v-html="markdown"></div> - </div> -</template> - <script> /* global katex */ import marked from 'marked'; @@ -95,6 +88,13 @@ }; </script> +<template> + <div class="cell text-cell"> + <prompt /> + <div class="markdown" v-html="markdown"></div> + </div> +</template> + <style> .markdown .katex { display: block; diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index 0f39cd138df..2110a9de7ed 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -1,10 +1,3 @@ -<template> - <div class="output"> - <prompt /> - <div v-html="rawCode"></div> - </div> -</template> - <script> import Prompt from '../prompt.vue'; @@ -20,3 +13,10 @@ export default { }, }; </script> + +<template> + <div class="output"> + <prompt /> + <div v-html="rawCode"></div> + </div> +</template> diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue index f3b873bbc0f..fbb39ea6e2d 100644 --- a/app/assets/javascripts/notebook/cells/output/image.vue +++ b/app/assets/javascripts/notebook/cells/output/image.vue @@ -1,11 +1,3 @@ -<template> - <div class="output"> - <prompt /> - <img - :src="'data:' + outputType + ';base64,' + rawCode" /> - </div> -</template> - <script> import Prompt from '../prompt.vue'; @@ -25,3 +17,11 @@ export default { }, }; </script> + +<template> + <div class="output"> + <prompt /> + <img + :src="'data:' + outputType + ';base64,' + rawCode" /> + </div> +</template> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 23c9ea78939..05af0bf1e8e 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -1,12 +1,3 @@ -<template> - <component :is="componentName" - type="output" - :outputType="outputType" - :count="count" - :raw-code="rawCode" - :code-css-class="codeCssClass" /> -</template> - <script> import CodeCell from '../code/index.vue'; import Html from './html.vue'; @@ -81,3 +72,12 @@ export default { }, }; </script> + +<template> + <component :is="componentName" + type="output" + :outputType="outputType" + :count="count" + :raw-code="rawCode" + :code-css-class="codeCssClass" /> +</template> diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue index 4540e4248d8..039fb99293d 100644 --- a/app/assets/javascripts/notebook/cells/prompt.vue +++ b/app/assets/javascripts/notebook/cells/prompt.vue @@ -1,11 +1,3 @@ -<template> - <div class="prompt"> - <span v-if="type && count"> - {{ type }} [{{ count }}]: - </span> - </div> -</template> - <script> export default { props: { @@ -21,6 +13,14 @@ }; </script> +<template> + <div class="prompt"> + <span v-if="type && count"> + {{ type }} [{{ count }}]: + </span> + </div> +</template> + <style scoped> .prompt { padding: 0 10px; diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue index fd62c1231ef..e88806431af 100644 --- a/app/assets/javascripts/notebook/index.vue +++ b/app/assets/javascripts/notebook/index.vue @@ -1,14 +1,3 @@ -<template> - <div v-if="hasNotebook"> - <component - v-for="(cell, index) in cells" - :is="cellType(cell.cell_type)" - :cell="cell" - :key="index" - :code-css-class="codeCssClass" /> - </div> -</template> - <script> import { MarkdownCell, @@ -59,6 +48,17 @@ }; </script> +<template> + <div v-if="hasNotebook"> + <component + v-for="(cell, index) in cells" + :is="cellType(cell.cell_type)" + :cell="cell" + :key="index" + :code-css-class="codeCssClass" /> + </div> +</template> + <style> .cell, .input, diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index fa7ac994058..1a7da84a424 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -272,6 +272,7 @@ v-model="note" ref="textarea" slot="textarea" + :disabled="isSubmitting" placeholder="Write a comment or drag your files here..." @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()"> diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index b874e484d45..c8a2f778ee8 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -1,13 +1,3 @@ -<template> - <div class="pdf-viewer" v-if="hasPDF"> - <page v-for="(page, index) in pages" - :key="index" - :v-if="!loading" - :page="page" - :number="index + 1" /> - </div> -</template> - <script> import pdfjsLib from 'vendor/pdf'; import workerSrc from 'vendor/pdf.worker.min'; @@ -64,6 +54,16 @@ }; </script> +<template> + <div class="pdf-viewer" v-if="hasPDF"> + <page v-for="(page, index) in pages" + :key="index" + :v-if="!loading" + :page="page" + :number="index + 1" /> + </div> +</template> + <style> .pdf-viewer { background: url('./assets/img/bg.gif'); diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index 7b74ee4eb2e..be38f7cc129 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -1,10 +1,3 @@ -<template> - <canvas - class="pdf-page" - ref="canvas" - :data-page="number" /> -</template> - <script> export default { props: { @@ -48,6 +41,13 @@ }; </script> +<template> + <canvas + class="pdf-page" + ref="canvas" + :data-page="number" /> +</template> + <style> .pdf-page { margin: 8px auto 0 auto; diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index 68cf47fd54e..65d46fa9a73 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -1,8 +1,7 @@ export default () => { - $('.fork-thumbnail a').on('click', function forkThumbnailClicked() { + $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() { if ($(this).hasClass('disabled')) return false; - $('.fork-namespaces').hide(); - return $('.save-project-loader').show(); + return $('.js-fork-content').toggle(); }); }; diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue new file mode 100644 index 00000000000..2d8ca443ea7 --- /dev/null +++ b/app/assets/javascripts/registry/components/app.vue @@ -0,0 +1,62 @@ +<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> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue new file mode 100644 index 00000000000..41ea9742406 --- /dev/null +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -0,0 +1,131 @@ +<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> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue new file mode 100644 index 00000000000..4ce1571b0aa --- /dev/null +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -0,0 +1,137 @@ +<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"> + · + </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> diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/constants.js new file mode 100644 index 00000000000..712b0fade3d --- /dev/null +++ b/app/assets/javascripts/registry/constants.js @@ -0,0 +1,15 @@ +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.'), +}; diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js new file mode 100644 index 00000000000..d8edff73f72 --- /dev/null +++ b/app/assets/javascripts/registry/index.js @@ -0,0 +1,25 @@ +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, + }, + }); + }, +})); diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js new file mode 100644 index 00000000000..34ed40b8b65 --- /dev/null +++ b/app/assets/javascripts/registry/stores/actions.js @@ -0,0 +1,39 @@ +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); diff --git a/app/assets/javascripts/registry/stores/getters.js b/app/assets/javascripts/registry/stores/getters.js new file mode 100644 index 00000000000..588f479c492 --- /dev/null +++ b/app/assets/javascripts/registry/stores/getters.js @@ -0,0 +1,2 @@ +export const isLoading = state => state.isLoading; +export const repos = state => state.repos; diff --git a/app/assets/javascripts/registry/stores/index.js b/app/assets/javascripts/registry/stores/index.js new file mode 100644 index 00000000000..78b67881210 --- /dev/null +++ b/app/assets/javascripts/registry/stores/index.js @@ -0,0 +1,39 @@ +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, +}); diff --git a/app/assets/javascripts/registry/stores/mutation_types.js b/app/assets/javascripts/registry/stores/mutation_types.js new file mode 100644 index 00000000000..2c69bf11807 --- /dev/null +++ b/app/assets/javascripts/registry/stores/mutation_types.js @@ -0,0 +1,7 @@ +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'; diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js new file mode 100644 index 00000000000..e40382e7afc --- /dev/null +++ b/app/assets/javascripts/registry/stores/mutations.js @@ -0,0 +1,54 @@ +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; + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js index aaf9d3304a4..09561694939 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="loading" showDisabledButton /> + <status-icon status="loading" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> Checking ability to merge automatically diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js index dc252f8a9b7..5d468a085cb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js @@ -12,7 +12,7 @@ export default { <div class="mr-widget-body media"> <status-icon status="failed" - showDisabledButton /> + :show-disabled-button="true" /> <div class="media-body space-children"> <span v-if="mr.shouldBeRebased" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js index 1cb24549d53..c25d6c359bb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js @@ -51,7 +51,7 @@ export default { </span> </template> <template v-else> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> <span diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js index 9f0a359d01a..1bc0b7e0819 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js @@ -24,7 +24,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold js-branch-text"> <span class="capitalize"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js index 797511d4e3a..00047718201 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="success" showDisabledButton /> + <status-icon status="success" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> Ready to be merged automatically. diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js index 167a0d4613a..1cedf86e811 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> Pipeline blocked. The pipeline for this merge request requires a manual action to proceed diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js index c5be9a0530a..6853ba4b9f8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index f83d3ca00dd..edc1d191bf2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -38,24 +38,40 @@ export default { return this.useCommitMessageWithDescription ? withoutDesc : withDesc; }, - mergeButtonClass() { - const defaultClass = 'btn btn-sm btn-success accept-merge-request'; - const failedClass = `${defaultClass} btn-danger`; - const inActionClass = `${defaultClass} btn-info`; + status() { const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; if (hasCI && !ciStatus) { - return failedClass; + return 'failed'; } else if (!pipeline) { - return defaultClass; + return 'success'; } else if (isPipelineActive) { - return inActionClass; + return 'pending'; } else if (isPipelineFailed) { + return 'failed'; + } + + return 'success'; + }, + mergeButtonClass() { + const defaultClass = 'btn btn-sm btn-success accept-merge-request'; + const failedClass = `${defaultClass} btn-danger`; + const inActionClass = `${defaultClass} btn-info`; + + if (this.status === 'failed') { return failedClass; + } else if (this.status === 'pending') { + return inActionClass; } return defaultClass; }, + iconClass() { + if (this.status === 'failed' || !this.commitMessage.length || !this.isMergeAllowed() || this.mr.preventMerge) { + return 'failed'; + } + return 'success'; + }, mergeButtonText() { if (this.isMergingImmediately) { return 'Merge in progress'; @@ -156,6 +172,7 @@ export default { eventHub.$emit('FetchActionsContent'); if (window.mergeRequest) { window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged'); + window.mergeRequest.hideCloseButton(); window.mergeRequest.decreaseCounter(); } stopPolling(); @@ -208,7 +225,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="success" /> + <status-icon :status="iconClass" /> <div class="media-body"> <div class="mr-widget-body-controls media space-children"> <span class="btn-group append-bottom-5"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js index 89f38e5bd2a..af19cf6ab87 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js @@ -7,7 +7,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> The source branch HEAD has recently changed. Please reload the page and review the changes before merging diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js index d762ca6e640..a119ecbbdfe 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js @@ -10,7 +10,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" showDisabledButton /> + <status-icon status="failed" :show-disabled-button="true" /> <div class="media-body space-children"> <span class="bold"> There are unresolved discussions. Please resolve these discussions diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js index b11a06899cf..54be1fbe675 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js @@ -38,7 +38,7 @@ export default { }, template: ` <div class="mr-widget-body media"> - <status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" /> + <status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" /> <div class="media-body space-children"> <span class="bold"> This is a Work in Progress diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue new file mode 100644 index 00000000000..3a7143c450e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -0,0 +1,32 @@ +<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> diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index bdcbd4021b3..f1aedc227f3 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -23,6 +23,7 @@ &.s60 { @include avatar-size(60px, 12px); } &.s70 { @include avatar-size(70px, 14px); } &.s90 { @include avatar-size(90px, 15px); } + &.s100 { @include avatar-size(100px, 15px); } &.s110 { @include avatar-size(110px, 15px); } &.s140 { @include avatar-size(140px, 15px); } &.s160 { @include avatar-size(160px, 20px); } @@ -78,6 +79,7 @@ &.s60 { font-size: 32px; line-height: 58px; } &.s70 { font-size: 34px; line-height: 70px; } &.s90 { font-size: 36px; line-height: 88px; } + &.s100 { font-size: 36px; line-height: 98px; } &.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; } &.s140 { font-size: 72px; line-height: 138px; } &.s160 { font-size: 96px; line-height: 158px; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 706a9cffe87..96f9dda26c4 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -11,6 +11,7 @@ .prepend-top-10 { margin-top: 10px; } .prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-20 { margin-top: 20px; } +.prepend-left-4 { margin-left: 4px; } .prepend-left-5 { margin-left: 5px; } .prepend-left-10 { margin-left: 10px; } .prepend-left-default { margin-left: $gl-padding; } @@ -129,11 +130,6 @@ span.update-author { } } -.user-mention { - color: $user-mention-color; - font-weight: $gl-font-weight-bold; -} - .field_with_errors { display: inline; } diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index dbdd5a4464b..34a35734acc 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -6,3 +6,14 @@ .gfm-commit_range { @extend .commit-sha; } + +.gfm-project_member { + padding: 0 2px; + border-radius: #{$border-radius-default / 2}; + background-color: $user-mention-bg; + + &:hover { + background-color: $user-mention-bg-hover; + text-decoration: none; + } +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 6c14e8b97e0..50f1445bc2e 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -48,31 +48,24 @@ } &:hover { - background-color: $white-normal; - border-color: $border-white-normal; + border-color: $gray-darkest; color: $gl-text-color; } } } -.select2-drop { - box-shadow: $select2-drop-shadow1 0 0 1px 0, $select2-drop-shadow2 0 2px 18px 0; - border-radius: $border-radius-default; - border: none; +.select2-drop, +.select2-drop.select2-drop-above { + box-shadow: 0 2px 4px $dropdown-shadow-color; + border-radius: $border-radius-base; + border: 1px solid $dropdown-border-color; min-width: 175px; + color: $gl-text-color; } -.select2-results .select2-result-label, -.select2-more-results { - padding: 10px 15px; -} - -.select2-drop { - color: $gl-grayish-blue; -} - -.select2-highlighted { - background: $gl-link-color !important; +.select2-drop.select2-drop-above.select2-drop-active { + border-top: 1px solid $dropdown-border-color; + margin-top: -6px; } .select2-results li.select2-result-with-children > .select2-result-label { @@ -87,13 +80,11 @@ } } -.select2-dropdown-open { +.select2-dropdown-open, +.select2-dropdown-open.select2-drop-above { .select2-choice { - border-color: $border-white-normal; + border-color: $gray-darkest; outline: 0; - background-image: none; - background-color: $white-dark; - box-shadow: $gl-btn-active-gradient; } } @@ -131,28 +122,14 @@ } } } - - &.select2-container-active .select2-choices, - &.select2-dropdown-open .select2-choices { - border-color: $border-white-normal; - box-shadow: $gl-btn-active-gradient; - } } .select2-drop-active { - margin-top: 6px; + margin-top: $dropdown-vertical-offset; font-size: 14px; - &.select2-drop-above { - margin-bottom: 8px; - } - .select2-results { max-height: 350px; - - .select2-highlighted { - background: $gl-primary; - } } } @@ -186,19 +163,35 @@ background-size: 16px 16px !important; } -.select2-results .select2-no-results, -.select2-results .select2-searching, -.select2-results .select2-ajax-error, -.select2-results .select2-selection-limit { - background: $gray-light; - display: list-item; - padding: 10px 15px; -} - - .select2-results { margin: 0; - padding: 10px 0; + padding: #{$gl-padding / 2} 0; + + .select2-no-results, + .select2-searching, + .select2-ajax-error, + .select2-selection-limit { + background: transparent; + padding: #{$gl-padding / 2} $gl-padding; + } + + .select2-result-label, + .select2-more-results { + padding: #{$gl-padding / 2} $gl-padding; + } + + .select2-highlighted { + background: transparent; + color: $gl-text-color; + + .select2-result-label { + background: $dropdown-item-hover-bg; + } + } + + .select2-result { + padding: 0 1px; + } } .ajax-users-select { @@ -265,56 +258,10 @@ min-width: 250px !important; } -// TODO: change global style -.ajax-project-dropdown, -.ajax-users-dropdown, -body[data-page="projects:edit"] #select2-drop, -body[data-page="projects:new"] #select2-drop, -body[data-page="projects:merge_requests:edit"] #select2-drop, -body[data-page="projects:blob:new"] #select2-drop, -body[data-page="profiles:show"] #select2-drop, -body[data-page="admin:groups:show"] #select2-drop, -body[data-page="projects:issues:show"] #select2-drop, -body[data-page="projects:blob:edit"] #select2-drop { - &.select2-drop { - border: 1px solid $dropdown-border-color; - border-radius: $border-radius-base; - color: $gl-text-color; - } - - &.select2-drop-above { - border-top: none; - margin-top: -4px; - } - - .select2-results { - .select2-no-results, - .select2-searching, - .select2-ajax-error, - .select2-selection-limit { - background: transparent; - } - - .select2-result { - padding: 0 1px; - - .select2-match { - font-weight: $gl-font-weight-bold; - text-decoration: none; - } - - .select2-result-label { - padding: #{$gl-padding / 2} $gl-padding; - } - - &.select2-highlighted { - background-color: transparent !important; - color: $gl-text-color; - - .select2-result-label { - background-color: $dropdown-item-hover-bg; - } - } - } +.select2-result-selectable, +.select2-result-unselectable { + .select2-match { + font-weight: $gl-font-weight-bold; + text-decoration: none; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 9bbda87dec9..60260355765 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -262,7 +262,8 @@ $well-pre-bg: #eee; $well-pre-color: #555; $loading-color: #555; $update-author-color: #999; -$user-mention-color: #2fa0bb; +$user-mention-bg: rgba($blue-500, 0.044); +$user-mention-bg-hover: rgba($blue-500, 0.15); $time-color: #999; $project-member-show-color: #aaa; $gl-promo-color: #aaa; diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss index 3266714396e..dfff3e15556 100644 --- a/app/assets/stylesheets/pages/container_registry.scss +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -9,6 +9,14 @@ .container-image-head { padding: 0 16px; line-height: 4em; + + .btn-link { + padding: 0; + + &:focus { + outline: none; + } + } } .table.tags { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 1f7b6703909..a086c11324d 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -499,73 +499,56 @@ a.deploy-project-label { } } -.fork-namespaces { - .row { - -webkit-flex-wrap: wrap; - display: -webkit-flex; - display: flex; - flex-wrap: wrap; - justify-content: flex-start; +.fork-thumbnail { + height: 200px; + width: calc((100% / 2) - #{$gl-padding * 2}); - .fork-thumbnail { - border-radius: $border-radius-base; - background-color: $white-light; - border: 1px solid $border-white-light; - height: 202px; - margin: $gl-padding; - text-align: center; - width: 169px; + @media (min-width: $screen-md-min) { + width: calc((100% / 4) - #{$gl-padding * 2}); + } - &:hover:not(.disabled), - &.forked { - background-color: $row-hover; - border-color: $row-hover-border; - } + @media (min-width: $screen-lg-min) { + width: calc((100% / 5) - #{$gl-padding * 2}); + } - .no-avatar { - width: 100px; - height: 100px; - background-color: $gray-light; - border: 1px solid $white-normal; - margin: 0 auto; - border-radius: 50%; - - i { - font-size: 100px; - color: $white-normal; - } - } + &:hover:not(.disabled), + &.forked { + background-color: $row-hover; + border-color: $row-hover-border; + } - a { - display: block; - width: 100%; - height: 100%; - padding-top: $gl-padding; - color: $gl-text-color; - - &.disabled { - opacity: .3; - cursor: not-allowed; - - &:hover { - text-decoration: none; - } - } - - .caption { - min-height: 30px; - padding: $gl-padding 0; - } - } + .avatar-container, + .identicon { + float: none; + margin-left: auto; + margin-right: auto; + } - img { - border-radius: 50%; - max-width: 100px; - } + a { + display: block; + width: 100%; + height: 100%; + padding-top: $gl-padding; + text-decoration: none; + + &.disabled { + opacity: .3; + cursor: not-allowed; } } } +.fork-thumbnail-container { + display: flex; + flex-wrap: wrap; + margin-left: -$gl-padding; + margin-right: -$gl-padding; + + > h5 { + width: 100%; + } +} + .project-template, .project-import { .form-group { diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss index fe22d186af1..a355e2dee24 100644 --- a/app/assets/stylesheets/pages/settings_ci_cd.scss +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -12,3 +12,7 @@ margin-left: 10px; } } + +.registry-placeholder { + min-height: 60px; +} diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index c1cc509a748..4146deefa89 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -1,6 +1,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController def index set_index_vars + @personal_access_token = finder.build end def create @@ -40,7 +41,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController def set_index_vars @scopes = Gitlab::Auth.available_scopes - @personal_access_token = finder.build @inactive_personal_access_tokens = finder(state: 'inactive').execute @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index a9cce578366..7f03ce07dec 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController def index @sort = params[:sort].presence || sort_value_recently_updated - @branches = BranchesFinder.new(@repository, params).execute + @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute @branches = Kaminari.paginate_array(@branches).page(params[:page]) respond_to do |format| diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb index 71e7dc70a4d..32c0fc6d14a 100644 --- a/app/controllers/projects/registry/repositories_controller.rb +++ b/app/controllers/projects/registry/repositories_controller.rb @@ -6,17 +6,26 @@ module Projects def index @images = project.container_repositories + + respond_to do |format| + format.html + format.json do + render json: ContainerRepositoriesSerializer + .new(project: project, current_user: current_user) + .represent(@images) + end + end end def destroy if image.destroy - redirect_to project_container_registry_index_path(@project), - status: 302, - notice: 'Image repository has been removed successfully!' + respond_to do |format| + format.json { head :no_content } + end else - redirect_to project_container_registry_index_path(@project), - status: 302, - alert: 'Failed to remove image repository!' + respond_to do |format| + format.json { head :bad_request } + end end end diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb index ae72bd03cfb..e602aa3f393 100644 --- a/app/controllers/projects/registry/tags_controller.rb +++ b/app/controllers/projects/registry/tags_controller.rb @@ -3,20 +3,35 @@ module Projects class TagsController < ::Projects::Registry::ApplicationController before_action :authorize_update_container_image!, only: [:destroy] + def index + respond_to do |format| + format.json do + render json: ContainerTagsSerializer + .new(project: @project, current_user: @current_user) + .with_pagination(request, response) + .represent(tags) + end + end + end + def destroy if tag.delete - redirect_to project_container_registry_index_path(@project), - status: 302, - notice: 'Registry tag has been removed successfully!' + respond_to do |format| + format.json { head :no_content } + end else - redirect_to project_container_registry_index_path(@project), - status: 302, - alert: 'Failed to remove registry tag!' + respond_to do |format| + format.json { head :bad_request } + end end end private + def tags + Kaminari::PaginatableArray.new(image.tags, limit: 15) + end + def image @image ||= project.container_repositories .find(params[:repository_id]) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index b331693c789..fd88e0d794a 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,13 +1,15 @@ module EventsHelper ICON_NAMES_BY_EVENT_TYPE = { - 'pushed to' => 'icon_commit', - 'pushed new' => 'icon_commit', - 'created' => 'icon_status_open', - 'opened' => 'icon_status_open', - 'closed' => 'icon_status_closed', - 'accepted' => 'icon_code_fork', - 'commented on' => 'icon_comment_o', - 'deleted' => 'icon_trash_o' + 'pushed to' => 'commit', + 'pushed new' => 'commit', + 'created' => 'status_open', + 'opened' => 'status_open', + 'closed' => 'status_closed', + 'accepted' => 'fork', + 'commented on' => 'comment', + 'deleted' => 'remove', + 'imported' => 'import', + 'joined' => 'users' }.freeze def link_to_author(event, self_added: false) @@ -197,7 +199,7 @@ module EventsHelper def icon_for_event(note) icon_name = ICON_NAMES_BY_EVENT_TYPE[note] - custom_icon(icon_name) if icon_name + sprite_icon(icon_name) if icon_name end def icon_for_profile_event(event) diff --git a/app/models/key.rb b/app/models/key.rb index 0c41e34d969..f119b15c737 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -34,6 +34,7 @@ class Key < ActiveRecord::Base value&.delete!("\n\r") value.strip! unless value.blank? write_attribute(:key, value) + @public_key = nil end def publishable_key diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index e85b83daf9e..0ba00d447e8 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -560,14 +560,20 @@ class MergeRequest < ActiveRecord::Base commits_for_notes_limit = 100 commit_ids = commit_shas.take(commits_for_notes_limit) - Note.where( - "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" + - "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", - mr_id: id, - commit_ids: commit_ids, - target_project_id: target_project_id, - source_project_id: source_project_id - ) + commit_notes = Note + .except(:order) + .where(project_id: [source_project_id, target_project_id]) + .where(noteable_type: 'Commit', commit_id: commit_ids) + + # We're using a UNION ALL here since this results in better performance + # compared to using OR statements. We're using UNION ALL since the queries + # used won't produce any duplicates (e.g. a note for a commit can't also be + # a note for an MR). + union = Gitlab::SQL::Union + .new([notes, commit_notes], remove_duplicates: false) + .to_sql + + Note.from("(#{union}) #{Note.table_name}") end alias_method :discussion_notes, :related_notes @@ -742,10 +748,9 @@ class MergeRequest < ActiveRecord::Base end def has_ci? - has_ci_integration = source_project.try(:ci_service) - uses_gitlab_ci = all_pipelines.any? + return false if has_no_commits? - (has_ci_integration || uses_gitlab_ci) && commits.any? + !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service) end def branch_missing? diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 1f9d712ef84..cfcb03138b7 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -17,6 +17,8 @@ class PersonalAccessToken < ActiveRecord::Base validates :scopes, presence: true validate :validate_scopes + after_initialize :set_default_scopes, if: :persisted? + def revoke! update!(revoked: true) end @@ -32,4 +34,8 @@ class PersonalAccessToken < ActiveRecord::Base errors.add :scopes, "can only contain available scopes" end end + + def set_default_scopes + self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty? + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index d47dc9a05cd..d725c65081d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -989,7 +989,7 @@ class Repository end def create_ref(ref, ref_path) - fetch_ref(path_to_repo, ref, ref_path) + raw_repository.write_ref(ref_path, ref) end def ls_files(ref) diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 2df84e58575..a25882cbb62 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -31,7 +31,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def remove_wip_path - if can?(current_user, :update_merge_request, merge_request.project) + if work_in_progress? && can?(current_user, :update_merge_request, merge_request.project) remove_wip_project_merge_request_path(project, merge_request) end end diff --git a/app/serializers/container_repositories_serializer.rb b/app/serializers/container_repositories_serializer.rb new file mode 100644 index 00000000000..56dc70b5687 --- /dev/null +++ b/app/serializers/container_repositories_serializer.rb @@ -0,0 +1,3 @@ +class ContainerRepositoriesSerializer < BaseSerializer + entity ContainerRepositoryEntity +end diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb new file mode 100644 index 00000000000..1103cf30a07 --- /dev/null +++ b/app/serializers/container_repository_entity.rb @@ -0,0 +1,25 @@ +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 diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb new file mode 100644 index 00000000000..ec1fc349586 --- /dev/null +++ b/app/serializers/container_tag_entity.rb @@ -0,0 +1,23 @@ +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 diff --git a/app/serializers/container_tags_serializer.rb b/app/serializers/container_tags_serializer.rb new file mode 100644 index 00000000000..6ff3adff135 --- /dev/null +++ b/app/serializers/container_tags_serializer.rb @@ -0,0 +1,17 @@ +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 diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index 7d3c752b8e6..36537c5bd02 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -23,7 +23,6 @@ class MergeRequestEntity < IssuableEntity expose :closed_event, using: EventEntity # User entities - expose :author, using: UserEntity expose :merge_user, using: UserEntity # Diff sha's @@ -31,7 +30,6 @@ class MergeRequestEntity < IssuableEntity merge_request.diff_head_sha if merge_request.diff_head_commit end - expose :merge_commit_sha expose :merge_commit_message expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index e3a9e99250e..0d5350f873b 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -37,9 +37,9 @@ - if content_for?(:library_javascripts) = yield :library_javascripts + = javascript_include_tag asset_path("locale/#{I18n.locale.to_s || I18n.default_locale.to_s}/app.js") = webpack_bundle_tag "webpack_runtime" = webpack_bundle_tag "common" - = webpack_bundle_tag "locale" = webpack_bundle_tag "main" = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled = webpack_bundle_tag "test" if Rails.env.test? diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index 906774a21e3..e9613534dde 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -9,50 +9,36 @@ %br Forking a repository allows you to make changes without affecting the original project. .col-lg-9 - .fork-namespaces - - if @namespaces.present? - %label.label-light - %span - Click to fork the project - - @namespaces.in_groups_of(6, false) do |group| - .row - - group.each do |namespace| - - avatar = namespace_icon(namespace, 100) - - if fork = namespace.find_fork_of(@project) - .fork-thumbnail.forked - = link_to project_path(fork) do - - if /no_((\w*)_)*avatar/.match(avatar) - .no-avatar - = icon 'question' - - else - = image_tag avatar - .caption - = namespace.human_name - - else - - can_create_project = current_user.can?(:create_projects, namespace) - .fork-thumbnail{ class: ("disabled" unless can_create_project) } - = link_to project_forks_path(@project, namespace_key: namespace.id), - method: "POST", - class: ("disabled has-tooltip" unless can_create_project), - title: (_('You have reached your project limit') unless can_create_project) do - - if /no_((\w*)_)*avatar/.match(avatar) - .no-avatar - = icon 'question' - - else - = image_tag avatar - .caption - = namespace.human_name - - else - %label.label-light - %span - No available namespaces to fork the project. - %br - %small - You must have permission to create a project in a namespace before forking. + - if @namespaces.present? + .fork-thumbnail-container.js-fork-content + %h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default + Click to fork the project + - @namespaces.each do |namespace| + - avatar = namespace_icon(namespace, 100) + - can_create_project = current_user.can?(:create_projects, namespace) + - forked_project = namespace.find_fork_of(@project) + - fork_path = forked_project ? project_path(forked_project) : project_forks_path(@project, namespace_key: namespace.id) + .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: [("disabled" unless can_create_project), ("forked" if forked_project)] } + = link_to fork_path, + method: "POST", + class: [("js-fork-thumbnail" unless forked_project), ("disabled has-tooltip" unless can_create_project)], + title: (_('You have reached your project limit') unless can_create_project) do + - if /no_((\w*)_)*avatar/.match(avatar) + = project_identicon(namespace, class: "avatar s100 identicon") + - else + .avatar-container.s100 + = image_tag(avatar, class: "avatar s100") + %h5.prepend-top-default + = namespace.human_name + - else + %strong + No available namespaces to fork the project. + %p.prepend-top-default + You must have permission to create a project in a namespace before forking. - .save-project-loader.hide - .center - %h2 - %i.fa.fa-spinner.fa-spin - Forking repository - %p Please wait a moment, this page will automatically refresh when ready. + .save-project-loader.hide.js-fork-content + %h2.text-center + = icon('spinner spin') + Forking repository + %p.text-center + Please wait a moment, this page will automatically refresh when ready. diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 6a567487514..5f97d31f610 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -2,13 +2,13 @@ %h2.merge-requests-title = pluralize(@merge_requests.count, 'Related Merge Request') %ul.unstyled-list.related-merge-requests - - has_any_ci = @merge_requests.any?(&:head_pipeline) + - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id) - @merge_requests.each do |merge_request| %li %span.merge-request-ci-status - if merge_request.head_pipeline = render_pipeline_status(merge_request.head_pipeline) - - elsif has_any_ci + - elsif has_any_head_pipeline = icon('blank fw') %span.merge-request-id = merge_request.to_reference diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index f3c44c94a5c..9ff85c2ee4c 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -29,7 +29,7 @@ - unless current_user == @merge_request.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)) - if can_update_merge_request - %li{ class: merge_request_button_visibility(@merge_request, true) } + %li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] } = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request' %li{ class: merge_request_button_visibility(@merge_request, false) } = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' diff --git a/app/views/projects/registry/repositories/_image.html.haml b/app/views/projects/registry/repositories/_image.html.haml deleted file mode 100644 index a0535edafc3..00000000000 --- a/app/views/projects/registry/repositories/_image.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -.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. diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 5661af01302..36ea5e013e4 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -1,60 +1,49 @@ - page_title "Container Registry" -.row.prepend-top-default.append-bottom-default - .col-lg-3 - %h4.prepend-top-0 +%section + .settings-header + %h4 = page_title %p - With the Docker Container Registry integrated into GitLab, every project - can have its own space to store its Docker images. + = s_('ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.') %p.append-bottom-0 = succeed '.' do - Learn more about - = link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank' + = s_('ContainerRegistry|Learn more about') + = link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank' + .row.registry-placeholder.prepend-bottom-10 + .col-lg-12 + #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } } - .col-lg-9 - .panel.panel-default - .panel-heading - %h4.panel-title - How to use the Container Registry - .panel-body - %p - First log in to GitLab’s Container Registry using your GitLab username - and password. If you have - = link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank' - you need to use a - = succeed ':' do - = link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank' - %pre - docker login #{Gitlab.config.registry.host_port} - %br - %p - Once you log in, you’re free to create and upload a container image - using the common - %code build - and - %code push - commands: - %pre - :plain - docker build -t #{escape_once(@project.container_registry_url)} . - docker push #{escape_once(@project.container_registry_url)} + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('registry_list') - %hr - %h5.prepend-top-default - Use different image names - %p.light - GitLab supports up to 3 levels of image names. The following - examples of images are valid for your project: - %pre - :plain - #{escape_once(@project.container_registry_url)}:tag - #{escape_once(@project.container_registry_url)}/optional-image-name:tag - #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag - - - if @images.blank? - %p.settings-message.text-center.append-bottom-default - No container images stored for this project. Add one by following the - instructions above. - - else - = render partial: 'image', collection: @images + .row.prepend-top-10 + .col-lg-12 + .panel.panel-default + .panel-heading + %h4.panel-title + = s_('ContainerRegistry|How to use the Container Registry') + .panel-body + %p + - link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank') + - link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank') + = s_('ContainerRegistry|First log in to GitLab’s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:').html_safe % { link_2fa: link_2fa, link_token: link_token } + %pre + docker login #{Gitlab.config.registry.host_port} + %br + %p + = s_('ContainerRegistry|Once you log in, you’re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe } + %pre + :plain + docker build -t #{escape_once(@project.container_registry_url)} . + docker push #{escape_once(@project.container_registry_url)} + %hr + %h5.prepend-top-default + = s_('ContainerRegistry|Use different image names') + %p.light + = s_('ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:') + %pre + :plain + #{escape_once(@project.container_registry_url)}:tag + #{escape_once(@project.container_registry_url)}/optional-image-name:tag + #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index b842fd57cf3..c0b1c62e8ef 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -23,7 +23,7 @@ - disabled_class = 'disabled' - disabled_title = @service.disabled_title - = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel' + = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel' - if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true) %hr diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 468ab922542..1927216e191 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -2,12 +2,11 @@ - release = @releases.find { |release| release.tag == tag.name } %li.flex-row .row-main-content.str-truncated - = link_to project_tag_path(@project, tag.name), class: 'item-title ref-name' do - = icon('tag') - = tag.name + = icon('tag') + = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name prepend-left-4' - if protected_tag?(@project, tag) - %span.label.label-success + %span.label.label-success.prepend-left-4 protected - if tag.message.present? diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml index e415ec64c38..b8b1f4ca42f 100644 --- a/app/views/shared/_personal_access_tokens_form.html.haml +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -1,9 +1,9 @@ - type = impersonation ? "impersonation" : "personal access" %h5.prepend-top-0 - Add a #{type} Token + Add a #{type} token %p.profile-settings-content - Pick a name for the application, and we'll give you a unique #{type} Token. + Pick a name for the application, and we'll give you a unique #{type} token. = form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f| diff --git a/changelogs/unreleased/14553-missing-space-in-log-msg.yml b/changelogs/unreleased/14553-missing-space-in-log-msg.yml new file mode 100644 index 00000000000..a0420d49770 --- /dev/null +++ b/changelogs/unreleased/14553-missing-space-in-log-msg.yml @@ -0,0 +1,5 @@ +--- +title: "Add missing space in Sidekiq memory killer log message" +merge_request: 14553 +author: Benjamin Drung +type: fixed diff --git a/changelogs/unreleased/26890-fix-default-branches-sorting.yml b/changelogs/unreleased/26890-fix-default-branches-sorting.yml new file mode 100644 index 00000000000..cf7060190b3 --- /dev/null +++ b/changelogs/unreleased/26890-fix-default-branches-sorting.yml @@ -0,0 +1,5 @@ +--- +title: Fix the default branches sorting to actually be 'Last updated' +merge_request: 14295 +author: +type: fixed diff --git a/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml b/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml new file mode 100644 index 00000000000..cea6cb2e48b --- /dev/null +++ b/changelogs/unreleased/3612-update-script-template-order-in-vue-files.yml @@ -0,0 +1,5 @@ +--- +title: Re-arrange <script> tags before <template> tags in .vue files +merge_request: 14671 +author: +type: changed diff --git a/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml b/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml new file mode 100644 index 00000000000..3d3efcdbcc6 --- /dev/null +++ b/changelogs/unreleased/36742-hide-close-mr-button-on-merge.yml @@ -0,0 +1,5 @@ +--- +title: Hide close MR button after merge without reloading page +merge_request: 14122 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/37229-mr-widget-status-icon.yml b/changelogs/unreleased/37229-mr-widget-status-icon.yml new file mode 100644 index 00000000000..6d84d1964ca --- /dev/null +++ b/changelogs/unreleased/37229-mr-widget-status-icon.yml @@ -0,0 +1,5 @@ +--- +title: fix merge request widget status icon for failed CI +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml b/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml new file mode 100644 index 00000000000..419e9295d32 --- /dev/null +++ b/changelogs/unreleased/38417-use-explicit-boolean-vue-attribute.yml @@ -0,0 +1,5 @@ +--- +title: Use explicit boolean true attribute for show-disabled-button in Vue files +merge_request: 14672 +author: +type: fixed diff --git a/changelogs/unreleased/dm-pat-revoke.yml b/changelogs/unreleased/dm-pat-revoke.yml new file mode 100644 index 00000000000..32ac66056d5 --- /dev/null +++ b/changelogs/unreleased/dm-pat-revoke.yml @@ -0,0 +1,5 @@ +--- +title: Set default scope on PATs that don't have one set to allow them to be revoked +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/docs-openid-connect.yml b/changelogs/unreleased/docs-openid-connect.yml new file mode 100644 index 00000000000..3989ec53cfa --- /dev/null +++ b/changelogs/unreleased/docs-openid-connect.yml @@ -0,0 +1,5 @@ +--- +title: Add link to OpenID Connect documentation +merge_request: 14368 +author: Markus Koller +type: other diff --git a/changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml b/changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml new file mode 100644 index 00000000000..efb993eff71 --- /dev/null +++ b/changelogs/unreleased/fix-edit-project-service-cancel-button-position.yml @@ -0,0 +1,5 @@ +--- +title: Fix edit project service cancel button position +merge_request: 14596 +author: Matt Coleman +type: fixed diff --git a/changelogs/unreleased/mentions-in-comments.yml b/changelogs/unreleased/mentions-in-comments.yml new file mode 100644 index 00000000000..907f455007b --- /dev/null +++ b/changelogs/unreleased/mentions-in-comments.yml @@ -0,0 +1,5 @@ +--- +title: Makes @mentions links have a different styling for better separation +merge_request: +author: +type: added diff --git a/changelogs/unreleased/merge-request-notes-performance.yml b/changelogs/unreleased/merge-request-notes-performance.yml new file mode 100644 index 00000000000..6cf7a5047df --- /dev/null +++ b/changelogs/unreleased/merge-request-notes-performance.yml @@ -0,0 +1,5 @@ +--- +title: Use a UNION ALL for getting merge request notes +merge_request: +author: +type: other diff --git a/changelogs/unreleased/tag-link-size.yml b/changelogs/unreleased/tag-link-size.yml new file mode 100644 index 00000000000..d94e415ba1f --- /dev/null +++ b/changelogs/unreleased/tag-link-size.yml @@ -0,0 +1,5 @@ +--- +title: Adjusts tag link to avoid underlining spaces +merge_request: 14544 +author: Guilherme Vieira +type: fixed diff --git a/config/application.rb b/config/application.rb index 30117b6a98e..ca2ab83becc 100644 --- a/config/application.rb +++ b/config/application.rb @@ -105,6 +105,7 @@ module Gitlab config.assets.precompile << "lib/ace.js" config.assets.precompile << "vendor/assets/fonts/*" config.assets.precompile << "test.css" + config.assets.precompile << "locale/**/app.js" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index c5c50c216e1..88771c5f5bb 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -499,6 +499,8 @@ production: &base # Gitaly settings gitaly: + # Path to the directory containing Gitaly client executables. + client_path: /home/git/gitaly # Default Gitaly authentication token. Can be overriden per storage. Can # be left blank when Gitaly is running locally on a Unix socket, which # is the normal way to deploy Gitaly. @@ -664,7 +666,7 @@ test: gitaly_address: unix:tmp/tests/gitaly/gitaly.socket gitaly: - enabled: true + client_path: tmp/tests/gitaly token: secret backup: path: tmp/tests/backups diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb index 377e5104f9d..49551319435 100644 --- a/config/initializers/gettext_rails_i18n_patch.rb +++ b/config/initializers/gettext_rails_i18n_patch.rb @@ -39,3 +39,17 @@ module GettextI18nRailsJs end end end + +class PoToJson + # This is required to modify the JS locale file output to our import needs + # Overwrites: https://github.com/webhippie/po_to_json/blob/master/lib/po_to_json.rb#L46 + def generate_for_jed(language, overwrite = {}) + @options = parse_options(overwrite.merge(language: language)) + @parsed ||= inject_meta(parse_document) + + generated = build_json_for(build_jed_for(@parsed)) + [ + "window.translations = #{generated};" + ].join(" ") + end +end diff --git a/config/routes/project.rb b/config/routes/project.rb index aa0819bc41c..7f0e056c884 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -281,7 +281,7 @@ constraints(ProjectUrlConstrainer.new) do namespace :registry do resources :repository, only: [] do - resources :tags, only: [:destroy], + resources :tags, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_tag_regex } end end diff --git a/config/webpack.config.js b/config/webpack.config.js index 3404715fe30..c515a170d2d 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -68,6 +68,7 @@ var config = { prometheus_metrics: './prometheus_metrics', protected_branches: './protected_branches', protected_tags: './protected_tags', + registry_list: './registry/index.js', repo: './repo/index.js', sidebar: './sidebar/sidebar_bundle.js', schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js', @@ -122,10 +123,6 @@ var config = { } }, { - test: /locale\/\w+\/(.*)\.js$/, - loader: 'exports-loader?locales', - }, - { test: /monaco-editor\/\w+\/vs\/loader\.js$/, use: [ { loader: 'exports-loader', options: 'l.global' }, @@ -200,6 +197,7 @@ var config = { 'pdf_viewer', 'pipelines', 'pipelines_details', + 'registry_list', 'repo', 'schedule_form', 'schedules_index', @@ -222,7 +220,7 @@ var config = { // create cacheable common library bundles new webpack.optimize.CommonsChunkPlugin({ - names: ['main', 'locale', 'common', 'webpack_runtime'], + names: ['main', 'common', 'webpack_runtime'], }), // enable scope hoisting diff --git a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb index 014f5b2f372..6f22641077d 100644 --- a/db/migrate/20150827121444_add_fast_forward_option_to_project.rb +++ b/db/migrate/20150827121444_add_fast_forward_option_to_project.rb @@ -7,11 +7,13 @@ class AddFastForwardOptionToProject < ActiveRecord::Migration disable_ddl_transaction! - def add + def up add_column_with_default(:projects, :merge_requests_ff_only_enabled, :boolean, default: false) end def down - remove_column(:projects, :merge_requests_ff_only_enabled) + if column_exists?(:projects, :merge_requests_ff_only_enabled) + remove_column(:projects, :merge_requests_ff_only_enabled) + end end end diff --git a/db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb b/db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb new file mode 100644 index 00000000000..ac266c3e22e --- /dev/null +++ b/db/migrate/20171004121444_make_sure_fast_forward_option_exists.rb @@ -0,0 +1,25 @@ +# 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 diff --git a/db/schema.rb b/db/schema.rb index cf0d54a3248..e7391ac9826 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170928100231) do +ActiveRecord::Schema.define(version: 20171004121444) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index bff8a2d3e4d..63a5dd1445c 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -121,6 +121,15 @@ GET /projects/:id/merge_requests?labels=bug,reproduced GET /projects/:id/merge_requests?my_reaction_emoji=star ``` +`project_id` represents the ID of the project where the MR resides. +`project_id` will always equal `target_project_id`. + +In the case of a merge request from the same project, +`source_project_id`, `target_project_id` and `project_id` +will be the same. In the case of a merge request from a fork, +`target_project_id` and `project_id` will be the same and +`source_project_id` will be the fork project's ID. + Parameters: | Attribute | Type | Required | Description | diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md index 796a025b951..b8f9988e3ef 100644 --- a/doc/ci/enable_or_disable_ci.md +++ b/doc/ci/enable_or_disable_ci.md @@ -1,51 +1,46 @@ -## Enable or disable GitLab CI +## Enable or disable GitLab CI/CD -_To effectively use GitLab CI, you need a valid [`.gitlab-ci.yml`](yaml/README.md) +To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md) file present at the root directory of your project and a [runner](runners/README.md) properly set up. You can read our -[quick start guide](quick_start/README.md) to get you started._ +[quick start guide](quick_start/README.md) to get you started. -If you are using an external CI server like Jenkins or Drone CI, it is advised -to disable GitLab CI in order to not have any conflicts with the commits status +If you are using an external CI/CD server like Jenkins or Drone CI, it is advised +to disable GitLab CI/CD in order to not have any conflicts with the commits status API. --- -GitLab CI is exposed via the `/pipelines` and `/builds` pages of a project. -Disabling GitLab CI in a project does not delete any previous jobs. -In fact, the `/pipelines` and `/builds` pages can still be accessed, although +GitLab CI/CD is exposed via the `/pipelines` and `/jobs` pages of a project. +Disabling GitLab CI/CD in a project does not delete any previous jobs. +In fact, the `/pipelines` and `/jobs` pages can still be accessed, although it's hidden from the left sidebar menu. -GitLab CI is enabled by default on new installations and can be disabled either +GitLab CI/CD is enabled by default on new installations and can be disabled either individually under each project's settings, or site-wide by modifying the settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations respectively. ### Per-project user setting -The setting to enable or disable GitLab CI can be found with the name **Pipelines** -under the **Sharing & Permissions** area of a project's settings along with -**Merge Requests**. Choose one of **Disabled**, **Only team members** and -**Everyone with access** and hit **Save changes** for the settings to take effect. +The setting to enable or disable GitLab CI/CD can be found under your project's +**Settings > General > Permissions**. Choose one of "Disabled", "Only team members" +or "Everyone with access" and hit **Save changes** for the settings to take effect. -![Sharing & Permissions settings](img/permissions_settings.png) +![Sharing & Permissions settings](../user/project/settings/img/sharing_and_permissions_settings.png) ---- - -### Site-wide administrator setting +### Site-wide admin setting -You can disable GitLab CI site-wide, by modifying the settings in `gitlab.yml` +You can disable GitLab CI/CD site-wide, by modifying the settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations respectively. Two things to note: -1. Disabling GitLab CI, will affect only newly-created projects. Projects that +1. Disabling GitLab CI/CD, will affect only newly-created projects. Projects that had it enabled prior to this modification, will work as before. -1. Even if you disable GitLab CI, users will still be able to enable it in the +1. Even if you disable GitLab CI/CD, users will still be able to enable it in the project's settings. ---- - For installations from source, open `gitlab.yml` with your editor and set `builds` to `false`: diff --git a/doc/ci/environments.md b/doc/ci/environments.md index acd5682841a..c03e16b1b38 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -26,7 +26,7 @@ so every environment can have one or more deployments. GitLab keeps track of your deployments, so you always know what is currently being deployed on your servers. If you have a deployment service such as [Kubernetes][kubernetes-service] enabled for your project, you can use it to assist with your deployments, and -can even access a web terminal for your environment from within GitLab! +can even access a [web terminal](#web-terminals) for your environment from within GitLab! To better understand how environments and deployments work, let's consider an example. We assume that you have already created a project in GitLab and set up @@ -119,7 +119,7 @@ where you can find information of the last deployment status of an environment. Here's how the Environments page looks so far. -![Staging environment view](img/environments_available_staging.png) +![Environment view](img/environments_available.png) There's a bunch of information there, specifically you can see: @@ -229,7 +229,7 @@ You can find it in the pipeline, job, environment, and deployment views. | Pipelines | Single pipeline | Environments | Deployments | jobs | | --------- | ----------------| ------------ | ----------- | -------| -| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_builds.png) | +| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_jobs.png) | Clicking on the play button in either of these places will trigger the `deploy_prod` job, and the deployment will be recorded under a new @@ -402,7 +402,7 @@ places within GitLab. | In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button | | -------------------- | ------------ | ----------- | -| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_link_url.png) | ![Environment URL in deployments](img/environments_link_url_deployments.png) | +| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_available.png) | ![Environment URL in deployments](img/deployments_view.png) | If a merge request is eventually merged to the default branch (in our case `master`) and that branch also deploys to an environment (in our case `staging` @@ -574,7 +574,7 @@ Once configured, GitLab will attempt to retrieve [supported performance metrics] environment which has had a successful deployment. If monitoring data was successfully retrieved, a Monitoring button will appear for each environment. -![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png) +![Environment Detail with Metrics](img/deployments_view.png) Clicking on the Monitoring button will display a new page, showing up to the last 8 hours of performance data. It may take a minute or two for data to appear @@ -593,10 +593,11 @@ Web terminals were added in GitLab 8.15 and are only available to project masters and owners. If you deploy to your environments with the help of a deployment service (e.g., -the [Kubernetes service][kubernetes-service], GitLab can open +the [Kubernetes service][kubernetes-service]), GitLab can open a terminal session to your environment! This is a very powerful feature that allows you to debug issues without leaving the comfort of your web browser. To -enable it, just follow the instructions given in the service documentation. +enable it, just follow the instructions given in the service integration +documentation. Once enabled, your environments will gain a "terminal" button: diff --git a/doc/ci/img/builds_tab.png b/doc/ci/img/builds_tab.png Binary files differdeleted file mode 100644 index 2d7eec8a949..00000000000 --- a/doc/ci/img/builds_tab.png +++ /dev/null diff --git a/doc/ci/img/deployments_view.png b/doc/ci/img/deployments_view.png Binary files differindex 7ded0c97b72..436fed5f465 100644 --- a/doc/ci/img/deployments_view.png +++ b/doc/ci/img/deployments_view.png diff --git a/doc/ci/img/environments_available.png b/doc/ci/img/environments_available.png Binary files differnew file mode 100644 index 00000000000..2991a309655 --- /dev/null +++ b/doc/ci/img/environments_available.png diff --git a/doc/ci/img/environments_available_staging.png b/doc/ci/img/environments_available_staging.png Binary files differdeleted file mode 100644 index 5c031ad0d9d..00000000000 --- a/doc/ci/img/environments_available_staging.png +++ /dev/null diff --git a/doc/ci/img/environments_dynamic_groups.png b/doc/ci/img/environments_dynamic_groups.png Binary files differindex 0f42b368c5b..45124b3d8d8 100644 --- a/doc/ci/img/environments_dynamic_groups.png +++ b/doc/ci/img/environments_dynamic_groups.png diff --git a/doc/ci/img/environments_link_url.png b/doc/ci/img/environments_link_url.png Binary files differdeleted file mode 100644 index 44010f6aa6f..00000000000 --- a/doc/ci/img/environments_link_url.png +++ /dev/null diff --git a/doc/ci/img/environments_link_url_deployments.png b/doc/ci/img/environments_link_url_deployments.png Binary files differdeleted file mode 100644 index 4f90143527a..00000000000 --- a/doc/ci/img/environments_link_url_deployments.png +++ /dev/null diff --git a/doc/ci/img/environments_link_url_mr.png b/doc/ci/img/environments_link_url_mr.png Binary files differindex 64f134e0b0d..7ce46063062 100644 --- a/doc/ci/img/environments_link_url_mr.png +++ b/doc/ci/img/environments_link_url_mr.png diff --git a/doc/ci/img/environments_manual_action_builds.png b/doc/ci/img/environments_manual_action_builds.png Binary files differdeleted file mode 100644 index e7cf63a1031..00000000000 --- a/doc/ci/img/environments_manual_action_builds.png +++ /dev/null diff --git a/doc/ci/img/environments_manual_action_deployments.png b/doc/ci/img/environments_manual_action_deployments.png Binary files differindex 2b3f6f3edad..93beaa0de54 100644 --- a/doc/ci/img/environments_manual_action_deployments.png +++ b/doc/ci/img/environments_manual_action_deployments.png diff --git a/doc/ci/img/environments_manual_action_environments.png b/doc/ci/img/environments_manual_action_environments.png Binary files differindex e0c07604e7f..9490be63f14 100644 --- a/doc/ci/img/environments_manual_action_environments.png +++ b/doc/ci/img/environments_manual_action_environments.png diff --git a/doc/ci/img/environments_manual_action_jobs.png b/doc/ci/img/environments_manual_action_jobs.png Binary files differnew file mode 100644 index 00000000000..9ae223cf77f --- /dev/null +++ b/doc/ci/img/environments_manual_action_jobs.png diff --git a/doc/ci/img/environments_manual_action_pipelines.png b/doc/ci/img/environments_manual_action_pipelines.png Binary files differindex 82bbae88027..129e44f6fb0 100644 --- a/doc/ci/img/environments_manual_action_pipelines.png +++ b/doc/ci/img/environments_manual_action_pipelines.png diff --git a/doc/ci/img/environments_manual_action_single_pipeline.png b/doc/ci/img/environments_manual_action_single_pipeline.png Binary files differindex 36337cb1870..1eeb4379eb7 100644 --- a/doc/ci/img/environments_manual_action_single_pipeline.png +++ b/doc/ci/img/environments_manual_action_single_pipeline.png diff --git a/doc/ci/img/environments_mr_review_app.png b/doc/ci/img/environments_mr_review_app.png Binary files differindex 7bff84362a3..4bb643d708f 100644 --- a/doc/ci/img/environments_mr_review_app.png +++ b/doc/ci/img/environments_mr_review_app.png diff --git a/doc/ci/img/environments_terminal_button_on_index.png b/doc/ci/img/environments_terminal_button_on_index.png Binary files differindex 6f05b2aa343..061bb7c3c87 100644 --- a/doc/ci/img/environments_terminal_button_on_index.png +++ b/doc/ci/img/environments_terminal_button_on_index.png diff --git a/doc/ci/img/environments_terminal_button_on_show.png b/doc/ci/img/environments_terminal_button_on_show.png Binary files differindex 9469fab99ab..4d24304bc93 100644 --- a/doc/ci/img/environments_terminal_button_on_show.png +++ b/doc/ci/img/environments_terminal_button_on_show.png diff --git a/doc/ci/img/environments_view.png b/doc/ci/img/environments_view.png Binary files differdeleted file mode 100644 index 821352188ef..00000000000 --- a/doc/ci/img/environments_view.png +++ /dev/null diff --git a/doc/ci/img/permissions_settings.png b/doc/ci/img/permissions_settings.png Binary files differdeleted file mode 100644 index 1454c75fd24..00000000000 --- a/doc/ci/img/permissions_settings.png +++ /dev/null diff --git a/doc/ci/img/prometheus_environment_detail_with_metrics.png b/doc/ci/img/prometheus_environment_detail_with_metrics.png Binary files differdeleted file mode 100644 index 214b10624a9..00000000000 --- a/doc/ci/img/prometheus_environment_detail_with_metrics.png +++ /dev/null diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index ebcb92b5db1..17839cbaef1 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -149,14 +149,15 @@ script: ## Secret variables ->**Notes:** -- This feature requires GitLab Runner 0.4.0 or higher. -- Group-level secret variables added in GitLab 9.4. -- Be aware that secret variables are not masked, and their values can be shown - in the job logs if explicitly asked to do so. If your project is public or - internal, you can set the pipelines private from your project's Pipelines - settings. Follow the discussion in issue [#13784][ce-13784] for masking the - secret variables. +NOTE: **Note:** +Group-level secret variables were added in GitLab 9.4. + +CAUTION: **Important:** +Be aware that secret variables are not masked, and their values can be shown +in the job logs if explicitly asked to do so. If your project is public or +internal, you can set the pipelines private from your [project's Pipelines +settings](../../user/project/pipelines/settings.md#visibility-of-pipelines). +Follow the discussion in issue [#13784][ce-13784] for masking the secret variables. GitLab CI allows you to define per-project or per-group secret variables that are set in the pipeline environment. The secret variables are stored out of @@ -171,6 +172,8 @@ Likewise, group-level secret variables can be added by going to your group's **Settings > CI/CD**, then finding the section called **Secret variables**. Any variables of [subgroups] will be inherited recursively. +![Secret variables](img/secret_variables.png) + Once you set them, they will be available for all subsequent pipelines. You can also [protect your variables](#protected-secret-variables). @@ -202,7 +205,7 @@ are set in the build environment. These variables are only defined for the project services that you are using to learn which variables they define. An example project service that defines deployment variables is -[Kubernetes Service](../../user/project/integrations/kubernetes.md). +[Kubernetes Service](../../user/project/integrations/kubernetes.md#deployment-variables). ## Debug tracing @@ -439,7 +442,7 @@ export CI_REGISTRY_USER="gitlab-ci-token" export CI_REGISTRY_PASSWORD="longalfanumstring" ``` -[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 +[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables" [eep]: https://about.gitlab.com/gitlab-ee/ "Available only in GitLab Enterprise Edition Premium" [envs]: ../environments.md [protected branches]: ../../user/project/protected_branches.md diff --git a/doc/ci/variables/img/secret_variables.png b/doc/ci/variables/img/secret_variables.png Binary files differnew file mode 100644 index 00000000000..f70935069d9 --- /dev/null +++ b/doc/ci/variables/img/secret_variables.png diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index c8d23609280..77ae6d2a0ea 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -470,7 +470,25 @@ On those a default key should not be provided. ``` #### Ordering -1. Order for a Vue Component: + +1. Tag order in `.vue` file + + ``` + <script> + // ... + </script> + + <template> + // ... + </template> + + // We don't use scoped styles but there are few instances of this + <style> + // ... + </style> + ``` + +1. Properties in a Vue Component: 1. `name` 1. `props` 1. `mixins` @@ -490,6 +508,7 @@ On those a default key should not be provided. 1. `beforeDestroy` 1. `destroyed` + #### Vue and Bootstrap 1. Tooltips: Do not rely on `has-tooltip` class name for Vue components diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 2607353782a..277e0cd5f00 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -428,7 +428,7 @@ is a good example of this pattern. ## Style guide -Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs) +Please refer to the Vue section of our [style guide](style_guide_js.md#vue-js) for best practices while writing your Vue components and templates. ## Testing Vue Components diff --git a/doc/development/testing.md b/doc/development/testing.md index d856b003353..4d5b90de6fc 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -302,7 +302,7 @@ range of inputs, might look like this: ```ruby describe "#==" do - using Rspec::Parameterized::TableSyntax + using RSpec::Parameterized::TableSyntax let(:project1) { create(:project) } let(:project2) { create(:project) } diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md index fac91935a45..597c98fbf6b 100644 --- a/doc/topics/authentication/index.md +++ b/doc/topics/authentication/index.md @@ -11,6 +11,7 @@ This page gathers all the resources for the topic **Authentication** within GitL - [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/) - **Integrations:** - [GitLab as OAuth2 authentication service provider](../../integration/oauth_provider.md#introduction-to-oauth) + - [GitLab as OpenID Connect identity provider](../../integration/openid_connect_provider.md) ## GitLab administrators diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 5c615daf464..2c4dfcff4a6 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -17,25 +17,25 @@ have its own space to store its Docker images. You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. ---- - ## Enable the Container Registry for your project +NOTE: **Note:** +If you cannot find the Container Registry entry under your project's settings, +that means that it is not enabled in your GitLab instance. Ask your administrator +to enable it. + 1. First, ask your system administrator to enable GitLab Container Registry following the [administration documentation](../../administration/container_registry.md). If you are using GitLab.com, this is enabled by default so you can start using the Registry immediately. - -1. Go to your project's settings and enable the **Container Registry** feature - on your project. For new projects this might be enabled by default. For - existing projects (prior GitLab 8.8), you will have to explicitly enable it. - - ![Enable Container Registry](img/container_registry_enable.png) - +1. Go to your [project's General settings](settings/index.md#sharing-and-permissions) + and enable the **Container Registry** feature on your project. For new + projects this might be enabled by default. For existing projects + (prior GitLab 8.8), you will have to explicitly enable it. 1. Hit **Save changes** for the changes to take effect. You should now be able - to see the **Registry** link in the project menu. + to see the **Registry** link in the sidebar. - ![Container Registry tab](img/container_registry_tab.png) +![Container Registry](img/container_registry.png) ## Build and push images diff --git a/doc/user/project/img/container_registry.png b/doc/user/project/img/container_registry.png Binary files differnew file mode 100644 index 00000000000..abbaf838538 --- /dev/null +++ b/doc/user/project/img/container_registry.png diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png Binary files differdeleted file mode 100644 index d067a8be1ca..00000000000 --- a/doc/user/project/img/container_registry_enable.png +++ /dev/null diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png Binary files differdeleted file mode 100644 index a85237271d9..00000000000 --- a/doc/user/project/img/container_registry_tab.png +++ /dev/null diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png Binary files differindex cf7f519f783..5f6dc9e4e8b 100644 --- a/doc/user/project/img/issue_board.png +++ b/doc/user/project/img/issue_board.png diff --git a/doc/user/project/img/issue_board_move_issue_card_list.png b/doc/user/project/img/issue_board_move_issue_card_list.png Binary files differindex c6b17ada40e..3666dbb87ab 100644 --- a/doc/user/project/img/issue_board_move_issue_card_list.png +++ b/doc/user/project/img/issue_board_move_issue_card_list.png diff --git a/doc/user/project/img/labels_assign_label_in_new_issue.png b/doc/user/project/img/labels_assign_label_in_new_issue.png Binary files differdeleted file mode 100644 index badfbed0bbe..00000000000 --- a/doc/user/project/img/labels_assign_label_in_new_issue.png +++ /dev/null diff --git a/doc/user/project/img/labels_default.png b/doc/user/project/img/labels_default.png Binary files differindex 474953d565b..7934e3bfb5e 100644 --- a/doc/user/project/img/labels_default.png +++ b/doc/user/project/img/labels_default.png diff --git a/doc/user/project/img/labels_filter.png b/doc/user/project/img/labels_filter.png Binary files differindex 3aca77f0070..6a1ebfc2ecb 100644 --- a/doc/user/project/img/labels_filter.png +++ b/doc/user/project/img/labels_filter.png diff --git a/doc/user/project/img/labels_filter_by_priority.png b/doc/user/project/img/labels_filter_by_priority.png Binary files differindex 5609a1f6d7f..419e555e709 100644 --- a/doc/user/project/img/labels_filter_by_priority.png +++ b/doc/user/project/img/labels_filter_by_priority.png diff --git a/doc/user/project/img/labels_new_label.png b/doc/user/project/img/labels_new_label.png Binary files differindex b44b4bd296d..e26425d0188 100644 --- a/doc/user/project/img/labels_new_label.png +++ b/doc/user/project/img/labels_new_label.png diff --git a/doc/user/project/img/labels_prioritize.png b/doc/user/project/img/labels_prioritize.png Binary files differindex 3e888f36364..d602a3c90ec 100644 --- a/doc/user/project/img/labels_prioritize.png +++ b/doc/user/project/img/labels_prioritize.png diff --git a/doc/user/project/img/project_repository_settings.png b/doc/user/project/img/project_repository_settings.png Binary files differindex 1aa7efc36f1..aa4d4452c87 100644 --- a/doc/user/project/img/project_repository_settings.png +++ b/doc/user/project/img/project_repository_settings.png diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index e2cc67726e0..96a5a23ee13 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -12,6 +12,8 @@ Other interesting links: - [GitLab Issue Board landing page on about.gitlab.com][landing] - [YouTube video introduction to Issue Boards][youtube] +![GitLab Issue Board](img/issue_board.png) + ## Overview The Issue Board builds on GitLab's existing @@ -89,10 +91,6 @@ two defaults: - **Backlog** (default): shows all open issues that does not belong to one of lists. Always appears on the very left. - **Closed** (default): shows all closed issues. Always appears on the very right. -![GitLab Issue Board](img/issue_board.png) - ---- - In short, here's a list of actions you can take in an Issue Board: - [Create a new list](#creating-a-new-list). diff --git a/doc/user/project/issues/img/button_close_issue.png b/doc/user/project/issues/img/button_close_issue.png Binary files differindex 8fb2e23f58a..05d257ce9bf 100644 --- a/doc/user/project/issues/img/button_close_issue.png +++ b/doc/user/project/issues/img/button_close_issue.png diff --git a/doc/user/project/issues/img/group_issues_list_view.png b/doc/user/project/issues/img/group_issues_list_view.png Binary files differindex 5d20e8cbc89..bba964076d0 100644 --- a/doc/user/project/issues/img/group_issues_list_view.png +++ b/doc/user/project/issues/img/group_issues_list_view.png diff --git a/doc/user/project/issues/img/issue_board.png b/doc/user/project/issues/img/issue_board.png Binary files differindex 1759b28a9ef..87b1016cc76 100644 --- a/doc/user/project/issues/img/issue_board.png +++ b/doc/user/project/issues/img/issue_board.png diff --git a/doc/user/project/issues/img/issue_template.png b/doc/user/project/issues/img/issue_template.png Binary files differindex c63229a4af2..0e4c8df897b 100644 --- a/doc/user/project/issues/img/issue_template.png +++ b/doc/user/project/issues/img/issue_template.png diff --git a/doc/user/project/issues/img/issues_main_view.png b/doc/user/project/issues/img/issues_main_view.png Binary files differindex 4faa42e40ee..a929916c682 100644 --- a/doc/user/project/issues/img/issues_main_view.png +++ b/doc/user/project/issues/img/issues_main_view.png diff --git a/doc/user/project/issues/img/issues_main_view_numbered.jpg b/doc/user/project/issues/img/issues_main_view_numbered.jpg Binary files differindex 4b5d7fba459..b4b68476d24 100644 --- a/doc/user/project/issues/img/issues_main_view_numbered.jpg +++ b/doc/user/project/issues/img/issues_main_view_numbered.jpg diff --git a/doc/user/project/issues/img/new_issue.png b/doc/user/project/issues/img/new_issue.png Binary files differindex e72ac49d6b9..07d65a93070 100644 --- a/doc/user/project/issues/img/new_issue.png +++ b/doc/user/project/issues/img/new_issue.png diff --git a/doc/user/project/issues/img/new_issue_from_issue_board.png b/doc/user/project/issues/img/new_issue_from_issue_board.png Binary files differindex 9c2b3ff50fa..da892eff0a6 100644 --- a/doc/user/project/issues/img/new_issue_from_issue_board.png +++ b/doc/user/project/issues/img/new_issue_from_issue_board.png diff --git a/doc/user/project/issues/img/new_issue_from_open_issue.png b/doc/user/project/issues/img/new_issue_from_open_issue.png Binary files differindex 2aed5372830..c6f3f0617ab 100644 --- a/doc/user/project/issues/img/new_issue_from_open_issue.png +++ b/doc/user/project/issues/img/new_issue_from_open_issue.png diff --git a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png Binary files differindex cddf36b7457..4b9535f6b15 100644 --- a/doc/user/project/issues/img/new_issue_from_projects_dashboard.png +++ b/doc/user/project/issues/img/new_issue_from_projects_dashboard.png diff --git a/doc/user/project/issues/img/new_issue_from_tracker_list.png b/doc/user/project/issues/img/new_issue_from_tracker_list.png Binary files differindex 7e5413f0b7d..66793cb44fa 100644 --- a/doc/user/project/issues/img/new_issue_from_tracker_list.png +++ b/doc/user/project/issues/img/new_issue_from_tracker_list.png diff --git a/doc/user/project/issues/img/project_issues_list_view.png b/doc/user/project/issues/img/project_issues_list_view.png Binary files differindex 2fcc9e8d9da..584a81aab8a 100644 --- a/doc/user/project/issues/img/project_issues_list_view.png +++ b/doc/user/project/issues/img/project_issues_list_view.png diff --git a/doc/user/project/issues/img/sidebar_move_issue.png b/doc/user/project/issues/img/sidebar_move_issue.png Binary files differindex 111f7861364..1e688cec894 100644 --- a/doc/user/project/issues/img/sidebar_move_issue.png +++ b/doc/user/project/issues/img/sidebar_move_issue.png diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md index 8ec7adad172..21a2e1213ec 100644 --- a/doc/user/project/labels.md +++ b/doc/user/project/labels.md @@ -20,8 +20,6 @@ Head over a single project and navigate to **Issues > Labels**. The first time you visit this page, you'll notice that there are no labels created yet. -![Generate new labels](img/labels_generate.png) - Creating a new label from scratch is as easy as pressing the **New label** button. From there on you can choose the name, give it an optional description, a color and you are set. @@ -32,21 +30,23 @@ When you are ready press the **Create label** button to create the new label. --- -## Default Labels - -It's possible to populate the labels for your project from a set of predefined labels. - -### Generate GitLab's predefined label set +## Default labels -![Generate new labels](img/labels_generate.png) +The very first time you visit the labels area, it's gonna be empty. In that +case, it's possible to populate the labels for your project from a set of +predefined labels. Click the link to 'Generate a default set of labels' and GitLab will -generate a set of predefined labels for you. There are 8 default generated labels -in total and you can see them in the screenshot below. - -![Default generated labels](img/labels_default.png) +generate them for you. There are 8 default generated labels in total: ---- +- bug +- confirmed +- critical +- discussion +- documentation +- enhancement +- suggestion +- support ## Labels Overview @@ -102,30 +102,25 @@ If you work on a large or popular project, try subscribing only to the labels that are relevant to you. You’ll notice it’ll be much easier to focus on what’s important. -## Create a new label right from the issue tracker - -> Introduced in GitLab 8.6. +## Create a new label when inside an issue -There are times when you are already in the issue tracker searching for a +There are times when you are already inside an issue searching to assign a label, only to realize it doesn't exist. Instead of going to the **Labels** page and being distracted from your original purpose, you can create new labels on the fly. -Select **Create new** from the labels dropdown list, provide a name, pick a -color and hit **Create**. +Expand the issue sidebar and select **Create new label** from the labels dropdown +list. Provide a name, pick a color and hit **Create**. The new label will be +ready to used right away! -![Create new label on the fly](img/labels_new_label_on_the_fly_create.png) ![New label on the fly](img/labels_new_label_on_the_fly.png) ## Assigning labels to issues and merge requests There are generally two ways to assign a label to an issue or merge request. -You can assign a label when you first create or edit an issue or merge request. - -![Assign label in new issue](img/labels_assign_label_in_new_issue.png) - ---- +The first one is to assign a label when you first create or edit an issue or +merge request. The second way is by using the right sidebar when inside an issue or merge request. Expand it and hit **Edit** in the labels area. Start typing the name diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md index 64b94d81024..22ef11e4049 100644 --- a/doc/user/project/merge_requests/cherry_pick_changes.md +++ b/doc/user/project/merge_requests/cherry_pick_changes.md @@ -2,24 +2,19 @@ > [Introduced][ce-3514] in GitLab 8.7. ---- - GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick] -with introducing a **Cherry-pick** button in Merge Requests and commit details. +with introducing a **Cherry-pick** button in merge requests and commit details. -## Cherry-picking a Merge Request +## Cherry-picking a merge request -After the Merge Request has been merged, a **Cherry-pick** button will be available -to cherry-pick the changes introduced by that Merge Request: +After the merge request has been merged, a **Cherry-pick** button will be available +to cherry-pick the changes introduced by that merge request. ![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png) ---- - -You can cherry-pick the changes directly into the selected branch or you can opt to -create a new Merge Request with the cherry-pick changes: - -![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png) +After you click that button, a modal will appear where you can choose to +cherry-pick the changes directly into the selected branch or you can opt to +create a new merge request with the cherry-pick changes ## Cherry-picking a Commit @@ -27,15 +22,9 @@ You can cherry-pick a Commit from the Commit details page: ![Cherry-pick commit](img/cherry_pick_changes_commit.png) ---- - -Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes -directly into the target branch or create a new Merge Request to cherry-pick the -changes: - -![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png) - ---- +Similar to cherry-picking a merge request, you can opt to cherry-pick the changes +directly into the target branch or create a new merge request to cherry-pick the +changes. Please note that when cherry-picking merge commits, the mainline will always be the first parent. If you want to use a different mainline then you need to do that diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png Binary files differindex 5ab094ab367..7dc344f8cf6 100644 --- a/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png +++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png Binary files differdeleted file mode 100644 index 42dcb9203ec..00000000000 --- a/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png Binary files differindex 71227747182..811b0998f85 100644 --- a/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png +++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png diff --git a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png Binary files differdeleted file mode 100644 index 604eb22f51c..00000000000 --- a/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png Binary files differdeleted file mode 100644 index e612a39716e..00000000000 --- a/doc/user/project/merge_requests/img/commit_compare.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/merge_request.png b/doc/user/project/merge_requests/img/merge_request.png Binary files differnew file mode 100644 index 00000000000..f9ca6348953 --- /dev/null +++ b/doc/user/project/merge_requests/img/merge_request.png diff --git a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png Binary files differindex 33f5a4a7a02..d7f0535d3c5 100644 --- a/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png +++ b/doc/user/project/merge_requests/img/merge_when_pipeline_succeeds_enable.png diff --git a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png Binary files differdeleted file mode 100644 index ef7b6dae553..00000000000 --- a/doc/user/project/merge_requests/img/revert_changes_commit_modal.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png Binary files differdeleted file mode 100644 index f6540c9dd33..00000000000 --- a/doc/user/project/merge_requests/img/revert_changes_mr_modal.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png Binary files differindex 33c58d2abff..3883fb4bc1c 100644 --- a/doc/user/project/merge_requests/img/versions.png +++ b/doc/user/project/merge_requests/img/versions.png diff --git a/doc/user/project/merge_requests/img/versions_compare.png b/doc/user/project/merge_requests/img/versions_compare.png Binary files differindex db978ea7b1d..f5bd85dc7c1 100644 --- a/doc/user/project/merge_requests/img/versions_compare.png +++ b/doc/user/project/merge_requests/img/versions_compare.png diff --git a/doc/user/project/merge_requests/img/versions_dropdown.png b/doc/user/project/merge_requests/img/versions_dropdown.png Binary files differindex 889a2d93e6c..cc70a5bf14b 100644 --- a/doc/user/project/merge_requests/img/versions_dropdown.png +++ b/doc/user/project/merge_requests/img/versions_dropdown.png diff --git a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png Binary files differindex 047b0b4620f..0c492aca363 100644 --- a/doc/user/project/merge_requests/img/wip_blocked_accept_button.png +++ b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png diff --git a/doc/user/project/merge_requests/img/wip_mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png Binary files differindex 8bd206bc24a..e405879b28a 100644 --- a/doc/user/project/merge_requests/img/wip_mark_as_wip.png +++ b/doc/user/project/merge_requests/img/wip_mark_as_wip.png diff --git a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png Binary files differindex c0bfa6a35a2..d7f8c419945 100644 --- a/doc/user/project/merge_requests/img/wip_unmark_as_wip.png +++ b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 8e081b4f0b8..6289fcf3c2b 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -3,6 +3,8 @@ Merge requests allow you to exchange changes you made to source code and collaborate with other people on the same project. +![Merge request view](img/merge_request.png) + ## Overview A Merge Request (**MR**) is the basis of GitLab as a code collaboration diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md index 5ead9f4177f..8cf8a59dbfe 100644 --- a/doc/user/project/merge_requests/revert_changes.md +++ b/doc/user/project/merge_requests/revert_changes.md @@ -2,51 +2,39 @@ > [Introduced][ce-1990] in GitLab 8.5. ---- - GitLab implements Git's powerful feature to [revert any commit][git-revert] -with introducing a **Revert** button in Merge Requests and commit details. +with introducing a **Revert** button in merge requests and commit details. ## Reverting a Merge Request -_**Note:** The **Revert** button will only be available for Merge Requests -created since GitLab 8.5. However, you can still revert a Merge Request -by reverting the merge commit from the list of Commits page._ +NOTE: **Note:** +The **Revert** button will only be available for merge requests +created since GitLab 8.5. However, you can still revert a merge request +by reverting the merge commit from the list of Commits page. After the Merge Request has been merged, a **Revert** button will be available -to revert the changes introduced by that Merge Request: - -![Revert Merge Request](img/revert_changes_mr.png) - ---- - -You can revert the changes directly into the selected branch or you can opt to -create a new Merge Request with the revert changes: +to revert the changes introduced by that merge request. -![Revert Merge Request modal](img/revert_changes_mr_modal.png) +![Revert Merge Request](img/cherry_pick_changes_mr.png) ---- +After you click that button, a modal will appear where you can choose to +revert the changes directly into the selected branch or you can opt to +create a new merge request with the revert changes. -After the Merge Request has been reverted, the **Revert** button will not be +After the merge request has been reverted, the **Revert** button will not be available anymore. ## Reverting a Commit You can revert a Commit from the Commit details page: -![Revert commit](img/revert_changes_commit.png) - ---- - -Similar to reverting a Merge Request, you can opt to revert the changes -directly into the target branch or create a new Merge Request to revert the -changes: - -![Revert commit modal](img/revert_changes_commit_modal.png) +![Revert commit](img/cherry_pick_changes_commit.png) ---- +Similar to reverting a merge request, you can opt to revert the changes +directly into the target branch or create a new merge request to revert the +changes. -After the Commit has been reverted, the **Revert** button will not be available +After the commit has been reverted, the **Revert** button will not be available anymore. Please note that when reverting merge commits, the mainline will always be the diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index f88738b4c61..3490bbd968c 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -26,7 +26,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I fork to my namespace' do - page.within '.fork-namespaces' do + page.within '.fork-thumbnail-container' do click_link current_user.name end end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 22b735c6f7b..89b654253cb 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -53,14 +53,15 @@ module Gitlab # Rugged repo object attr_reader :rugged - attr_reader :storage, :gl_repository, :relative_path + attr_reader :storage, :gl_repository, :relative_path, :gitaly_resolver - # 'path' must be the path to a _bare_ git repository, e.g. - # /path/to/my-repo.git + # This initializer method is only used on the client side (gitlab-ce). + # Gitaly-ruby uses a different initializer. def initialize(storage, relative_path, gl_repository) @storage = storage @relative_path = relative_path @gl_repository = gl_repository + @gitaly_resolver = Gitlab::GitalyClient storage_path = Gitlab.config.repositories.storages[@storage]['path'] @path = File.join(storage_path, @relative_path) @@ -676,7 +677,13 @@ module Gitlab end def rm_branch(branch_name, user:) - OperationService.new(user, self).rm_branch(find_branch(branch_name)) + gitaly_migrate(:operation_user_delete_branch) do |is_enabled| + if is_enabled + gitaly_operations_client.user_delete_branch(branch_name, user) + else + OperationService.new(user, self).rm_branch(find_branch(branch_name)) + end + end end def rm_tag(tag_name, user:) @@ -981,9 +988,9 @@ module Gitlab def with_repo_tmp_commit(start_repository, start_branch_name, sha) tmp_ref = fetch_ref( - start_repository.path, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - "refs/tmp/#{SecureRandom.hex}/head" + start_repository, + source_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", + target_ref: "refs/tmp/#{SecureRandom.hex}/head" ) yield commit(sha) @@ -1015,13 +1022,27 @@ module Gitlab end end - def write_ref(ref_path, sha) - rugged.references.create(ref_path, sha, force: true) + def write_ref(ref_path, ref) + raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') + raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") + + command = [Gitlab.config.git.bin_path] + %w[update-ref --stdin -z] + input = "update #{ref_path}\x00#{ref}\x00\x00" + output, status = circuit_breaker.perform do + popen(command, path) { |stdin| stdin.write(input) } + end + + raise GitError, output unless status.zero? end - def fetch_ref(source_path, source_ref, target_ref) - args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - message, status = run_git(args) + def fetch_ref(source_repository, source_ref:, target_ref:) + message, status = GitalyClient.migrate(:fetch_ref) do |is_enabled| + if is_enabled + gitaly_fetch_ref(source_repository, source_ref: source_ref, target_ref: target_ref) + else + local_fetch_ref(source_repository.path, source_ref: source_ref, target_ref: target_ref) + end + end # Make sure ref was created, and raise Rugged::ReferenceError when not raise Rugged::ReferenceError, message if status != 0 @@ -1030,9 +1051,9 @@ module Gitlab end # Refactoring aid; allows us to copy code from app/models/repository.rb - def run_git(args) + def run_git(args, env: {}) circuit_breaker.perform do - popen([Gitlab.config.git.bin_path, *args], path) + popen([Gitlab.config.git.bin_path, *args], path, env) end end @@ -1489,9 +1510,33 @@ module Gitlab OperationService.new(user, self).add_branch(branch_name, target_object.oid) find_branch(branch_name) - rescue Rugged::ReferenceError + rescue Rugged::ReferenceError => ex raise InvalidRef, ex end + + def local_fetch_ref(source_path, source_ref:, target_ref:) + args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) + run_git(args) + end + + def gitaly_fetch_ref(source_repository, source_ref:, target_ref:) + gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh')) + gitaly_address = gitaly_resolver.address(source_repository.storage) + gitaly_token = gitaly_resolver.token(source_repository.storage) + + request = Gitaly::SSHUploadPackRequest.new(repository: source_repository.gitaly_repository) + env = { + 'GITALY_ADDRESS' => gitaly_address, + 'GITALY_PAYLOAD' => request.to_json, + 'GITALY_WD' => Dir.pwd, + 'GIT_SSH_COMMAND' => "#{gitaly_ssh} upload-pack" + } + env['GITALY_TOKEN'] = gitaly_token if gitaly_token.present? + + args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref}) + + run_git(args, env: env) + end end end end diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb index e0943d3a3eb..92a6a672534 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -31,7 +31,7 @@ module Gitlab output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys) unless status.zero? - raise "Got a non-zero exit code while calling out `#{args.join(' ')}`." + raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}" end output.split("\n") diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 46bd5c18603..81ddaf13e10 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -60,6 +60,20 @@ module Gitlab target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit) Gitlab::Git::Branch.new(@repository, branch.name, target_commit.id, target_commit) end + + def user_delete_branch(branch_name, user) + request = Gitaly::UserDeleteBranchRequest.new( + repository: @gitaly_repo, + branch_name: GitalyClient.encode(branch_name), + user: Util.gitaly_user(user) + ) + + response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request) + + if pre_receive_error = response.pre_receive_error.presence + raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error + end + end end end end diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb index 104280f520a..d7d24eeb37b 100644 --- a/lib/gitlab/sidekiq_middleware/memory_killer.rb +++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb @@ -25,7 +25,7 @@ module Gitlab Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\ "#{MAX_RSS}" - Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']}"\ + Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} - Worker #{worker.class} - JID-#{job['jid']} "\ "in #{GRACE_TIME} seconds" sleep(GRACE_TIME) diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb index 222021e8802..f30c771837a 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -12,8 +12,9 @@ module Gitlab # # Project.where("id IN (#{sql})") class Union - def initialize(relations) + def initialize(relations, remove_duplicates: true) @relations = relations + @remove_duplicates = remove_duplicates end def to_sql @@ -25,7 +26,11 @@ module Gitlab @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?) end - fragments.join("\nUNION\n") + fragments.join("\n#{union_keyword}\n") + end + + def union_keyword + @remove_duplicates ? 'UNION' : 'UNION ALL' end end end diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index 259a755d724..a42f02a84fd 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -3,8 +3,8 @@ namespace :gitlab do desc 'GitLab | Assets | Compile all frontend assets' task compile: [ 'yarn:check', - 'rake:assets:precompile', 'gettext:po_to_json', + 'rake:assets:precompile', 'webpack:compile', 'fix_urls' ] diff --git a/qa/Gemfile b/qa/Gemfile index 5d089a45934..ff29824529f 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -1,5 +1,6 @@ source 'https://rubygems.org' +gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'capybara', '~> 2.12.1' gem 'capybara-screenshot', '~> 1.0.14' gem 'rake', '~> 12.0.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 4dd71aa5010..95aeef10752 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -3,6 +3,7 @@ GEM specs: addressable (2.5.0) public_suffix (~> 2.0, >= 2.0.2) + byebug (9.0.6) capybara (2.12.1) addressable mime-types (>= 1.16) @@ -13,22 +14,27 @@ GEM capybara-screenshot (1.0.14) capybara (>= 1.0, < 3) launchy - capybara-webkit (1.12.0) - capybara (>= 2.3.0, < 2.13.0) - json childprocess (0.7.0) ffi (~> 1.0, >= 1.0.11) + coderay (1.1.1) diff-lcs (1.3) ffi (1.9.18) - json (2.0.3) launchy (2.4.3) addressable (~> 2.3) + method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mini_portile2 (2.1.0) nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) + pry (0.10.4) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-byebug (3.4.2) + byebug (~> 9.0) + pry (~> 0.10) public_suffix (2.0.5) rack (2.0.1) rack-test (0.6.3) @@ -52,6 +58,7 @@ GEM childprocess (~> 0.5) rubyzip (~> 1.0) websocket (~> 1.0) + slop (3.6.0) websocket (1.2.4) xpath (2.0.0) nokogiri (~> 1.3) @@ -62,7 +69,7 @@ PLATFORMS DEPENDENCIES capybara (~> 2.12.1) capybara-screenshot (~> 1.0.14) - capybara-webkit (~> 1.12.0) + pry-byebug (~> 3.4.1) rake (~> 12.0.0) rspec (~> 3.5) selenium-webdriver (~> 2.53) diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb index f4619042e34..baa06b1c75e 100644 --- a/qa/qa/page/admin/menu.rb +++ b/qa/qa/page/admin/menu.rb @@ -4,8 +4,6 @@ module QA class Menu < Page::Base def go_to_license link = find_link 'License' - # Click space to scroll this link into the view - link.send_keys(:space) link.click end end diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb index 4dfdd6cd93c..79c681168cc 100644 --- a/qa/qa/specs/config.rb +++ b/qa/qa/specs/config.rb @@ -43,8 +43,7 @@ module QA Capybara.register_driver :chrome do |app| capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( 'chromeOptions' => { - 'binary' => '/usr/bin/google-chrome-stable', - 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1024] + 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] } ) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 629c131aee6..e46d1995498 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -96,18 +96,6 @@ describe Projects::MergeRequestsController do expect(response).to match_response_schema('entities/merge_request') end end - - context 'number of queries', :request_store do - it 'verifies number of queries' do - # pre-create objects - merge_request - - recorded = ActiveRecord::QueryRecorder.new { go(format: :json) } - - expect(recorded.count).to be_within(5).of(30) - expect(recorded.cached_count).to eq(0) - end - end end describe "as diff" do diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb index 2805968dcd9..5d9d5351687 100644 --- a/spec/controllers/projects/registry/repositories_controller_spec.rb +++ b/spec/controllers/projects/registry/repositories_controller_spec.rb @@ -42,6 +42,13 @@ describe Projects::Registry::RepositoriesController do expect { go_to_index }.to change { ContainerRepository.all.count }.by(1) expect(ContainerRepository.first).to be_root_repository end + + it 'json has a list of projects' do + go_to_index(format: :json) + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('registry/repositories') + end end context 'when there are no tags for this repository' do @@ -58,6 +65,31 @@ describe Projects::Registry::RepositoriesController do it 'does not ensure root container repository' do expect { go_to_index }.not_to change { ContainerRepository.all.count } end + + it 'responds with json if asked' do + go_to_index(format: :json) + + expect(response).to have_http_status(:ok) + expect(json_response).to be_kind_of(Array) + end + end + end + end + + describe 'DELETE destroy' do + context 'when root container repository exists' do + let!(:repository) do + create(:container_repository, :root, project: project) + end + + before do + stub_container_registry_tags(repository: :any, tags: []) + end + + it 'deletes a repository' do + expect { delete_repository(repository) }.to change { ContainerRepository.all.count }.by(-1) + + expect(response).to have_http_status(:no_content) end end end @@ -77,8 +109,16 @@ describe Projects::Registry::RepositoriesController do end end - def go_to_index + def go_to_index(format: :html) get :index, namespace_id: project.namespace, - project_id: project + project_id: project, + format: format + end + + def delete_repository(repository) + delete :destroy, namespace_id: project.namespace, + project_id: project, + id: repository, + format: :json end end diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb index f4af3587d23..bb702ebeb23 100644 --- a/spec/controllers/projects/registry/tags_controller_spec.rb +++ b/spec/controllers/projects/registry/tags_controller_spec.rb @@ -4,24 +4,83 @@ describe Projects::Registry::TagsController do let(:user) { create(:user) } let(:project) { create(:project, :private) } + let(:repository) do + create(:container_repository, name: 'image', project: project) + end + before do sign_in(user) stub_container_registry_config(enabled: true) end - context 'when user has access to registry' do + describe 'GET index' do + let(:tags) do + Array.new(40) { |i| "tag#{i}" } + end + before do - project.add_developer(user) + stub_container_registry_tags(repository: /image/, tags: tags) end - describe 'POST destroy' do + context 'when user can control the registry' do + before do + project.add_developer(user) + end + + it 'receive a list of tags' do + get_tags + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('registry/tags') + expect(response).to include_pagination_headers + end + end + + context 'when user can read the registry' do + before do + project.add_reporter(user) + end + + it 'receive a list of tags' do + get_tags + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('registry/tags') + expect(response).to include_pagination_headers + end + end + + context 'when user does not have access to registry' do + before do + project.add_guest(user) + end + + it 'does not receive a list of tags' do + get_tags + + expect(response).to have_http_status(:not_found) + end + end + + private + + def get_tags + get :index, namespace_id: project.namespace, + project_id: project, + repository_id: repository, + format: :json + end + end + + describe 'POST destroy' do + context 'when user has access to registry' do + before do + project.add_developer(user) + end + context 'when there is matching tag present' do before do - stub_container_registry_tags(repository: /image/, tags: %w[rc1 test.]) - end - - let(:repository) do - create(:container_repository, name: 'image', project: project) + stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.]) end it 'makes it possible to delete regular tag' do @@ -37,12 +96,15 @@ describe Projects::Registry::TagsController do end end end - end - def destroy_tag(name) - post :destroy, namespace_id: project.namespace, - project_id: project, - repository_id: repository, - id: name + private + + def destroy_tag(name) + post :destroy, namespace_id: project.namespace, + project_id: project, + repository_id: repository, + id: name, + format: :json + end end end diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index e5abfd67d60..0dd1238d6e2 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -12,7 +12,7 @@ FactoryGirl.define do deployment.project ||= deployment.environment.project unless deployment.project.repository_exists? - allow(deployment.project.repository).to receive(:fetch_ref) + allow(deployment.project.repository).to receive(:create_ref) end end end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index ae39ba4da6b..45213dc6995 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Container Registry" do +describe "Container Registry", js: true do let(:user) { create(:user) } let(:project) { create(:project) } @@ -41,16 +41,19 @@ describe "Container Registry" do expect_any_instance_of(ContainerRepository) .to receive(:delete_tags!).and_return(true) - click_on 'Remove repository' + click_on(class: 'js-remove-repo') end scenario 'user removes a specific tag from container repository' do visit_container_registry + find('.js-toggle-repo').trigger('click') + wait_for_requests + expect_any_instance_of(ContainerRegistry::Tag) .to receive(:delete).and_return(true) - click_on 'Remove tag' + click_on(class: 'js-delete-registry') end end diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb index ad06cee4e81..2f407b13c2f 100644 --- a/spec/features/projects/branches/download_buttons_spec.rb +++ b/spec/features/projects/branches/download_buttons_spec.rb @@ -29,7 +29,7 @@ feature 'Download buttons in branches page' do describe 'when checking branches' do context 'with artifacts' do before do - visit project_branches_path(project) + visit project_branches_path(project, search: 'binary-encoding') end scenario 'shows download artifacts button' do diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index ad4527a0b74..d1f5623554d 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -5,12 +5,6 @@ describe 'Branches' do let(:project) { create(:project, :public, :repository) } let(:repository) { project.repository } - def set_protected_branch_name(branch_name) - find(".js-protected-branch-select").click - find(".dropdown-input-field").set(branch_name) - click_on("Create wildcard #{branch_name}") - end - context 'logged in as developer' do before do sign_in(user) @@ -18,12 +12,10 @@ describe 'Branches' do end describe 'Initial branches page' do - it 'shows all the branches' do + it 'shows all the branches sorted by last updated by default' do visit project_branches_path(project) - repository.branches_sorted_by(:name).first(20).each do |branch| - expect(page).to have_content("#{branch.name}") - end + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc)) end it 'sorts the branches by name' do @@ -32,22 +24,7 @@ describe 'Branches' do click_button "Last updated" # Open sorting dropdown click_link "Name" - sorted = repository.branches_sorted_by(:name).first(20).map do |branch| - Regexp.escape(branch.name) - end - expect(page).to have_content(/#{sorted.join(".*")}/) - end - - it 'sorts the branches by last updated' do - visit project_branches_path(project) - - click_button "Last updated" # Open sorting dropdown - click_link "Last updated" - - sorted = repository.branches_sorted_by(:updated_desc).first(20).map do |branch| - Regexp.escape(branch.name) - end - expect(page).to have_content(/#{sorted.join(".*")}/) + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :name)) end it 'sorts the branches by oldest updated' do @@ -56,10 +33,7 @@ describe 'Branches' do click_button "Last updated" # Open sorting dropdown click_link "Oldest updated" - sorted = repository.branches_sorted_by(:updated_asc).first(20).map do |branch| - Regexp.escape(branch.name) - end - expect(page).to have_content(/#{sorted.join(".*")}/) + expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_asc)) end it 'avoids a N+1 query in branches index' do @@ -99,28 +73,6 @@ describe 'Branches' do expect(find('.all-branches')).to have_selector('li', count: 0) end end - - describe 'Delete protected branch' do - before do - project.add_user(user, :master) - visit project_protected_branches_path(project) - set_protected_branch_name('fix') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('fix') } - expect(ProtectedBranch.count).to eq(1) - project.add_user(user, :developer) - end - - it 'does not allow devleoper to removes protected branch', js: true do - visit project_branches_path(project) - - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_css('.btn-remove.disabled') - end - end end context 'logged in as master' do @@ -136,37 +88,6 @@ describe 'Branches' do expect(page).to have_content("Protected branches can be managed in project settings") end end - - describe 'Delete protected branch' do - before do - visit project_protected_branches_path(project) - set_protected_branch_name('fix') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('fix') } - expect(ProtectedBranch.count).to eq(1) - end - - it 'removes branch after modal confirmation', js: true do - visit project_branches_path(project) - - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_content('fix') - expect(find('.all-branches')).to have_selector('li', count: 1) - page.find('[data-target="#modal-delete-branch"]').trigger(:click) - - expect(page).to have_css('.js-delete-branch[disabled]') - fill_in 'delete_branch_input', with: 'fix' - click_link 'Delete protected branch' - - fill_in 'branch-search', with: 'fix' - find('#branch-search').native.send_keys(:enter) - - expect(page).to have_content('No branches to show') - end - end end context 'logged out' do @@ -180,4 +101,13 @@ describe 'Branches' do end end end + + def sorted_branches(repository, count:, sort_by:) + sorted_branches = + repository.branches_sorted_by(sort_by).first(count).map do |branch| + Regexp.escape(branch.name) + end + + Regexp.new(sorted_branches.join('.*')) + end end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 3677bf38724..bf9885f73bd 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,93 +1,153 @@ require 'spec_helper' -feature 'Protected Branches', js: true do - let(:user) { create(:user, :admin) } +feature 'Protected Branches', :js do + let(:user) { create(:user) } + let(:admin) { create(:admin) } let(:project) { create(:project, :repository) } - before do - sign_in(user) - end + context 'logged in as developer' do + before do + project.add_developer(user) + sign_in(user) + end - def set_protected_branch_name(branch_name) - find(".js-protected-branch-select").trigger('click') - find(".dropdown-input-field").set(branch_name) - click_on("Create wildcard #{branch_name}") - end + describe 'Delete protected branch' do + before do + create(:protected_branch, project: project, name: 'fix') + expect(ProtectedBranch.count).to eq(1) + end + + it 'does not allow developer to removes protected branch' do + visit project_branches_path(project) + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) - describe "explicit protected branches" do - it "allows creating explicit protected branches" do - visit project_protected_branches_path(project) - set_protected_branch_name('some-branch') - click_on "Protect" + expect(page).to have_css('.btn-remove.disabled') + end + end + end - within(".protected-branches-list") { expect(page).to have_content('some-branch') } - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.name).to eq('some-branch') + context 'logged in as master' do + before do + project.add_master(user) + sign_in(user) end - it "displays the last commit on the matching branch if it exists" do - commit = create(:commit, project: project) - project.repository.add_branch(user, 'some-branch', commit.id) + describe 'Delete protected branch' do + before do + create(:protected_branch, project: project, name: 'fix') + expect(ProtectedBranch.count).to eq(1) + end - visit project_protected_branches_path(project) - set_protected_branch_name('some-branch') - click_on "Protect" + it 'removes branch after modal confirmation' do + visit project_branches_path(project) - within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) } - end + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) - it "displays an error message if the named branch does not exist" do - visit project_protected_branches_path(project) - set_protected_branch_name('some-branch') - click_on "Protect" + expect(page).to have_content('fix') + expect(find('.all-branches')).to have_selector('li', count: 1) + page.find('[data-target="#modal-delete-branch"]').trigger(:click) - within(".protected-branches-list") { expect(page).to have_content('branch was removed') } + expect(page).to have_css('.js-delete-branch[disabled]') + fill_in 'delete_branch_input', with: 'fix' + click_link 'Delete protected branch' + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) + + expect(page).to have_content('No branches to show') + end end end - describe "wildcard protected branches" do - it "allows creating protected branches with a wildcard" do - visit project_protected_branches_path(project) - set_protected_branch_name('*-stable') - click_on "Protect" - - within(".protected-branches-list") { expect(page).to have_content('*-stable') } - expect(ProtectedBranch.count).to eq(1) - expect(ProtectedBranch.last.name).to eq('*-stable') + context 'logged in as admin' do + before do + sign_in(admin) end - it "displays the number of matching branches" do - project.repository.add_branch(user, 'production-stable', 'master') - project.repository.add_branch(user, 'staging-stable', 'master') + describe "explicit protected branches" do + it "allows creating explicit protected branches" do + visit project_protected_branches_path(project) + set_protected_branch_name('some-branch') + click_on "Protect" - visit project_protected_branches_path(project) - set_protected_branch_name('*-stable') - click_on "Protect" + within(".protected-branches-list") { expect(page).to have_content('some-branch') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('some-branch') + end - within(".protected-branches-list") { expect(page).to have_content("2 matching branches") } + it "displays the last commit on the matching branch if it exists" do + commit = create(:commit, project: project) + project.repository.add_branch(admin, 'some-branch', commit.id) + + visit project_protected_branches_path(project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content(commit.id[0..7]) } + end + + it "displays an error message if the named branch does not exist" do + visit project_protected_branches_path(project) + set_protected_branch_name('some-branch') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('branch was removed') } + end end - it "displays all the branches matching the wildcard" do - project.repository.add_branch(user, 'production-stable', 'master') - project.repository.add_branch(user, 'staging-stable', 'master') - project.repository.add_branch(user, 'development', 'master') + describe "wildcard protected branches" do + it "allows creating protected branches with a wildcard" do + visit project_protected_branches_path(project) + set_protected_branch_name('*-stable') + click_on "Protect" + + within(".protected-branches-list") { expect(page).to have_content('*-stable') } + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.name).to eq('*-stable') + end - visit project_protected_branches_path(project) - set_protected_branch_name('*-stable') - click_on "Protect" + it "displays the number of matching branches" do + project.repository.add_branch(admin, 'production-stable', 'master') + project.repository.add_branch(admin, 'staging-stable', 'master') - visit project_protected_branches_path(project) - click_on "2 matching branches" + visit project_protected_branches_path(project) + set_protected_branch_name('*-stable') + click_on "Protect" - within(".protected-branches-list") do - expect(page).to have_content("production-stable") - expect(page).to have_content("staging-stable") - expect(page).not_to have_content("development") + within(".protected-branches-list") { expect(page).to have_content("2 matching branches") } end + + it "displays all the branches matching the wildcard" do + project.repository.add_branch(admin, 'production-stable', 'master') + project.repository.add_branch(admin, 'staging-stable', 'master') + project.repository.add_branch(admin, 'development', 'master') + + visit project_protected_branches_path(project) + set_protected_branch_name('*-stable') + click_on "Protect" + + visit project_protected_branches_path(project) + click_on "2 matching branches" + + within(".protected-branches-list") do + expect(page).to have_content("production-stable") + expect(page).to have_content("staging-stable") + expect(page).not_to have_content("development") + end + end + end + + describe "access control" do + include_examples "protected branches > access control > CE" end end - describe "access control" do - include_examples "protected branches > access control > CE" + def set_protected_branch_name(branch_name) + find(".js-protected-branch-select").trigger('click') + find(".dropdown-input-field").set(branch_name) + click_on("Create wildcard #{branch_name}") end end diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json index 0796d9b8af9..30b4e56bc98 100644 --- a/spec/fixtures/api/schemas/entities/merge_request.json +++ b/spec/fixtures/api/schemas/entities/merge_request.json @@ -93,7 +93,7 @@ "merge_commit_message_with_description": { "type": "string" }, "diverged_commits_count": { "type": "integer" }, "commit_change_content_path": { "type": "string" }, - "remove_wip_path": { "type": "string" }, + "remove_wip_path": { "type": ["string", "null"] }, "commits_count": { "type": "integer" }, "remove_source_branch": { "type": ["boolean", "null"] }, "merge_ongoing": { "type": "boolean" }, diff --git a/spec/fixtures/api/schemas/registry/repositories.json b/spec/fixtures/api/schemas/registry/repositories.json new file mode 100644 index 00000000000..4978bd89cda --- /dev/null +++ b/spec/fixtures/api/schemas/registry/repositories.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "repository.json" + } +} diff --git a/spec/fixtures/api/schemas/registry/repository.json b/spec/fixtures/api/schemas/registry/repository.json new file mode 100644 index 00000000000..4175642eb00 --- /dev/null +++ b/spec/fixtures/api/schemas/registry/repository.json @@ -0,0 +1,27 @@ +{ + "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 +} diff --git a/spec/fixtures/api/schemas/registry/tag.json b/spec/fixtures/api/schemas/registry/tag.json new file mode 100644 index 00000000000..5bc307e0e64 --- /dev/null +++ b/spec/fixtures/api/schemas/registry/tag.json @@ -0,0 +1,28 @@ +{ + "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 +} diff --git a/spec/fixtures/api/schemas/registry/tags.json b/spec/fixtures/api/schemas/registry/tags.json new file mode 100644 index 00000000000..c72f957459a --- /dev/null +++ b/spec/fixtures/api/schemas/registry/tags.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "tag.json" + } +} diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index 4bc2205e642..3fd16d76f51 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -41,6 +41,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont remove_repository(project) end + it 'merge_requests/merge_request_of_current_user.html.raw' do |example| + merge_request.update(author: admin) + + render_merge_request(example.description, merge_request) + end + it 'merge_requests/merge_request_with_task_list.html.raw' do |example| create(:ci_build, :pending, pipeline: pipeline) diff --git a/spec/javascripts/notes/stores/helpers.js b/spec/javascripts/helpers/vuex_action_helper.js index 2d386fe1da5..2d386fe1da5 100644 --- a/spec/javascripts/notes/stores/helpers.js +++ b/spec/javascripts/helpers/vuex_action_helper.js diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 6ff42e2378d..3ab901da6b6 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -58,5 +58,44 @@ import IssuablesHelper from '~/helpers/issuables_helper'; expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled(); }); }); + + describe('hideCloseButton', () => { + describe('merge request of another user', () => { + beforeEach(() => { + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + this.el = document.querySelector('.merge-request .issuable-actions'); + const merge = new MergeRequest(); + merge.hideCloseButton(); + }); + + it('hides the dropdown close item and selects the next item', () => { + const closeItem = this.el.querySelector('li.close-item'); + const smallCloseItem = this.el.querySelector('.js-close-item'); + const reportItem = this.el.querySelector('li.report-item'); + + expect(closeItem).toHaveClass('hidden'); + expect(smallCloseItem).toHaveClass('hidden'); + expect(reportItem).toHaveClass('droplab-item-selected'); + expect(reportItem).not.toHaveClass('hidden'); + }); + }); + + describe('merge request of current_user', () => { + beforeEach(() => { + loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + this.el = document.querySelector('.merge-request .issuable-actions'); + const merge = new MergeRequest(); + merge.hideCloseButton(); + }); + + it('hides the close button', () => { + const closeButton = this.el.querySelector('.btn-close'); + const smallCloseItem = this.el.querySelector('.js-close-item'); + + expect(closeButton).toHaveClass('hidden'); + expect(smallCloseItem).toHaveClass('hidden'); + }); + }); + }); }); }).call(window); diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js index 1c8b1b98242..3f659af5c3b 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/issue_comment_form_spec.js @@ -33,6 +33,30 @@ describe('issue_comment_form component', () => { expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path); }); + describe('handleSave', () => { + it('should request to save note when note is entered', () => { + vm.note = 'hello world'; + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + spyOn(vm, 'resizeTextarea'); + spyOn(vm, 'stopPolling'); + + vm.handleSave(); + expect(vm.isSubmitting).toEqual(true); + expect(vm.note).toEqual(''); + expect(vm.saveNote).toHaveBeenCalled(); + expect(vm.stopPolling).toHaveBeenCalled(); + expect(vm.resizeTextarea).toHaveBeenCalled(); + }); + + it('should toggle issue state when no note', () => { + spyOn(vm, 'toggleIssueState'); + + vm.handleSave(); + + expect(vm.toggleIssueState).toHaveBeenCalled(); + }); + }); + describe('textarea', () => { it('should render textarea with placeholder', () => { expect( @@ -40,6 +64,22 @@ describe('issue_comment_form component', () => { ).toEqual('Write a comment or drag your files here...'); }); + it('should make textarea disabled while requesting', (done) => { + const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button')); + vm.note = 'hello world'; + spyOn(vm, 'stopPolling'); + spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {})); + + vm.$nextTick(() => { // Wait for vm.note change triggered. It should enable $submitButton. + $submitButton.trigger('click'); + + vm.$nextTick(() => { // Wait for vm.isSubmitting triggered. It should disable textarea. + expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy(); + done(); + }); + }); + }); + it('should support quick actions', () => { expect( vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'), diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 2b2219dcf0c..3d1ca870ca4 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,5 +1,5 @@ import * as actions from '~/notes/stores/actions'; -import testAction from './helpers'; +import testAction from '../../helpers/vuex_action_helper'; import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data'; describe('Actions Notes Store', () => { diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js new file mode 100644 index 00000000000..43e7d9e1224 --- /dev/null +++ b/spec/javascripts/registry/components/app_spec.js @@ -0,0 +1,122 @@ +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(); + }); + }); + }); +}); diff --git a/spec/javascripts/registry/components/collapsible_container_spec.js b/spec/javascripts/registry/components/collapsible_container_spec.js new file mode 100644 index 00000000000..5891921318a --- /dev/null +++ b/spec/javascripts/registry/components/collapsible_container_spec.js @@ -0,0 +1,58 @@ +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(); + }); + }); +}); diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js new file mode 100644 index 00000000000..6aa61afc445 --- /dev/null +++ b/spec/javascripts/registry/components/table_registry_spec.js @@ -0,0 +1,49 @@ +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(); + }); + }); +}); diff --git a/spec/javascripts/registry/getters_spec.js b/spec/javascripts/registry/getters_spec.js new file mode 100644 index 00000000000..3d989541881 --- /dev/null +++ b/spec/javascripts/registry/getters_spec.js @@ -0,0 +1,43 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js new file mode 100644 index 00000000000..18600d00bff --- /dev/null +++ b/spec/javascripts/registry/mock_data.js @@ -0,0 +1,122 @@ +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, + }, +}; diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js new file mode 100644 index 00000000000..3c9da4f107b --- /dev/null +++ b/spec/javascripts/registry/stores/actions_spec.js @@ -0,0 +1,85 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/registry/stores/mutations_spec.js b/spec/javascripts/registry/stores/mutations_spec.js new file mode 100644 index 00000000000..2e4c0659daa --- /dev/null +++ b/spec/javascripts/registry/stores/mutations_spec.js @@ -0,0 +1,81 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 2422e844e97..c83b947579b 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -95,35 +95,84 @@ describe('MRWidgetReadyToMerge', () => { }); }); + describe('status', () => { + it('defaults to success', () => { + vm.mr.pipeline = true; + expect(vm.status).toEqual('success'); + }); + + it('returns failed when MR has CI but also has an unknown status', () => { + vm.mr.hasCI = true; + expect(vm.status).toEqual('failed'); + }); + + it('returns default when MR has no pipeline', () => { + expect(vm.status).toEqual('success'); + }); + + it('returns pending when pipeline is active', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineActive = true; + expect(vm.status).toEqual('pending'); + }); + + it('returns failed when pipeline is failed', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineFailed = true; + expect(vm.status).toEqual('failed'); + }); + }); + describe('mergeButtonClass', () => { const defaultClass = 'btn btn-sm btn-success accept-merge-request'; const failedClass = `${defaultClass} btn-danger`; const inActionClass = `${defaultClass} btn-info`; - it('should return default class', () => { + it('defaults to success class', () => { + expect(vm.mergeButtonClass).toEqual(defaultClass); + }); + + it('returns success class for success status', () => { vm.mr.pipeline = true; expect(vm.mergeButtonClass).toEqual(defaultClass); }); - it('should return failed class when MR has CI but also has an unknown status', () => { + it('returns info class for pending status', () => { + vm.mr.pipeline = {}; + vm.mr.isPipelineActive = true; + expect(vm.mergeButtonClass).toEqual(inActionClass); + }); + + it('returns failed class for failed status', () => { vm.mr.hasCI = true; expect(vm.mergeButtonClass).toEqual(failedClass); }); + }); - it('should return default class when MR has no pipeline', () => { - expect(vm.mergeButtonClass).toEqual(defaultClass); + describe('status icon', () => { + it('defaults to tick icon', () => { + expect(vm.iconClass).toEqual('success'); }); - it('should return in action class when pipeline is active', () => { + it('shows tick for success status', () => { + vm.mr.pipeline = true; + expect(vm.iconClass).toEqual('success'); + }); + + it('shows tick for pending status', () => { vm.mr.pipeline = {}; vm.mr.isPipelineActive = true; - expect(vm.mergeButtonClass).toEqual(inActionClass); + expect(vm.iconClass).toEqual('success'); }); - it('should return failed class when pipeline is failed', () => { - vm.mr.pipeline = {}; - vm.mr.isPipelineFailed = true; - expect(vm.mergeButtonClass).toEqual(failedClass); + it('shows x for failed status', () => { + vm.mr.hasCI = true; + expect(vm.iconClass).toEqual('failed'); + }); + + it('shows x for merge not allowed', () => { + vm.mr.hasCI = true; + expect(vm.iconClass).toEqual('failed'); }); }); @@ -177,7 +226,7 @@ describe('MRWidgetReadyToMerge', () => { expect(vm.isMergeButtonDisabled).toBeTruthy(); }); - it('should return true when there vm instance is making request', () => { + it('should return true when the vm instance is making request', () => { vm.isMakingRequest = true; expect(vm.isMergeButtonDisabled).toBeTruthy(); }); diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index a0482e30a33..5f12125beb2 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1444,6 +1444,51 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#rm_branch' do + shared_examples "user deleting a branch" do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } + let(:user) { create(:user) } + let(:branch_name) { "to-be-deleted-soon" } + + before do + project.team << [user, :developer] + repository.create_branch(branch_name) + end + + it "removes the branch from the repo" do + repository.rm_branch(branch_name, user: user) + + expect(repository.rugged.branches[branch_name]).to be_nil + end + end + + context "when Gitaly user_delete_branch is enabled" do + it_behaves_like "user deleting a branch" + end + + context "when Gitaly user_delete_branch is disabled", skip_gitaly_mock: true do + it_behaves_like "user deleting a branch" + end + end + + describe '#write_ref' do + context 'validations' do + using RSpec::Parameterized::TableSyntax + + where(:ref_path, :ref) do + 'foo bar' | '123' + 'foobar' | "12\x003" + end + + with_them do + it 'raises ArgumentError' do + expect { repository.write_ref(ref_path, ref) }.to raise_error(ArgumentError) + end + end + end + end + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) source_branch = repository.branches.find { |branch| branch.name == source_branch_name } rugged = repository.rugged diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 769b14687ac..7bd6a7fa842 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -4,10 +4,10 @@ describe Gitlab::GitalyClient::OperationService do let(:project) { create(:project) } let(:repository) { project.repository.raw } let(:client) { described_class.new(repository) } + let(:user) { create(:user) } + let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) } describe '#user_create_branch' do - let(:user) { create(:user) } - let(:gitaly_user) { Gitlab::GitalyClient::Util.gitaly_user(user) } let(:branch_name) { 'new' } let(:start_point) { 'master' } let(:request) do @@ -52,4 +52,41 @@ describe Gitlab::GitalyClient::OperationService do end end end + + describe '#user_delete_branch' do + let(:branch_name) { 'my-branch' } + let(:request) do + Gitaly::UserDeleteBranchRequest.new( + repository: repository.gitaly_repository, + branch_name: branch_name, + user: gitaly_user + ) + end + let(:response) { Gitaly::UserDeleteBranchResponse.new } + + subject { client.user_delete_branch(branch_name, user) } + + it 'sends a user_delete_branch message' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_delete_branch).with(request, kind_of(Hash)) + .and_return(response) + + subject + end + + context "when pre_receive_error is present" do + let(:response) do + Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed") + end + + it "throws a PreReceive exception" do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_delete_branch).with(request, kind_of(Hash)) + .and_return(response) + + expect { subject }.to raise_error( + Gitlab::Git::HooksService::PreReceiveError, "something failed") + end + end + end end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index baf8f6644bf..8026fba9f0a 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -22,5 +22,12 @@ describe Gitlab::SQL::Union do expect {User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end + + it 'uses UNION ALL when removing duplicates is disabled' do + union = described_class + .new([relation_1, relation_2], remove_duplicates: false) + + expect(union.to_sql).to include('UNION ALL') + end end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 8eabc4ca72f..81c2057e175 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -155,5 +155,15 @@ describe Key, :mailer do it 'strips white spaces' do expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) end + + it 'invalidates the public_key attribute' do + key = build(:key) + + original = key.public_key + key.key = valid_key + + expect(original.key_text).not_to be_nil + expect(key.public_key.key_text).to eq(valid_key) + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index d80d5657c42..188a0a98ec3 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -791,6 +791,49 @@ describe MergeRequest do end end + describe '#has_ci?' do + let(:merge_request) { build_stubbed(:merge_request) } + + context 'has ci' do + it 'returns true if MR has head_pipeline_id and commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil } + allow(merge_request).to receive(:head_pipeline_id) { double } + allow(merge_request).to receive(:has_no_commits?) { false } + + expect(merge_request.has_ci?).to be(true) + end + + it 'returns true if MR has any pipeline and commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil } + allow(merge_request).to receive(:head_pipeline_id) { nil } + allow(merge_request).to receive(:has_no_commits?) { false } + allow(merge_request).to receive(:all_pipelines) { [double] } + + expect(merge_request.has_ci?).to be(true) + end + + it 'returns true if MR has CI service and commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { double } + allow(merge_request).to receive(:head_pipeline_id) { nil } + allow(merge_request).to receive(:has_no_commits?) { false } + allow(merge_request).to receive(:all_pipelines) { [] } + + expect(merge_request.has_ci?).to be(true) + end + end + + context 'has no ci' do + it 'returns false if MR has no CI service nor pipeline, and no commits' do + allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil } + allow(merge_request).to receive(:head_pipeline_id) { nil } + allow(merge_request).to receive(:all_pipelines) { [] } + allow(merge_request).to receive(:has_no_commits?) { true } + + expect(merge_request.has_ci?).to be(false) + end + end + end + describe '#all_pipelines' do shared_examples 'returning pipelines with proper ordering' do let!(:all_pipelines) do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 8a4dcdc311e..7156c1b7aa8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -636,18 +636,18 @@ describe Repository do describe '#fetch_ref' do describe 'when storage is broken', broken_storage: true do it 'should raise a storage error' do - path = broken_repository.path_to_repo - - expect_to_raise_storage_error { broken_repository.fetch_ref(path, '1', '2') } + expect_to_raise_storage_error do + broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2') + end end end end describe '#create_ref' do - it 'redirects the call to fetch_ref' do + it 'redirects the call to write_ref' do ref, ref_path = '1', '2' - expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path) + expect(repository.raw_repository).to receive(:write_ref).with(ref_path, ref) repository.create_ref(ref, ref_path) end @@ -901,47 +901,6 @@ describe Repository do end end - describe '#rm_branch' do - let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature - let(:blank_sha) { '0000000000000000000000000000000000000000' } - - context 'when pre hooks were successful' do - it 'runs without errors' do - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') - - expect { repository.rm_branch(user, 'feature') }.not_to raise_error - end - - it 'deletes the branch' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) - - expect { repository.rm_branch(user, 'feature') }.not_to raise_error - - expect(repository.find_branch('feature')).to be_nil - end - end - - context 'when pre hooks failed' do - it 'gets an error' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) - - expect do - repository.rm_branch(user, 'feature') - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) - end - - it 'does not delete the branch' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) - - expect do - repository.rm_branch(user, 'feature') - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) - expect(repository.find_branch('feature')).not_to be_nil - end - end - end - describe '#update_branch_with_hooks' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev @@ -1744,13 +1703,75 @@ describe Repository do end describe '#rm_branch' do - let(:user) { create(:user) } + shared_examples "user deleting a branch" do + it 'removes a branch' do + expect(repository).to receive(:before_remove_branch) + expect(repository).to receive(:after_remove_branch) + + repository.rm_branch(user, 'feature') + end + end + + context 'with gitaly enabled' do + it_behaves_like "user deleting a branch" + + context 'when pre hooks failed' do + before do + allow_any_instance_of(Gitlab::GitalyClient::OperationService) + .to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError) + end + + it 'gets an error and does not delete the branch' do + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + + expect(repository.find_branch('feature')).not_to be_nil + end + end + end + + context 'with gitaly disabled', skip_gitaly_mock: true do + it_behaves_like "user deleting a branch" + + let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + let(:blank_sha) { '0000000000000000000000000000000000000000' } + + context 'when pre hooks were successful' do + it 'runs without errors' do + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) + .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') - it 'removes a branch' do - expect(repository).to receive(:before_remove_branch) - expect(repository).to receive(:after_remove_branch) + expect { repository.rm_branch(user, 'feature') }.not_to raise_error + end + + it 'deletes the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) - repository.rm_branch(user, 'feature') + expect { repository.rm_branch(user, 'feature') }.not_to raise_error + + expect(repository.find_branch('feature')).to be_nil + end + end + + context 'when pre hooks failed' do + it 'gets an error' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) + + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + end + + it 'does not delete the branch' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) + + expect do + repository.rm_branch(user, 'feature') + end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + expect(repository.find_branch('feature')).not_to be_nil + end + end end end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index 2187be0190d..5e114434a67 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -300,6 +300,10 @@ describe MergeRequestPresenter do described_class.new(resource, current_user: user).remove_wip_path end + before do + allow(resource).to receive(:work_in_progress?).and_return(true) + end + context 'when merge request enabled and has permission' do it 'has remove_wip_path' do allow(project).to receive(:merge_requests_enabled?) { true } diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb new file mode 100644 index 00000000000..c589cd18f77 --- /dev/null +++ b/spec/serializers/container_repository_entity_spec.rb @@ -0,0 +1,41 @@ +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 diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb new file mode 100644 index 00000000000..6dcc5204516 --- /dev/null +++ b/spec/serializers/container_tag_entity_spec.rb @@ -0,0 +1,43 @@ +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 diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb index 4288955ddbc..4aeb593da44 100644 --- a/spec/serializers/merge_request_entity_spec.rb +++ b/spec/serializers/merge_request_entity_spec.rb @@ -11,16 +11,6 @@ describe MergeRequestEntity do described_class.new(resource, request: request).as_json end - it 'includes author' do - req = double('request') - - author_payload = UserEntity - .represent(resource.author, request: req) - .as_json - - expect(subject[:author]).to eq(author_payload) - end - it 'includes pipeline' do req = double('request', current_user: user) pipeline = build_stubbed(:ci_pipeline) diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 78a2ff73746..5f22d886910 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -39,11 +39,11 @@ module StubGitlabCalls .and_return({ 'tags' => tags }) allow_any_instance_of(ContainerRegistry::Client) - .to receive(:repository_manifest).with(repository) + .to receive(:repository_manifest).with(repository, anything) .and_return(stub_container_registry_tag_manifest) allow_any_instance_of(ContainerRegistry::Client) - .to receive(:blob).with(repository) + .to receive(:blob).with(repository, anything, 'application/octet-stream') .and_return(stub_container_registry_blob) end diff --git a/spec/views/projects/registry/repositories/index.html.haml_spec.rb b/spec/views/projects/registry/repositories/index.html.haml_spec.rb deleted file mode 100644 index cf0aa44a4a2..00000000000 --- a/spec/views/projects/registry/repositories/index.html.haml_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -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 |