diff options
126 files changed, 1482 insertions, 893 deletions
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index 9d53a48409a..aec734870d6 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -1,11 +1,18 @@ Please read this! Before opening a new issue, make sure to search for keywords in the issues -filtered by the "regression" or "bug" label: +filtered by the "regression" or "bug" label. + +For the Community Edition issue tracker: - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=regression - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=bug +For the Enterprise Edition issue tracker: + +- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=regression +- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug + and verify the issue you're about to submit isn't a duplicate. Please remove this notice if you're confident your issue isn't a duplicate. diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md index d96c9ad59e0..85ca1bca623 100644 --- a/.gitlab/issue_templates/Feature Proposal.md +++ b/.gitlab/issue_templates/Feature Proposal.md @@ -3,8 +3,14 @@ Please read this! Before opening a new issue, make sure to search for keywords in the issues filtered by the "feature proposal" label: +For the Community Edition issue tracker: + - https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=feature+proposal +For the Enterprise Edition issue tracker: + +- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=feature+proposal + and verify the issue you're about to submit isn't a duplicate. Please remove this notice if you're confident your issue isn't a duplicate. diff --git a/CHANGELOG.md b/CHANGELOG.md index f43858a00a5..af5f5809c41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.2.7 (2017-06-21) + +- Reinstate is_admin flag in users api when authenticated user is an admin. !12211 (rickettm) + ## 9.2.6 (2017-06-16) - Fix the last coverage in trace log should be extracted. !11128 (dosuken123) @@ -260,6 +260,7 @@ gem 'premailer-rails', '~> 1.9.0' # I18n gem 'ruby_parser', '~> 3.8', require: false +gem 'rails-i18n', '~> 4.0.9' gem 'gettext_i18n_rails', '~> 1.8.0' gem 'gettext_i18n_rails_js', '~> 1.2.0' gem 'gettext', '~> 3.2.2', require: false, group: :development diff --git a/Gemfile.lock b/Gemfile.lock index e38f8b92c8c..7ca330b6a59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -646,6 +646,9 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) + rails-i18n (4.0.9) + i18n (~> 0.7) + railties (~> 4.0) railties (4.2.8) actionpack (= 4.2.8) activesupport (= 4.2.8) @@ -1054,6 +1057,7 @@ DEPENDENCIES rack-proxy (~> 0.6.0) rails (= 4.2.8) rails-deprecated_sanitizer (~> 1.0.3) + rails-i18n (~> 4.0.9) rainbow (~> 2.2) rblineprof (~> 0.3.6) rdoc (~> 4.2) diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 86d99dd87da..2c38440a2af 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -1,29 +1,30 @@ -/* eslint-disable no-param-reassign */ - import Vue from 'vue'; -import VueResource from 'vue-resource'; -import CommitPipelinesTable from './pipelines_table'; - -Vue.use(VueResource); +import commitPipelinesTable from './pipelines_table.vue'; /** - * Commits View > Pipelines Tab > Pipelines Table. - * - * Renders Pipelines table in pipelines tab in the commits show view. + * Used in: + * - Commit details View > Pipelines Tab > Pipelines Table. + * - Merge Request details View > Pipelines Tab > Pipelines Table. + * - New Merge Request View > Pipelines Tab > Pipelines Table. */ -// export for use in merge_request_tabs.js (TODO: remove this hack) +const CommitPipelinesTable = Vue.extend(commitPipelinesTable); + +// export for use in merge_request_tabs.js (TODO: remove this hack when we understand how to load +// vue.js in merge_request_tabs.js) window.gl = window.gl || {}; window.gl.CommitPipelinesTable = CommitPipelinesTable; -$(() => { - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; - +document.addEventListener('DOMContentLoaded', () => { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { - gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); - pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); + const table = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + }, + }).$mount(); + pipelineTableViewEl.appendChild(table.$el); } }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js deleted file mode 100644 index 70ba83ce5b9..00000000000 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ /dev/null @@ -1,191 +0,0 @@ -import Vue from 'vue'; -import Visibility from 'visibilityjs'; -import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue'; -import PipelinesService from '../../pipelines/services/pipelines_service'; -import PipelineStore from '../../pipelines/stores/pipelines_store'; -import eventHub from '../../pipelines/event_hub'; -import emptyState from '../../pipelines/components/empty_state.vue'; -import errorState from '../../pipelines/components/error_state.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import '../../lib/utils/common_utils'; -import '../../vue_shared/vue_resource_interceptor'; -import Poll from '../../lib/utils/poll'; - -/** - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as `endpoint`. - * We need a store to store the received environemnts. - * We need a service to communicate with the server. - * - */ - -export default Vue.component('pipelines-table', { - - components: { - pipelinesTableComponent, - errorState, - emptyState, - loadingIcon, - }, - - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} - */ - data() { - const store = new PipelineStore(); - - return { - endpoint: null, - helpPagePath: null, - store, - state: store.state, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, - }; - }, - - computed: { - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, - - /** - * Empty state is only rendered if after the first request we receive no pipelines. - * - * @return {Boolean} - */ - shouldRenderEmptyState() { - return !this.state.pipelines.length && - !this.isLoading && - this.hasMadeRequest && - !this.hasError; - }, - - shouldRenderTable() { - return !this.isLoading && - this.state.pipelines.length > 0 && - !this.hasError; - }, - }, - - /** - * When the component is about to be mounted, tell the service to fetch the data - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - beforeMount() { - const element = document.querySelector('#commit-pipeline-table-view'); - - this.endpoint = element.dataset.endpoint; - this.helpPagePath = element.dataset.helpPagePath; - this.service = new PipelinesService(this.endpoint); - - this.poll = new Poll({ - resource: this.service, - method: 'getPipelines', - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - this.poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - - beforeDestroy() { - eventHub.$off('refreshPipelines'); - }, - - destroyed() { - this.poll.stop(); - }, - - methods: { - fetchPipelines() { - this.isLoading = true; - - return this.service.getPipelines() - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - }, - - successCallback(resp) { - const response = resp.json(); - - this.hasMadeRequest = true; - - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = response.pipelines || response; - this.store.storePipelines(pipelines); - this.isLoading = false; - this.updateGraphDropdown = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } - }, - }, - - template: ` - <div class="content-list pipelines"> - - <loading-icon - label="Loading pipelines" - size="3" - v-if="isLoading" - /> - - <empty-state - v-if="shouldRenderEmptyState" - :help-page-path="helpPagePath" /> - - <error-state v-if="shouldRenderErrorState" /> - - <div - class="table-holder" - v-if="shouldRenderTable"> - <pipelines-table-component - :pipelines="state.pipelines" - :service="service" - :update-graph-dropdown="updateGraphDropdown" - /> - </div> - </div> - `, -}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue new file mode 100644 index 00000000000..3c77f14d533 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -0,0 +1,90 @@ +<script> + import PipelinesService from '../../pipelines/services/pipelines_service'; + import PipelineStore from '../../pipelines/stores/pipelines_store'; + import pipelinesMixin from '../../pipelines/mixins/pipelines'; + + export default { + props: { + endpoint: { + type: String, + required: true, + }, + helpPagePath: { + type: String, + required: true, + }, + }, + mixins: [ + pipelinesMixin, + ], + + data() { + const store = new PipelineStore(); + + return { + store, + state: store.state, + }; + }, + + computed: { + /** + * Empty state is only rendered if after the first request we receive no pipelines. + * + * @return {Boolean} + */ + shouldRenderEmptyState() { + return !this.state.pipelines.length && + !this.isLoading && + this.hasMadeRequest && + !this.hasError; + }, + + shouldRenderTable() { + return !this.isLoading && + this.state.pipelines.length > 0 && + !this.hasError; + }, + }, + created() { + this.service = new PipelinesService(this.endpoint); + }, + methods: { + successCallback(resp) { + const response = resp.json(); + + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = response.pipelines || response; + this.setCommonData(pipelines); + }, + }, + }; +</script> +<template> + <div class="content-list pipelines"> + + <loading-icon + label="Loading pipelines" + size="3" + v-if="isLoading" + /> + + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" + /> + + <error-state + v-if="shouldRenderErrorState" + /> + + <div + class="table-holder" + v-if="shouldRenderTable"> + <pipelines-table-component + :pipelines="state.pipelines" + :update-graph-dropdown="updateGraphDropdown" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index 84bd2e092e6..a8856120c5e 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -22,6 +22,7 @@ export default class IssuableBulkUpdateSidebar { initDomElements() { this.$page = $('.page-with-sidebar'); this.$sidebar = $('.right-sidebar'); + this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar'); this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide'); this.$bulkEditSubmitBtn = $('.update-selected-issues'); this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle'); @@ -113,6 +114,7 @@ export default class IssuableBulkUpdateSidebar { toggleSidebarDisplay(show) { this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); + this.$sidebarInnerContainer.toggleClass(HIDDEN_CLASS, !show); this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show); this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show); } diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e14414d3f68..8473a81bc88 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -51,6 +51,11 @@ export default { required: false, default: '', }, + initialTaskStatus: { + type: String, + required: false, + default: '', + }, updatedAt: { type: String, required: false, @@ -105,6 +110,7 @@ export default { updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, + taskStatus: this.initialTaskStatus, }); return { diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index bb95ff0101b..43db66c8e08 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -37,18 +37,7 @@ }); }, taskStatus() { - const taskRegexMatches = this.taskStatus.match(/(\d+) of (\d+)/); - const $issuableHeader = $('.issuable-meta'); - const $tasks = $('#task_status', $issuableHeader); - const $tasksShort = $('#task_status_short', $issuableHeader); - - if (taskRegexMatches) { - $tasks.text(this.taskStatus); - $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); - } else { - $tasks.text(''); - $tasksShort.text(''); - } + this.updateTaskStatusText(); }, }, methods: { @@ -64,9 +53,24 @@ }); } }, + updateTaskStatusText() { + const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); + const $issuableHeader = $('.issuable-meta'); + const $tasks = $('#task_status', $issuableHeader); + const $tasksShort = $('#task_status_short', $issuableHeader); + + if (taskRegexMatches) { + $tasks.text(this.taskStatus); + $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`); + } else { + $tasks.text(''); + $tasksShort.text(''); + } + }, }, mounted() { this.renderGFM(); + this.updateTaskStatusText(); }, }; </script> diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 14b2a1e18e9..ad8cb6465e2 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -45,6 +45,7 @@ document.addEventListener('DOMContentLoaded', () => { updatedAt: this.updatedAt, updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, + initialTaskStatus: this.initialTaskStatus, }, }); }, diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 27c2d349f52..f2b822f3cbb 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -1,23 +1,6 @@ export default class Store { - constructor({ - titleHtml, - titleText, - descriptionHtml, - descriptionText, - updatedAt, - updatedByName, - updatedByPath, - }) { - this.state = { - titleHtml, - titleText, - descriptionHtml, - descriptionText, - taskStatus: '', - updatedAt, - updatedByName, - updatedByPath, - }; + constructor(initialState) { + this.state = initialState; this.formState = { title: '', confidential: false, diff --git a/app/assets/javascripts/locale/es/app.js b/app/assets/javascripts/locale/es/app.js index f198809cc20..eafcd15acf9 100644 --- a/app/assets/javascripts/locale/es/app.js +++ b/app/assets/javascripts/locale/es/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-06-15 21:59-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"Bob Van Landuyt <bob@gitlab.com>","X-Generator":"Poedit 2.0.2","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"%{commit_author_link} committed %{commit_timeago}":["%{commit_author_link} cambió %{commit_timeago}"],"About auto deploy":["Acerca del auto despliegue"],"Active":["Activo"],"Activity":["Actividad"],"Add Changelog":["Agregar Changelog"],"Add Contribution guide":["Agregar guía de contribución"],"Add License":["Agregar Licencia"],"Add an SSH key to your profile to pull or push via SSH.":["Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH."],"Add new directory":["Agregar nuevo directorio"],"Archived project! Repository is read-only":["¡Proyecto archivado! El repositorio es de solo lectura"],"Are you sure you want to delete this pipeline schedule?":["¿Estás seguro que deseas eliminar esta programación del pipeline?"],"Attach a file by drag & drop or %{upload_link}":["Adjunte un archivo arrastrando & soltando o %{upload_link}"],"Branch":["Rama","Ramas"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"],"Branches":["Ramas"],"Browse files":["Examinar los archivos"],"ByAuthor|by":["por"],"CI configuration":["Configuración de CI"],"Cancel":["Cancelar"],"ChangeTypeActionLabel|Pick into branch":["Escoger en la rama"],"ChangeTypeActionLabel|Revert in branch":["Revertir en la rama"],"ChangeTypeAction|Cherry-pick":["Cherry-pick"],"ChangeTypeAction|Revert":["Revertir"],"Changelog":["Changelog"],"Charts":["Gráficos"],"Cherry-pick this commit":["Escoger este cambio"],"Cherry-pick this merge request":["Escoger esta solicitud de fusión"],"CiStatusLabel|canceled":["cancelado"],"CiStatusLabel|created":["creado"],"CiStatusLabel|failed":["fallido"],"CiStatusLabel|manual action":["acción manual"],"CiStatusLabel|passed":["pasó"],"CiStatusLabel|passed with warnings":["pasó con advertencias"],"CiStatusLabel|pending":["pendiente"],"CiStatusLabel|skipped":["omitido"],"CiStatusLabel|waiting for manual action":["esperando acción manual"],"CiStatusText|blocked":["bloqueado"],"CiStatusText|canceled":["cancelado"],"CiStatusText|created":["creado"],"CiStatusText|failed":["fallado"],"CiStatusText|manual":["manual"],"CiStatusText|passed":["pasó"],"CiStatusText|pending":["pendiente"],"CiStatusText|skipped":["omitido"],"CiStatus|running":["en ejecución"],"Commit":["Cambio","Cambios"],"Commit message":["Mensaje del cambio"],"CommitBoxTitle|Commit":["Cambio"],"CommitMessage|Add %{file_name}":["Agregar %{file_name}"],"Commits":["Cambios"],"Commits|History":["Historial"],"Committed by":["Enviado por"],"Compare":["Comparar"],"Contribution guide":["Guía de contribución"],"Contributors":["Contribuidores"],"Copy URL to clipboard":["Copiar URL al portapapeles"],"Copy commit SHA to clipboard":["Copiar SHA del cambio al portapapeles"],"Create New Directory":["Crear Nuevo Directorio"],"Create directory":["Crear directorio"],"Create empty bare repository":["Crear repositorio vacío"],"Create merge request":["Crear solicitud de fusión"],"Create new...":["Crear nuevo..."],"CreateNewFork|Fork":["Bifurcar"],"CreateTag|Tag":["Etiqueta"],"Cron Timezone":["Zona horaria del Cron"],"Cron syntax":["Sintaxis de Cron"],"Custom notification events":["Eventos de notificaciones personalizadas"],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":["Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."],"Cycle Analytics":["Cycle Analytics"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Define a custom pattern with cron syntax":["Definir un patrón personalizado con la sintaxis de cron"],"Delete":["Eliminar"],"Deploy":["Despliegue","Despliegues"],"Description":["Descripción"],"Directory name":["Nombre del directorio"],"Don't show again":["No mostrar de nuevo"],"Download":["Descargar"],"Download tar":["Descargar tar"],"Download tar.bz2":["Descargar tar.bz2"],"Download tar.gz":["Descargar tar.gz"],"Download zip":["Descargar zip"],"DownloadArtifacts|Download":["Descargar"],"DownloadCommit|Email Patches":["Parches por correo electrónico"],"DownloadCommit|Plain Diff":["Diferencias en texto plano"],"DownloadSource|Download":["Descargar"],"Edit":["Editar"],"Edit Pipeline Schedule %{id}":["Editar Programación del Pipeline %{id}"],"Every day (at 4:00am)":["Todos los días (a las 4:00 am)"],"Every month (on the 1st at 4:00am)":["Todos los meses (el día 1 a las 4:00 am)"],"Every week (Sundays at 4:00am)":["Todas las semanas (domingos a las 4:00 am)"],"Failed to change the owner":["Error al cambiar el propietario"],"Failed to remove the pipeline schedule":["Error al eliminar la programación del pipeline"],"Files":["Archivos"],"Find by path":["Buscar por ruta"],"Find file":["Buscar archivo"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"Fork":["Bifurcación","Bifurcaciones"],"ForkedFromProjectPath|Forked from":["Bifurcado de"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Go to your fork":["Ir a tu bifurcación"],"GoToYourFork|Fork":["Bifurcación"],"Home":["Inicio"],"Housekeeping successfully started":["Servicio de limpieza iniciado con éxito"],"Import repository":["Importar repositorio"],"Interval Pattern":["Patrón de intervalo"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"LFSStatus|Disabled":["Deshabilitado"],"LFSStatus|Enabled":["Habilitado"],"Last %d day":["Último %d día","Últimos %d días"],"Last Pipeline":["Último Pipeline"],"Last Update":["Última actualización"],"Last commit":["Último cambio"],"Learn more in the":["Más información en la"],"Learn more in the|pipeline schedules documentation":["documentación sobre la programación de pipelines"],"Leave group":["Abandonar grupo"],"Leave project":["Abandonar proyecto"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"MissingSSHKeyWarningLink|add an SSH key":["agregar una clave SSH"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"New Pipeline Schedule":["Nueva Programación del Pipeline"],"New branch":["Nueva rama"],"New directory":["Nuevo directorio"],"New file":["Nuevo archivo"],"New issue":["Nueva incidencia"],"New merge request":["Nueva solicitud de fusión"],"New schedule":["Nueva programación"],"New snippet":["Nuevo fragmento de código"],"New tag":["Nueva etiqueta"],"No repository":["No hay repositorio"],"No schedules":["No hay programaciones"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"Notification events":["Eventos de notificación"],"NotificationEvent|Close issue":["Cerrar incidencia"],"NotificationEvent|Close merge request":["Cerrar solicitud de fusión"],"NotificationEvent|Failed pipeline":["Pipeline fallido"],"NotificationEvent|Merge merge request":["Integrar solicitud de fusión"],"NotificationEvent|New issue":["Nueva incidencia"],"NotificationEvent|New merge request":["Nueva solicitud de fusión"],"NotificationEvent|New note":["Nueva nota"],"NotificationEvent|Reassign issue":["Reasignar incidencia"],"NotificationEvent|Reassign merge request":["Reasignar solicitud de fusión"],"NotificationEvent|Reopen issue":["Reabrir incidencia"],"NotificationEvent|Successful pipeline":["Pipeline exitoso"],"NotificationLevel|Custom":["Personalizado"],"NotificationLevel|Disabled":["Deshabilitado"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["Cuando me mencionan"],"NotificationLevel|Participate":["Participación"],"NotificationLevel|Watch":["Vigilancia"],"OfSearchInADropdown|Filter":["Filtrar"],"OpenedNDaysAgo|Opened":["Abierto"],"Options":["Opciones"],"Owner":["Propietario"],"Pipeline":["Pipeline"],"Pipeline Health":["Estado del Pipeline"],"Pipeline Schedule":["Programación del Pipeline"],"Pipeline Schedules":["Programaciones de los Pipelines"],"PipelineSchedules|Activated":["Activado"],"PipelineSchedules|Active":["Activos"],"PipelineSchedules|All":["Todos"],"PipelineSchedules|Inactive":["Inactivos"],"PipelineSchedules|Next Run":["Próxima Ejecución"],"PipelineSchedules|None":["Ninguno"],"PipelineSchedules|Provide a short description for this pipeline":["Proporcione una breve descripción para este pipeline"],"PipelineSchedules|Take ownership":["Tomar posesión"],"PipelineSchedules|Target":["Destino"],"PipelineSheduleIntervalPattern|Custom":["Personalizado"],"Pipeline|with stage":["con etapa"],"Pipeline|with stages":["con etapas"],"Project '%{project_name}' queued for deletion.":["Proyecto ‘%{project_name}’ en cola para eliminación."],"Project '%{project_name}' was successfully created.":["Proyecto ‘%{project_name}’ fue creado satisfactoriamente."],"Project '%{project_name}' was successfully updated.":["Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."],"Project '%{project_name}' will be deleted.":["Proyecto ‘%{project_name}’ será eliminado."],"Project access must be granted explicitly to each user.":["El acceso al proyecto debe concederse explícitamente a cada usuario."],"Project export could not be deleted.":["No se pudo eliminar la exportación del proyecto."],"Project export has been deleted.":["La exportación del proyecto ha sido eliminada."],"Project export link has expired. Please generate a new export from your project settings.":["El enlace de exportación del proyecto ha caducado. Por favor, genera una nueva exportación desde la configuración del proyecto."],"Project export started. A download link will be sent by email.":["Se inició la exportación del proyecto. Se enviará un enlace de descarga por correo electrónico."],"Project home":["Inicio del proyecto"],"ProjectFeature|Disabled":["Deshabilitada"],"ProjectFeature|Everyone with access":["Todos con acceso"],"ProjectFeature|Only team members":["Solo miembros del equipo"],"ProjectFileTree|Name":["Nombre"],"ProjectLastActivity|Never":["Nunca"],"ProjectLifecycle|Stage":["Etapa"],"ProjectNetworkGraph|Graph":["Historial gráfico"],"Read more":["Leer más"],"Readme":["Léeme"],"RefSwitcher|Branches":["Ramas"],"RefSwitcher|Tags":["Etiquetas"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Remind later":["Recordar después"],"Remove project":["Eliminar proyecto"],"Request Access":["Solicitar acceso"],"Revert this commit":["Revertir este cambio"],"Revert this merge request":["Revertir esta solicitud de fusión"],"Save pipeline schedule":["Guardar programación del pipeline"],"Schedule a new pipeline":["Programar un nuevo pipeline"],"Scheduling Pipelines":["Programación de Pipelines"],"Search branches and tags":["Buscar ramas y etiquetas"],"Select Archive Format":["Seleccionar formato de archivo"],"Select a timezone":["Selecciona una zona horaria"],"Select target branch":["Selecciona una rama de destino"],"Set a password on your account to pull or push via %{protocol}":["Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}"],"Set up CI":["Configurar CI"],"Set up Koding":["Configurar Koding"],"Set up auto deploy":["Configurar auto despliegue"],"SetPasswordToCloneLink|set a password":["establecer una contraseña"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"Source code":["Código fuente"],"StarProject|Star":["Destacar"],"Start a %{new_merge_request} with these changes":["Iniciar una %{new_merge_request} con estos cambios"],"Switch branch/tag":["Cambiar rama/etiqueta"],"Tag":["Etiqueta","Etiquetas"],"Tags":["Etiquetas"],"Target Branch":["Rama de destino"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The fork relationship has been removed.":["La relación con la bifurcación se ha eliminado."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":["La programación de pipelines ejecuta pipelines en el futuro, repetidamente, para ramas o etiquetas específicas. Los pipelines programados heredarán acceso limitado al proyecto basado en su usuario asociado."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The project can be accessed by any logged in user.":["El proyecto puede ser accedido por cualquier usuario conectado."],"The project can be accessed without any authentication.":["El proyecto puede accederse sin ninguna autenticación."],"The repository for this project does not exist.":["El repositorio para este proyecto no existe."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"This means you can not push code until you create an empty repository or import existing one.":["Esto significa que no puede enviar código hasta que cree un repositorio vacío o importe uno existente."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Timeago|%s days ago":["hace %s días"],"Timeago|%s days remaining":["%s días restantes"],"Timeago|%s hours remaining":["%s horas restantes"],"Timeago|%s minutes ago":["hace %s minutos"],"Timeago|%s minutes remaining":["%s minutos restantes"],"Timeago|%s months ago":["hace %s meses"],"Timeago|%s months remaining":["%s meses restantes"],"Timeago|%s seconds remaining":["%s segundos restantes"],"Timeago|%s weeks ago":["hace %s semanas"],"Timeago|%s weeks remaining":["%s semanas restantes"],"Timeago|%s years ago":["hace %s años"],"Timeago|%s years remaining":["%s años restantes"],"Timeago|1 day remaining":["1 día restante"],"Timeago|1 hour remaining":["1 hora restante"],"Timeago|1 minute remaining":["1 minuto restante"],"Timeago|1 month remaining":["1 mes restante"],"Timeago|1 week remaining":["1 semana restante"],"Timeago|1 year remaining":["1 año restante"],"Timeago|Past due":["Atrasado"],"Timeago|a day ago":["hace un día"],"Timeago|a month ago":["hace un mes"],"Timeago|a week ago":["hace una semana"],"Timeago|a while":["hace un momento"],"Timeago|a year ago":["hace un año"],"Timeago|about %s hours ago":["hace alrededor de %s horas"],"Timeago|about a minute ago":["hace alrededor de 1 minuto"],"Timeago|about an hour ago":["hace alrededor de 1 hora"],"Timeago|in %s days":["en %s días"],"Timeago|in %s hours":["en %s horas"],"Timeago|in %s minutes":["en %s minutos"],"Timeago|in %s months":["en %s meses"],"Timeago|in %s seconds":["en %s segundos"],"Timeago|in %s weeks":["en %s semanas"],"Timeago|in %s years":["en %s años"],"Timeago|in 1 day":["en 1 día"],"Timeago|in 1 hour":["en 1 hora"],"Timeago|in 1 minute":["en 1 minuto"],"Timeago|in 1 month":["en 1 mes"],"Timeago|in 1 week":["en 1 semana"],"Timeago|in 1 year":["en 1 año"],"Timeago|less than a minute ago":["hace menos de 1 minuto"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Unstar":["No Destacar"],"Upload New File":["Subir nuevo archivo"],"Upload file":["Subir archivo"],"Use your global notification setting":["Utiliza tu configuración de notificación global"],"VisibilityLevel|Internal":["Interno"],"VisibilityLevel|Private":["Privado"],"VisibilityLevel|Public":["Público"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"Withdraw Access Request":["Retirar Solicitud de Acceso"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["Va a eliminar %{project_name_with_namespace}.\\n¡El proyecto eliminado NO puede ser restaurado!\\n¿Estás TOTALMENTE seguro?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"],"You can only add files when you are on a branch":["Solo puedes agregar archivos cuando estás en una rama"],"You must sign in to star a project":["Debes iniciar sesión para destacar un proyecto"],"You need permission.":["Necesitas permisos."],"You will not get any notifications via email":["No recibirás ninguna notificación por correo electrónico"],"You will only receive notifications for the events you choose":["Solo recibirás notificaciones de los eventos que elijas"],"You will only receive notifications for threads you have participated in":["Solo recibirás notificaciones de los temas en los que has participado"],"You will receive notifications for any activity":["Recibirás notificaciones por cualquier actividad"],"You will receive notifications only for comments in which you were @mentioned":["Recibirás notificaciones solo para los comentarios en los que se te mencionó"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil"],"Your name":["Tu nombre"],"day":["día","días"],"new merge request":["nueva solicitud de fusión"],"notification emails":["correos electrónicos de notificación"],"parent":["padre","padres"]}}};
\ No newline at end of file +var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-06-15 21:59-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"Bob Van Landuyt <bob@gitlab.com>","X-Generator":"Poedit 2.0.2","POT-Creation-Date":"2017-06-15 21:59-0500","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"%{commit_author_link} committed %{commit_timeago}":["%{commit_author_link} cambió %{commit_timeago}"],"About auto deploy":["Acerca del auto despliegue"],"Active":["Activo"],"Activity":["Actividad"],"Add Changelog":["Agregar Changelog"],"Add Contribution guide":["Agregar guía de contribución"],"Add License":["Agregar Licencia"],"Add an SSH key to your profile to pull or push via SSH.":["Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH."],"Add new directory":["Agregar nuevo directorio"],"Archived project! Repository is read-only":["¡Proyecto archivado! El repositorio es de solo lectura"],"Are you sure you want to delete this pipeline schedule?":["¿Estás seguro que deseas eliminar esta programación del pipeline?"],"Attach a file by drag & drop or %{upload_link}":["Adjunte un archivo arrastrando & soltando o %{upload_link}"],"Branch":["Rama","Ramas"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"],"Branches":["Ramas"],"Browse files":["Examinar los archivos"],"ByAuthor|by":["por"],"CI configuration":["Configuración de CI"],"Cancel":["Cancelar"],"ChangeTypeActionLabel|Pick into branch":["Escoger en la rama"],"ChangeTypeActionLabel|Revert in branch":["Revertir en la rama"],"ChangeTypeAction|Cherry-pick":["Cherry-pick"],"ChangeTypeAction|Revert":["Revertir"],"Changelog":["Changelog"],"Charts":["Gráficos"],"Cherry-pick this commit":["Escoger este cambio"],"Cherry-pick this merge request":["Escoger esta solicitud de fusión"],"CiStatusLabel|canceled":["cancelado"],"CiStatusLabel|created":["creado"],"CiStatusLabel|failed":["fallido"],"CiStatusLabel|manual action":["acción manual"],"CiStatusLabel|passed":["pasó"],"CiStatusLabel|passed with warnings":["pasó con advertencias"],"CiStatusLabel|pending":["pendiente"],"CiStatusLabel|skipped":["omitido"],"CiStatusLabel|waiting for manual action":["esperando acción manual"],"CiStatusText|blocked":["bloqueado"],"CiStatusText|canceled":["cancelado"],"CiStatusText|created":["creado"],"CiStatusText|failed":["fallado"],"CiStatusText|manual":["manual"],"CiStatusText|passed":["pasó"],"CiStatusText|pending":["pendiente"],"CiStatusText|skipped":["omitido"],"CiStatus|running":["en ejecución"],"Commit":["Cambio","Cambios"],"Commit message":["Mensaje del cambio"],"CommitBoxTitle|Commit":["Cambio"],"CommitMessage|Add %{file_name}":["Agregar %{file_name}"],"Commits":["Cambios"],"Commits|History":["Historial"],"Committed by":["Enviado por"],"Compare":["Comparar"],"Contribution guide":["Guía de contribución"],"Contributors":["Contribuidores"],"Copy URL to clipboard":["Copiar URL al portapapeles"],"Copy commit SHA to clipboard":["Copiar SHA del cambio al portapapeles"],"Create New Directory":["Crear Nuevo Directorio"],"Create directory":["Crear directorio"],"Create empty bare repository":["Crear repositorio vacío"],"Create merge request":["Crear solicitud de fusión"],"Create new...":["Crear nuevo..."],"CreateNewFork|Fork":["Bifurcar"],"CreateTag|Tag":["Etiqueta"],"Cron Timezone":["Zona horaria del Cron"],"Cron syntax":["Sintaxis de Cron"],"Custom notification events":["Eventos de notificaciones personalizadas"],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":["Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."],"Cycle Analytics":["Cycle Analytics"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Define a custom pattern with cron syntax":["Definir un patrón personalizado con la sintaxis de cron"],"Delete":["Eliminar"],"Deploy":["Despliegue","Despliegues"],"Description":["Descripción"],"Directory name":["Nombre del directorio"],"Don't show again":["No mostrar de nuevo"],"Download":["Descargar"],"Download tar":["Descargar tar"],"Download tar.bz2":["Descargar tar.bz2"],"Download tar.gz":["Descargar tar.gz"],"Download zip":["Descargar zip"],"DownloadArtifacts|Download":["Descargar"],"DownloadCommit|Email Patches":["Parches por correo electrónico"],"DownloadCommit|Plain Diff":["Diferencias en texto plano"],"DownloadSource|Download":["Descargar"],"Edit":["Editar"],"Edit Pipeline Schedule %{id}":["Editar Programación del Pipeline %{id}"],"Every day (at 4:00am)":["Todos los días (a las 4:00 am)"],"Every month (on the 1st at 4:00am)":["Todos los meses (el día 1 a las 4:00 am)"],"Every week (Sundays at 4:00am)":["Todas las semanas (domingos a las 4:00 am)"],"Failed to change the owner":["Error al cambiar el propietario"],"Failed to remove the pipeline schedule":["Error al eliminar la programación del pipeline"],"Files":["Archivos"],"Find by path":["Buscar por ruta"],"Find file":["Buscar archivo"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"Fork":["Bifurcación","Bifurcaciones"],"ForkedFromProjectPath|Forked from":["Bifurcado de"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Go to your fork":["Ir a tu bifurcación"],"GoToYourFork|Fork":["Bifurcación"],"Home":["Inicio"],"Housekeeping successfully started":["Servicio de limpieza iniciado con éxito"],"Import repository":["Importar repositorio"],"Interval Pattern":["Patrón de intervalo"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"LFSStatus|Disabled":["Deshabilitado"],"LFSStatus|Enabled":["Habilitado"],"Last %d day":["Último %d día","Últimos %d días"],"Last Pipeline":["Último Pipeline"],"Last Update":["Última actualización"],"Last commit":["Último cambio"],"Learn more in the":["Más información en la"],"Learn more in the|pipeline schedules documentation":["documentación sobre la programación de pipelines"],"Leave group":["Abandonar grupo"],"Leave project":["Abandonar proyecto"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"MissingSSHKeyWarningLink|add an SSH key":["agregar una clave SSH"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"New Pipeline Schedule":["Nueva Programación del Pipeline"],"New branch":["Nueva rama"],"New directory":["Nuevo directorio"],"New file":["Nuevo archivo"],"New issue":["Nueva incidencia"],"New merge request":["Nueva solicitud de fusión"],"New schedule":["Nueva programación"],"New snippet":["Nuevo fragmento de código"],"New tag":["Nueva etiqueta"],"No repository":["No hay repositorio"],"No schedules":["No hay programaciones"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"Notification events":["Eventos de notificación"],"NotificationEvent|Close issue":["Cerrar incidencia"],"NotificationEvent|Close merge request":["Cerrar solicitud de fusión"],"NotificationEvent|Failed pipeline":["Pipeline fallido"],"NotificationEvent|Merge merge request":["Integrar solicitud de fusión"],"NotificationEvent|New issue":["Nueva incidencia"],"NotificationEvent|New merge request":["Nueva solicitud de fusión"],"NotificationEvent|New note":["Nueva nota"],"NotificationEvent|Reassign issue":["Reasignar incidencia"],"NotificationEvent|Reassign merge request":["Reasignar solicitud de fusión"],"NotificationEvent|Reopen issue":["Reabrir incidencia"],"NotificationEvent|Successful pipeline":["Pipeline exitoso"],"NotificationLevel|Custom":["Personalizado"],"NotificationLevel|Disabled":["Deshabilitado"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["Cuando me mencionan"],"NotificationLevel|Participate":["Participación"],"NotificationLevel|Watch":["Vigilancia"],"OfSearchInADropdown|Filter":["Filtrar"],"OpenedNDaysAgo|Opened":["Abierto"],"Options":["Opciones"],"Owner":["Propietario"],"Pipeline":["Pipeline"],"Pipeline Health":["Estado del Pipeline"],"Pipeline Schedule":["Programación del Pipeline"],"Pipeline Schedules":["Programaciones de los Pipelines"],"PipelineSchedules|Activated":["Activado"],"PipelineSchedules|Active":["Activos"],"PipelineSchedules|All":["Todos"],"PipelineSchedules|Inactive":["Inactivos"],"PipelineSchedules|Next Run":["Próxima Ejecución"],"PipelineSchedules|None":["Ninguno"],"PipelineSchedules|Provide a short description for this pipeline":["Proporcione una breve descripción para este pipeline"],"PipelineSchedules|Take ownership":["Tomar posesión"],"PipelineSchedules|Target":["Destino"],"PipelineSheduleIntervalPattern|Custom":["Personalizado"],"Pipeline|with stage":["con etapa"],"Pipeline|with stages":["con etapas"],"Project '%{project_name}' queued for deletion.":["Proyecto ‘%{project_name}’ en cola para eliminación."],"Project '%{project_name}' was successfully created.":["Proyecto ‘%{project_name}’ fue creado satisfactoriamente."],"Project '%{project_name}' was successfully updated.":["Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."],"Project '%{project_name}' will be deleted.":["Proyecto ‘%{project_name}’ será eliminado."],"Project access must be granted explicitly to each user.":["El acceso al proyecto debe concederse explícitamente a cada usuario."],"Project export could not be deleted.":["No se pudo eliminar la exportación del proyecto."],"Project export has been deleted.":["La exportación del proyecto ha sido eliminada."],"Project export link has expired. Please generate a new export from your project settings.":["El enlace de exportación del proyecto ha caducado. Por favor, genera una nueva exportación desde la configuración del proyecto."],"Project export started. A download link will be sent by email.":["Se inició la exportación del proyecto. Se enviará un enlace de descarga por correo electrónico."],"Project home":["Inicio del proyecto"],"ProjectFeature|Disabled":["Deshabilitada"],"ProjectFeature|Everyone with access":["Todos con acceso"],"ProjectFeature|Only team members":["Solo miembros del equipo"],"ProjectFileTree|Name":["Nombre"],"ProjectLastActivity|Never":["Nunca"],"ProjectLifecycle|Stage":["Etapa"],"ProjectNetworkGraph|Graph":["Historial gráfico"],"Read more":["Leer más"],"Readme":["Léeme"],"RefSwitcher|Branches":["Ramas"],"RefSwitcher|Tags":["Etiquetas"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Remind later":["Recordar después"],"Remove project":["Eliminar proyecto"],"Request Access":["Solicitar acceso"],"Revert this commit":["Revertir este cambio"],"Revert this merge request":["Revertir esta solicitud de fusión"],"Save pipeline schedule":["Guardar programación del pipeline"],"Schedule a new pipeline":["Programar un nuevo pipeline"],"Scheduling Pipelines":["Programación de Pipelines"],"Search branches and tags":["Buscar ramas y etiquetas"],"Select Archive Format":["Seleccionar formato de archivo"],"Select a timezone":["Selecciona una zona horaria"],"Select target branch":["Selecciona una rama de destino"],"Set a password on your account to pull or push via %{protocol}":["Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}"],"Set up CI":["Configurar CI"],"Set up Koding":["Configurar Koding"],"Set up auto deploy":["Configurar auto despliegue"],"SetPasswordToCloneLink|set a password":["establecer una contraseña"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"Source code":["Código fuente"],"StarProject|Star":["Destacar"],"Start a %{new_merge_request} with these changes":["Iniciar una %{new_merge_request} con estos cambios"],"Switch branch/tag":["Cambiar rama/etiqueta"],"Tag":["Etiqueta","Etiquetas"],"Tags":["Etiquetas"],"Target Branch":["Rama de destino"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The fork relationship has been removed.":["La relación con la bifurcación se ha eliminado."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":["La programación de pipelines ejecuta pipelines en el futuro, repetidamente, para ramas o etiquetas específicas. Los pipelines programados heredarán acceso limitado al proyecto basado en su usuario asociado."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The project can be accessed by any logged in user.":["El proyecto puede ser accedido por cualquier usuario conectado."],"The project can be accessed without any authentication.":["El proyecto puede accederse sin ninguna autenticación."],"The repository for this project does not exist.":["El repositorio para este proyecto no existe."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"This means you can not push code until you create an empty repository or import existing one.":["Esto significa que no puede enviar código hasta que cree un repositorio vacío o importe uno existente."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Timeago|%s days ago":["hace %s días"],"Timeago|%s days remaining":["%s días restantes"],"Timeago|%s hours remaining":["%s horas restantes"],"Timeago|%s minutes ago":["hace %s minutos"],"Timeago|%s minutes remaining":["%s minutos restantes"],"Timeago|%s months ago":["hace %s meses"],"Timeago|%s months remaining":["%s meses restantes"],"Timeago|%s seconds remaining":["%s segundos restantes"],"Timeago|%s weeks ago":["hace %s semanas"],"Timeago|%s weeks remaining":["%s semanas restantes"],"Timeago|%s years ago":["hace %s años"],"Timeago|%s years remaining":["%s años restantes"],"Timeago|1 day remaining":["1 día restante"],"Timeago|1 hour remaining":["1 hora restante"],"Timeago|1 minute remaining":["1 minuto restante"],"Timeago|1 month remaining":["1 mes restante"],"Timeago|1 week remaining":["1 semana restante"],"Timeago|1 year remaining":["1 año restante"],"Timeago|Past due":["Atrasado"],"Timeago|a day ago":["hace un día"],"Timeago|a month ago":["hace un mes"],"Timeago|a week ago":["hace una semana"],"Timeago|a while":["hace un momento"],"Timeago|a year ago":["hace un año"],"Timeago|about %s hours ago":["hace alrededor de %s horas"],"Timeago|about a minute ago":["hace alrededor de 1 minuto"],"Timeago|about an hour ago":["hace alrededor de 1 hora"],"Timeago|in %s days":["en %s días"],"Timeago|in %s hours":["en %s horas"],"Timeago|in %s minutes":["en %s minutos"],"Timeago|in %s months":["en %s meses"],"Timeago|in %s seconds":["en %s segundos"],"Timeago|in %s weeks":["en %s semanas"],"Timeago|in %s years":["en %s años"],"Timeago|in 1 day":["en 1 día"],"Timeago|in 1 hour":["en 1 hora"],"Timeago|in 1 minute":["en 1 minuto"],"Timeago|in 1 month":["en 1 mes"],"Timeago|in 1 week":["en 1 semana"],"Timeago|in 1 year":["en 1 año"],"Timeago|less than a minute ago":["hace menos de 1 minuto"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Unstar":["No Destacar"],"Upload New File":["Subir nuevo archivo"],"Upload file":["Subir archivo"],"Use your global notification setting":["Utiliza tu configuración de notificación global"],"VisibilityLevel|Internal":["Interno"],"VisibilityLevel|Private":["Privado"],"VisibilityLevel|Public":["Público"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"Withdraw Access Request":["Retirar Solicitud de Acceso"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["Va a eliminar %{project_name_with_namespace}.\\n¡El proyecto eliminado NO puede ser restaurado!\\n¿Estás TOTALMENTE seguro?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"],"You can only add files when you are on a branch":["Solo puedes agregar archivos cuando estás en una rama"],"You must sign in to star a project":["Debes iniciar sesión para destacar un proyecto"],"You need permission.":["Necesitas permisos."],"You will not get any notifications via email":["No recibirás ninguna notificación por correo electrónico"],"You will only receive notifications for the events you choose":["Solo recibirás notificaciones de los eventos que elijas"],"You will only receive notifications for threads you have participated in":["Solo recibirás notificaciones de los temas en los que has participado"],"You will receive notifications for any activity":["Recibirás notificaciones por cualquier actividad"],"You will receive notifications only for comments in which you were @mentioned":["Recibirás notificaciones solo para los comentarios en los que se te mencionó"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil"],"Your name":["Tu nombre"],"day":["día","días"],"new merge request":["nueva solicitud de fusión"],"notification emails":["correos electrónicos de notificación"],"parent":["padre","padres"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 894ed81b044..7bb2236017e 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -233,11 +233,18 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; } mountPipelinesView() { - this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount(); + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + const CommitPipelinesTable = gl.CommitPipelinesTable; + this.commitPipelinesTable = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + }, + }).$mount(); + // $mount(el) replaces the el with the new rendered component. We need it in order to mount // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount - document.querySelector('#commit-pipeline-table-view') - .appendChild(this.commitPipelinesTable.$el); + pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el); } loadDiff(source) { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 6a6dabfd00c..624dd336786 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -187,7 +187,7 @@ const normalizeNewlines = function(str) { if ($textarea.val() !== '') { return; } - myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, #notes')); + myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes')); if (myLastNote.length) { myLastNoteEditBtn = myLastNote.find('.js-note-edit'); return myLastNoteEditBtn.trigger('click', [true, myLastNote]); diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index 37a6f02d8fd..abcd0c4ecea 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -1,9 +1,9 @@ <script> /* eslint-disable no-new, no-alert */ -/* global Flash */ -import '~/flash'; + import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltipMixin from '../../vue_shared/mixins/tooltip'; export default { props: { @@ -11,53 +11,42 @@ export default { type: String, required: true, }, - - service: { - type: Object, - required: true, - }, - title: { type: String, required: true, }, - icon: { type: String, required: true, }, - cssClass: { type: String, required: true, }, - confirmActionMessage: { type: String, required: false, }, }, - components: { loadingIcon, }, - + mixins: [ + tooltipMixin, + ], data() { return { isLoading: false, }; }, - computed: { iconClass() { return `fa fa-${this.icon}`; }, - buttonClass() { - return `btn has-tooltip ${this.cssClass}`; + return `btn ${this.cssClass}`; }, }, - methods: { onClick() { if (this.confirmActionMessage && confirm(this.confirmActionMessage)) { @@ -66,21 +55,11 @@ export default { this.makeRequest(); } }, - makeRequest() { this.isLoading = true; - $(this.$el).tooltip('destroy'); - - this.service.postAction(this.endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - new Flash('An error occured while making the request.'); - }); + $(this.$refs.tooltip).tooltip('destroy'); + eventHub.$emit('postAction', this.endpoint); }, }, }; @@ -95,10 +74,12 @@ export default { :aria-label="title" data-container="body" data-placement="top" + ref="tooltip" :disabled="isLoading"> <i :class="iconClass" - aria-hidden="true" /> + aria-hidden="true"> + </i> <loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index fed42d23112..01ae07aad65 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -1,15 +1,9 @@ <script> - import Visibility from 'visibilityjs'; import PipelinesService from '../services/pipelines_service'; - import eventHub from '../event_hub'; - import pipelinesTableComponent from '../../vue_shared/components/pipelines_table.vue'; + import pipelinesMixin from '../mixins/pipelines'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import emptyState from './empty_state.vue'; - import errorState from './error_state.vue'; import navigationTabs from './navigation_tabs.vue'; import navigationControls from './nav_controls.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import Poll from '../../lib/utils/poll'; export default { props: { @@ -20,13 +14,12 @@ }, components: { tablePagination, - pipelinesTableComponent, - emptyState, - errorState, navigationTabs, navigationControls, - loadingIcon, }, + mixins: [ + pipelinesMixin, + ], data() { const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; @@ -47,11 +40,6 @@ state: this.store.state, apiScope: 'all', pagenum: 1, - isLoading: false, - hasError: false, - isMakingRequest: false, - updateGraphDropdown: false, - hasMadeRequest: false, }; }, computed: { @@ -62,9 +50,6 @@ const scope = gl.utils.getParameterByName('scope'); return scope === null ? 'all' : scope; }, - shouldRenderErrorState() { - return this.hasError && !this.isLoading; - }, /** * The empty state should only be rendered when the request is made to fetch all pipelines @@ -106,7 +91,6 @@ this.state.pipelines.length && this.state.pageInfo.total > this.state.pageInfo.perPage; }, - hasCiEnabled() { return this.hasCi !== undefined; }, @@ -129,37 +113,7 @@ }, created() { this.service = new PipelinesService(this.endpoint); - - const poll = new Poll({ - resource: this.service, - method: 'getPipelines', - data: { page: this.pageParameter, scope: this.scopeParameter }, - successCallback: this.successCallback, - errorCallback: this.errorCallback, - notificationCallback: this.setIsMakingRequest, - }); - - if (!Visibility.hidden()) { - this.isLoading = true; - poll.makeRequest(); - } else { - // If tab is not visible we need to make the first request so we don't show the empty - // state without knowing if there are any pipelines - this.fetchPipelines(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - poll.restart(); - } else { - poll.stop(); - } - }); - - eventHub.$on('refreshPipelines', this.fetchPipelines); - }, - beforeDestroy() { - eventHub.$off('refreshPipelines'); + this.requestData = { page: this.pageParameter, scope: this.scopeParameter }; }, methods: { /** @@ -174,15 +128,6 @@ return param; }, - fetchPipelines() { - if (!this.isMakingRequest) { - this.isLoading = true; - - this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter }) - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - } - }, successCallback(resp) { const response = { headers: resp.headers, @@ -190,33 +135,14 @@ }; this.store.storeCount(response.body.count); - this.store.storePipelines(response.body.pipelines); this.store.storePagination(response.headers); - - this.isLoading = false; - this.updateGraphDropdown = true; - this.hasMadeRequest = true; - }, - - errorCallback() { - this.hasError = true; - this.isLoading = false; - this.updateGraphDropdown = false; - }, - - setIsMakingRequest(isMakingRequest) { - this.isMakingRequest = isMakingRequest; - - if (isMakingRequest) { - this.updateGraphDropdown = false; - } + this.setCommonData(response.body.pipelines); }, }, }; </script> <template> <div :class="cssClass"> - <div class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="!isLoading && !shouldRenderEmptyState"> @@ -274,7 +200,6 @@ <pipelines-table-component :pipelines="state.pipelines" - :service="service" :update-graph-dropdown="updateGraphDropdown" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 97b4de26214..a6fc4f04237 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -11,10 +11,6 @@ type: Array, required: true, }, - service: { - type: Object, - required: true, - }, }, components: { loadingIcon, @@ -31,17 +27,9 @@ $(this.$refs.tooltip).tooltip('destroy'); - this.service.postAction(endpoint) - .then(() => { - this.isLoading = false; - eventHub.$emit('refreshPipelines'); - }) - .catch(() => { - this.isLoading = false; - // eslint-disable-next-line no-new - new Flash('An error occured while making the request.'); - }); + eventHub.$emit('postAction', endpoint); }, + isActionDisabled(action) { if (action.playable === undefined) { return false; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 884f1ce9689..5088d92209f 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -12,10 +12,6 @@ type: Array, required: true, }, - service: { - type: Object, - required: true, - }, updateGraphDropdown: { type: Boolean, required: false, @@ -57,7 +53,6 @@ v-for="model in pipelines" :key="model.id" :pipeline="model" - :service="service" :update-graph-dropdown="updateGraphDropdown" /> </div> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 4d5ebe2e9ed..c3f1c426d8a 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,13 +1,13 @@ <script> /* eslint-disable no-param-reassign */ -import asyncButtonComponent from '../../pipelines/components/async_button.vue'; -import pipelinesActionsComponent from '../../pipelines/components/pipelines_actions.vue'; -import pipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts.vue'; -import ciBadge from './ci_badge_link.vue'; -import pipelineStage from '../../pipelines/components/stage.vue'; -import pipelineUrl from '../../pipelines/components/pipeline_url.vue'; -import pipelinesTimeago from '../../pipelines/components/time_ago.vue'; -import commitComponent from './commit.vue'; +import asyncButtonComponent from './async_button.vue'; +import pipelinesActionsComponent from './pipelines_actions.vue'; +import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; +import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; +import pipelineStage from './stage.vue'; +import pipelineUrl from './pipeline_url.vue'; +import pipelinesTimeago from './time_ago.vue'; +import commitComponent from '../../vue_shared/components/commit.vue'; /** * Pipeline table row. @@ -20,10 +20,6 @@ export default { type: Object, required: true, }, - service: { - type: Object, - required: true, - }, updateGraphDropdown: { type: Boolean, required: false, @@ -271,7 +267,6 @@ export default { <pipelines-actions-component v-if="pipeline.details.manual_actions.length" :actions="pipeline.details.manual_actions" - :service="service" /> <pipelines-artifacts-component @@ -282,7 +277,6 @@ export default { <async-button-component v-if="pipeline.flags.retryable" - :service="service" :endpoint="pipeline.retry_path" css-class="js-pipelines-retry-button btn-default btn-retry" title="Retry" @@ -291,7 +285,6 @@ export default { <async-button-component v-if="pipeline.flags.cancelable" - :service="service" :endpoint="pipeline.cancel_path" css-class="js-pipelines-cancel-button btn-remove" title="Cancel" diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js new file mode 100644 index 00000000000..9adc15e6266 --- /dev/null +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -0,0 +1,103 @@ +/* global Flash */ +import '~/flash'; +import Visibility from 'visibilityjs'; +import Poll from '../../lib/utils/poll'; +import emptyState from '../components/empty_state.vue'; +import errorState from '../components/error_state.vue'; +import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import pipelinesTableComponent from '../components/pipelines_table.vue'; +import eventHub from '../event_hub'; + +export default { + components: { + pipelinesTableComponent, + errorState, + emptyState, + loadingIcon, + }, + computed: { + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + }, + data() { + return { + isLoading: false, + hasError: false, + isMakingRequest: false, + updateGraphDropdown: false, + hasMadeRequest: false, + }; + }, + beforeMount() { + this.poll = new Poll({ + resource: this.service, + method: 'getPipelines', + data: this.requestData ? this.requestData : undefined, + successCallback: this.successCallback, + errorCallback: this.errorCallback, + notificationCallback: this.setIsMakingRequest, + }); + + if (!Visibility.hidden()) { + this.isLoading = true; + this.poll.makeRequest(); + } else { + // If tab is not visible we need to make the first request so we don't show the empty + // state without knowing if there are any pipelines + this.fetchPipelines(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + + eventHub.$on('refreshPipelines', this.fetchPipelines); + eventHub.$on('postAction', this.postAction); + }, + beforeDestroy() { + eventHub.$off('refreshPipelines'); + eventHub.$on('postAction', this.postAction); + }, + destroyed() { + this.poll.stop(); + }, + methods: { + fetchPipelines() { + if (!this.isMakingRequest) { + this.isLoading = true; + + this.service.getPipelines(this.requestData) + .then(response => this.successCallback(response)) + .catch(() => this.errorCallback()); + } + }, + setCommonData(pipelines) { + this.store.storePipelines(pipelines); + this.isLoading = false; + this.updateGraphDropdown = true; + this.hasMadeRequest = true; + }, + errorCallback() { + this.hasError = true; + this.isLoading = false; + this.updateGraphDropdown = false; + }, + setIsMakingRequest(isMakingRequest) { + this.isMakingRequest = isMakingRequest; + + if (isMakingRequest) { + this.updateGraphDropdown = false; + } + }, + postAction(endpoint) { + this.service.postAction(endpoint) + .then(() => eventHub.$emit('refreshPipelines')) + .catch(() => new Flash('An error occured while making the request.')); + }, + }, +}; diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b71c3097706..da7c0c5a36c 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -7,6 +7,13 @@ import Cookies from 'js-cookie'; function Sidebar(currentUser) { this.toggleTodo = this.toggleTodo.bind(this); this.sidebar = $('aside'); + + this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); + this.$navGitlab = $('.navbar-gitlab'); + this.$layoutNav = $('.layout-nav'); + this.$subScroll = $('.sub-nav-scroll'); + this.$rightSidebar = $('.js-right-sidebar'); + this.removeListeners(); this.addEventListeners(); } @@ -21,14 +28,15 @@ import Cookies from 'js-cookie'; Sidebar.prototype.addEventListeners = function() { const $document = $(document); - const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10); + const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); + const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $(window).on('resize', () => throttledSetSidebarHeight()); - $document.on('scroll', () => throttledSetSidebarHeight()); + $document.on('scroll', () => debouncedSetSidebarHeight()); $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); @@ -207,13 +215,14 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight(); - const $rightSidebar = $('.js-right-sidebar'); + const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0); const diff = $navHeight - $(window).scrollTop(); if (diff > 0) { - $rightSidebar.outerHeight($(window).height() - diff); + this.$rightSidebar.outerHeight($(window).height() - diff); + this.$sidebarInner.height('100%'); } else { - $rightSidebar.outerHeight('100%'); + this.$rightSidebar.outerHeight('100%'); + this.$sidebarInner.height(''); } }; diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index e67f449e1a2..59ff2a86293 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -1,11 +1,28 @@ +function expandSectionParent($section, $content) { + $section.addClass('expanded'); + $content.off('animationend.expandSectionParent'); +} + function expandSection($section) { $section.find('.js-settings-toggle').text('Close'); - $section.find('.settings-content').addClass('expanded').off('scroll').scrollTop(0); + + const $content = $section.find('.settings-content'); + $content.addClass('expanded').off('scroll.expandSection').scrollTop(0); + + if ($content.hasClass('no-animate')) { + expandSectionParent($section, $content); + } else { + $content.on('animationend.expandSectionParent', () => expandSectionParent($section, $content)); + } } function closeSection($section) { $section.find('.js-settings-toggle').text('Expand'); - $section.find('.settings-content').removeClass('expanded').on('scroll', () => expandSection($section)); + + const $content = $section.find('.settings-content'); + $content.removeClass('expanded').on('scroll.expandSection', () => expandSection($section)); + + $section.removeClass('expanded'); } function toggleSection($section) { @@ -21,7 +38,7 @@ function toggleSection($section) { export default function initSettingsPanels() { $('.settings').each((i, elm) => { const $section = $(elm); - $section.on('click', '.js-settings-toggle', () => toggleSection($section)); - $section.find('.settings-content:not(.expanded)').on('scroll', () => expandSection($section)); + $section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section)); + $section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section)); }); } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index fefe5575d9b..95a08c960ea 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -254,7 +254,7 @@ } .landing { - margin-bottom: $gl-padding; + margin: $gl-padding auto; overflow: hidden; display: flex; position: relative; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 880ab52fa1b..767cf5ffea5 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -236,9 +236,6 @@ width: 35px; background-color: $white-light; border: none; - position: static; - right: 0; - height: 100%; outline: none; z-index: 1; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index d4421e3af74..5cf9330b8f8 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -97,17 +97,19 @@ .issues-bulk-update.right-sidebar { @include maintain-sidebar-dimensions; - transition: right $sidebar-transition-duration; - right: -$gutter-width; + width: 0; + padding: 0; + transition: width $sidebar-transition-duration; &.right-sidebar-expanded { @include maintain-sidebar-dimensions; - right: 0; + width: $gutter-width; } &.right-sidebar-collapsed { @include maintain-sidebar-dimensions; - right: -$gutter-width; + width: 0; + padding: 0; .block { padding: 16px 0; @@ -118,5 +120,6 @@ .issuable-sidebar { padding: 0 3px; + width: calc(100% + 35px); } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 7bec4bd5f56..3039732ca5b 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -4,7 +4,7 @@ position: relative; .landing { - margin-top: 10px; + margin-top: 0; .inner-content { white-space: normal; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 72d73b89a2a..6f6c6839975 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -90,8 +90,6 @@ } .explore-groups.landing { - margin-top: 10px; - .inner-content { padding: 0; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index b3f310ff67d..20f2eec9af5 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -204,7 +204,7 @@ .issuable-sidebar { width: calc(100% + 100px); - height: 100%; + height: calc(100% - #{$header-height}); overflow-y: scroll; overflow-x: hidden; -webkit-overflow-scrolling: touch; @@ -729,33 +729,3 @@ } } } - -.confidential-issue-warning { - background-color: $gl-gray; - border-radius: 3px; - padding: $gl-btn-padding $gl-padding; - margin-top: $gl-padding-top; - font-size: 14px; - color: $white-light; - - .fa { - margin-right: 8px; - } - - a { - color: $white-light; - text-decoration: underline; - } - - &.affix { - position: static; - width: initial; - - @media (min-width: $screen-sm-min) { - position: sticky; - position: -webkit-sticky; - top: 60px; - z-index: 200; - } - } -} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index aa307414737..69fed4e6bf7 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -103,6 +103,42 @@ } } +.confidential-issue-warning { + background-color: $gray-normal; + border-radius: 3px; + padding: 3px 12px; + margin: auto; + margin-top: 0; + text-align: center; + font-size: 12px; + align-items: center; + + @media (max-width: $screen-md-max) { + // On smaller devices the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + margin: 6px auto; + width: 100%; + } + + .fa { + margin-right: 8px; + } +} + +.right-sidebar-expanded { + .confidential-issue-warning { + // When the sidebar is open the warning becomes the fourth item in the list, + // rather than centering, and grows to span the full width of the + // comment area. + order: 4; + margin: 6px auto; + width: 100%; + } +} + + .discussion-form { padding: $gl-padding-top $gl-padding $gl-padding; background-color: $white-light; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 33b3c083fd2..7697a1b1c58 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -29,6 +29,10 @@ &:first-of-type { margin-top: 10px; } + + &.expanded { + overflow: visible; + } } .settings-header { diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb index b0450ddc1fd..29beb6cb224 100644 --- a/app/finders/events_finder.rb +++ b/app/finders/events_finder.rb @@ -33,7 +33,8 @@ class EventsFinder private def by_current_user_access(events) - events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project) + events.merge(ProjectsFinder.new(current_user: current_user).execute). + joins(:project) end def by_action(events) diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index f043c38c6f9..f2d3b90b8e2 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -29,35 +29,69 @@ class GroupProjectsFinder < ProjectsFinder private def init_collection - only_owned = options.fetch(:only_owned, false) - only_shared = options.fetch(:only_shared, false) + projects = if current_user + collection_with_user + else + collection_without_user + end - projects = [] + union(projects) + end - if current_user - if group.users.include?(current_user) - projects << group.projects unless only_shared - projects << group.shared_projects unless only_owned + def collection_with_user + if group.users.include?(current_user) + if only_shared? + [shared_projects] + elsif only_owned? + [owned_projects] else - unless only_shared - projects << group.projects.visible_to_user(current_user) - projects << group.projects.public_to_user(current_user) - end - - unless only_owned - projects << group.shared_projects.visible_to_user(current_user) - projects << group.shared_projects.public_to_user(current_user) - end + [shared_projects, owned_projects] end else - projects << group.projects.public_only unless only_shared - projects << group.shared_projects.public_only unless only_owned + if only_shared? + [shared_projects.public_or_visible_to_user(current_user)] + elsif only_owned? + [owned_projects.public_or_visible_to_user(current_user)] + else + [ + owned_projects.public_or_visible_to_user(current_user), + shared_projects.public_or_visible_to_user(current_user) + ] + end end + end - projects + def collection_without_user + if only_shared? + [shared_projects.public_only] + elsif only_owned? + [owned_projects.public_only] + else + [shared_projects.public_only, owned_projects.public_only] + end end def union(items) - find_union(items, Project) + if items.one? + items.first + else + find_union(items, Project) + end + end + + def only_owned? + options.fetch(:only_owned, false) + end + + def only_shared? + options.fetch(:only_shared, false) + end + + def owned_projects + group.projects + end + + def shared_projects + group.shared_projects end end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 5bf722d1ec6..8bfbe37c543 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -28,34 +28,56 @@ class ProjectsFinder < UnionFinder end def execute - items = init_collection - items = items.map do |item| - item = by_ids(item) - item = by_personal(item) - item = by_starred(item) - item = by_trending(item) - item = by_visibilty_level(item) - item = by_tags(item) - item = by_search(item) - by_archived(item) - end - items = union(items) - sort(items) + collection = init_collection + collection = by_ids(collection) + collection = by_personal(collection) + collection = by_starred(collection) + collection = by_trending(collection) + collection = by_visibilty_level(collection) + collection = by_tags(collection) + collection = by_search(collection) + collection = by_archived(collection) + + sort(collection) end private def init_collection - projects = [] + if current_user + collection_with_user + else + collection_without_user + end + end - if params[:owned].present? - projects << current_user.owned_projects if current_user + def collection_with_user + if owned_projects? + current_user.owned_projects else - projects << current_user.authorized_projects if current_user - projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present? + if private_only? + current_user.authorized_projects + else + Project.public_or_visible_to_user(current_user) + end end + end + + # Builds a collection for an anonymous user. + def collection_without_user + if private_only? || owned_projects? + Project.none + else + Project.public_to_user + end + end + + def owned_projects? + params[:owned].present? + end - projects + def private_only? + params[:non_public].present? end def by_ids(items) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 06bbed777d5..a3b243fccb7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -68,7 +68,7 @@ module ApplicationHelper end end - def avatar_icon(user_or_email = nil, size = nil, scale = 2) + def avatar_icon(user_or_email = nil, size = nil, scale = 2, only_path: true) user = if user_or_email.is_a?(User) user_or_email @@ -77,7 +77,7 @@ module ApplicationHelper end if user - user.avatar_url(size: size) || default_avatar + user.avatar_url(size: size, only_path: only_path) || default_avatar else gravatar_icon(user_or_email, size, scale) end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 5e8f0849969..3259a9c1933 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -138,8 +138,8 @@ module IssuablesHelper end output << " ".html_safe - output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs hidden-sm") - output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-md hidden-lg") + output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm") + output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg") output end @@ -216,7 +216,8 @@ module IssuablesHelper initialTitleHtml: markdown_field(issuable, :title), initialTitleText: issuable.title, initialDescriptionHtml: markdown_field(issuable, :description), - initialDescriptionText: issuable.description + initialDescriptionText: issuable.description, + initialTaskStatus: issuable.task_status } data.merge!(updated_at_by(issuable)) diff --git a/app/models/project.rb b/app/models/project.rb index 4c394646787..36ec4f398ca 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -266,20 +266,49 @@ class Project < ActiveRecord::Base enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } + # Returns a collection of projects that is either public or visible to the + # logged in user. + def self.public_or_visible_to_user(user = nil) + if user + authorized = user. + project_authorizations. + select(1). + where('project_authorizations.project_id = projects.id') + + levels = Gitlab::VisibilityLevel.levels_for_user(user) + + where('EXISTS (?) OR projects.visibility_level IN (?)', authorized, levels) + else + public_to_user + end + end + # project features may be "disabled", "internal" or "enabled". If "internal", # they are only available to team members. This scope returns projects where # the feature is either enabled, or internal with permission for the user. + # + # This method uses an optimised version of `with_feature_access_level` for + # logged in users to more efficiently get private projects with the given + # feature. def self.with_feature_available_for_user(feature, user) - return with_feature_enabled(feature) if user.try(:admin?) + visible = [nil, ProjectFeature::ENABLED] - unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED]) - return unconditional if user.nil? + if user&.admin? + with_feature_enabled(feature) + elsif user + column = ProjectFeature.quoted_access_level_column(feature) - conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE) - authorized = user.authorized_projects.merge(conditional.reorder(nil)) + authorized = user.project_authorizations.select(1). + where('project_authorizations.project_id = projects.id') - union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)]) - where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql))) + with_project_feature. + where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", + visible, + ProjectFeature::PRIVATE, + authorized) + else + with_feature_access_level(feature, visible) + end end scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') } diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index e3ef4919b28..dde2a11440d 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -27,6 +27,13 @@ class ProjectFeature < ActiveRecord::Base "#{feature}_access_level".to_sym end + + def quoted_access_level_column(feature) + attribute = connection.quote_column_name(access_level_attribute(feature)) + table = connection.quote_table_name(table_name) + + "#{table}.#{attribute}" + end end # Default scopes force us to unscope here since a service may need to check diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 3edc395033c..d63d4ec2b12 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -70,7 +70,7 @@ module ChatMessage end def branch_link - "`[#{ref}](#{branch_url})`" + "[#{ref}](#{branch_url})" end def project_link diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb index 04a59d559ca..c52dd6ef8ef 100644 --- a/app/models/project_services/chat_message/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -61,7 +61,7 @@ module ChatMessage end def removed_branch_message - "#{user_name} removed #{ref_type} `#{ref}` from #{project_link}" + "#{user_name} removed #{ref_type} #{ref} from #{project_link}" end def push_message @@ -102,7 +102,7 @@ module ChatMessage end def branch_link - "`[#{ref}](#{branch_url})`" + "[#{ref}](#{branch_url})" end def project_link diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 1c24b27a870..fd701e33524 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -12,87 +12,121 @@ module Projects TransferError = Class.new(StandardError) def execute(new_namespace) - if new_namespace.blank? + @new_namespace = new_namespace + + if @new_namespace.blank? raise TransferError, 'Please select a new namespace for your project.' end - unless allowed_transfer?(current_user, project, new_namespace) + + unless allowed_transfer?(current_user, project) raise TransferError, 'Transfer failed, please contact an admin.' end - transfer(project, new_namespace) + + transfer(project) + + true rescue Projects::TransferService::TransferError => ex project.reload project.errors.add(:new_namespace, ex.message) false end - def transfer(project, new_namespace) - old_namespace = project.namespace + private - Project.transaction do - old_path = project.path_with_namespace - old_group = project.group - new_path = File.join(new_namespace.try(:full_path) || '', project.path) + def transfer(project) + @old_path = project.path_with_namespace + @old_group = project.group + @new_path = File.join(@new_namespace.try(:full_path) || '', project.path) + @old_namespace = project.namespace - if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present? - raise TransferError.new("Project with same path in target namespace already exists") - end + if Project.where(path: project.path, namespace_id: @new_namespace.try(:id)).exists? + raise TransferError.new("Project with same path in target namespace already exists") + end - if project.has_container_registry_tags? - # we currently doesn't support renaming repository if it contains tags in container registry - raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') - end + if project.has_container_registry_tags? + # We currently don't support renaming repository if it contains tags in container registry + raise TransferError.new('Project cannot be transferred, because tags are present in its container registry') + end - project.expire_caches_before_rename(old_path) + attempt_transfer_transaction + end + + def attempt_transfer_transaction + Project.transaction do + project.expire_caches_before_rename(@old_path) - # Apply new namespace id and visibility level - project.namespace = new_namespace - project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group? - project.save! + update_namespace_and_visibility(@new_namespace) # Notifications - project.send_move_instructions(old_path) + project.send_move_instructions(@old_path) # Move main repository - unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, new_path) + unless move_repo_folder(@old_path, @new_path) raise TransferError.new('Cannot move project') end # Move wiki repo also if present - gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki") + move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki") # Move missing group labels to project - Labels::TransferService.new(current_user, old_group, project).execute + Labels::TransferService.new(current_user, @old_group, project).execute # Move uploads - Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) + Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) # Move pages - Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path) + Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) - project.old_path_with_namespace = old_path + project.old_path_with_namespace = @old_path - SystemHooksService.new.execute_hooks_for(project, :transfer) + execute_system_hooks end - - refresh_permissions(old_namespace, new_namespace) - - true + rescue Exception # rubocop:disable Lint/RescueException + rollback_side_effects + raise + ensure + refresh_permissions end - def allowed_transfer?(current_user, project, namespace) - namespace && + def allowed_transfer?(current_user, project) + @new_namespace && can?(current_user, :change_namespace, project) && - namespace.id != project.namespace_id && - current_user.can?(:create_projects, namespace) + @new_namespace.id != project.namespace_id && + current_user.can?(:create_projects, @new_namespace) end - def refresh_permissions(old_namespace, new_namespace) + def update_namespace_and_visibility(to_namespace) + # Apply new namespace id and visibility level + project.namespace = to_namespace + project.visibility_level = to_namespace.visibility_level unless project.visibility_level_allowed_by_group? + project.save! + end + + def refresh_permissions # This ensures we only schedule 1 job for every user that has access to # the namespaces. - user_ids = old_namespace.user_ids_for_project_authorizations | - new_namespace.user_ids_for_project_authorizations + user_ids = @old_namespace.user_ids_for_project_authorizations | + @new_namespace.user_ids_for_project_authorizations UserProjectAccessChangedService.new(user_ids).execute end + + def rollback_side_effects + rollback_folder_move + update_namespace_and_visibility(@old_namespace) + end + + def rollback_folder_move + move_repo_folder(@new_path, @old_path) + move_repo_folder("#{@new_path}.wiki", "#{@old_path}.wiki") + end + + def move_repo_folder(from_name, to_name) + gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name) + end + + def execute_system_hooks + SystemHooksService.new.execute_hooks_for(project, :transfer) + end end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 95dffdafabe..b21d5665970 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -325,6 +325,10 @@ = f.label :prometheus_metrics_enabled do = f.check_box :prometheus_metrics_enabled Enable Prometheus Metrics + - unless Gitlab::Metrics.metrics_folder_present? + .help-block + %strong.cred WARNING: + Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory. %fieldset %legend Background Jobs diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index a83faa839df..b7a60938132 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -60,7 +60,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.committer %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@ triggered by - if @pipeline.user %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } - %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } = @pipeline.user.name diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 9c2e2a599b2..3f16885b8e3 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -60,7 +60,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.author %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } @@ -76,7 +76,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } - %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } - if commit.committer %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } @@ -100,7 +100,7 @@ triggered by - if @pipeline.user %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } - %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } = @pipeline.user.name diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 07445434cf3..d0698285f84 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -9,6 +9,12 @@ %li %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } Preview + + - if defined?(@issue) && @issue.confidential? + %li.confidential-issue-warning + = icon('warning') + %span This is a confidential issue. Your comment will not be visible to the public. + %li.pull-right .toolbar-group = markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 24314e03b46..32dbc1b3417 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -9,7 +9,7 @@ .dropzone .dropzone-previews.blob-upload-dropzone-previews %p.dz-message.light - - upload_link = link_to n_('UploadLink|click to upload'), '#', class: "markdown-selector" + - upload_link = link_to s_('UploadLink|click to upload'), '#', class: "markdown-selector" - dropzone_text = _('Attach a file by drag & drop or %{upload_link}') % { upload_link: upload_link } #{ dropzone_text.html_safe } diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 208d4908721..2267f123e38 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -22,7 +22,7 @@ = label_tag 'start_branch', branch_label, class: 'control-label' .col-sm-10 = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' - = dropdown_tag(@project.default_branch, options: { title: n_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: n_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) + = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false } }) - if can?(current_user, :push_code, @project) = render 'shared/new_merge_request_checkbox' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 5f92d020eef..d909b0bfbbd 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -5,13 +5,6 @@ - can_update_issue = can?(current_user, :update_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) -- if defined?(@issue) && @issue.confidential? - .confidential-issue-warning{ data: { spy: 'affix' } } - %span.confidential-issue-text - #{confidential_icon(@issue)} This issue is confidential. - %a{ href: help_page_path('user/project/issues/confidential_issues'), target: '_blank' } - What are confidential issues? - .clearfix.detail-page-header .issuable-header .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } @@ -26,6 +19,7 @@ = icon('angle-double-left') .issuable-meta + = confidential_icon(@issue) = issuable_meta(@issue, @project, "Issue") .issuable-actions diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index e0d45054854..75a4687e1e3 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -1,14 +1,19 @@ -.dropdown.more-actions - = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do - = icon('ellipsis-v', class: 'icon') - %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left - %li - = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' - %li.divider - %li - = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do - Report as abuse - - if note_editable - %li - = link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do - %span.text-danger Delete comment +- is_current_user = current_user == note.author + +- if note_editable || !is_current_user + .dropdown.more-actions + = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do + = icon('ellipsis-v', class: 'icon') + %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left + - if note_editable + %li + = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' + %li.divider + - unless is_current_user + %li + = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do + Report as abuse + - if note_editable + %li + = link_to note_url(note), method: :delete, data: { confirm: 'Are you sure you want to delete this comment?' }, remote: true, class: 'js-note-delete' do + %span.text-danger Delete comment diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index 467f8844e33..fc7fa5c1876 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -15,12 +15,12 @@ .form-group .col-md-9 = f.label :cron_timezone, _('Cron Timezone'), class: 'label-light' - = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: _("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) + = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true .form-group .col-md-9 = f.label :ref, _('Target Branch'), class: 'label-light' - = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: _("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) + = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true .form-group .col-md-9 diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index a8a6d84128d..7cfdfb6e6ee 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -1,7 +1,7 @@ - type = local_assigns.fetch(:type) %aside.issues-bulk-update.js-right-sidebar.right-sidebar.affix-top{ data: { "offset-top" => "50", "spy" => "affix" }, "aria-live" => "polite" } - .issuable-sidebar + .issuable-sidebar.hidden = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do .block .filter-item.inline.update-issues-btn.pull-left diff --git a/changelogs/unreleased/27645-html-email-brackets-bug.yml b/changelogs/unreleased/27645-html-email-brackets-bug.yml new file mode 100644 index 00000000000..e8004d03884 --- /dev/null +++ b/changelogs/unreleased/27645-html-email-brackets-bug.yml @@ -0,0 +1,4 @@ +--- +title: Fix an email parsing bug where brackets would be inserted in emails from some Outlook clients +merge_request: 9045 +author: jneen diff --git a/changelogs/unreleased/30213-project-transfer-move-rollback.yml b/changelogs/unreleased/30213-project-transfer-move-rollback.yml new file mode 100644 index 00000000000..3eb1e399c54 --- /dev/null +++ b/changelogs/unreleased/30213-project-transfer-move-rollback.yml @@ -0,0 +1,4 @@ +--- +title: Rollback project repo move if there is an error in Projects::TransferService +merge_request: 11877 +author: diff --git a/changelogs/unreleased/dt-printing-to-api.yml b/changelogs/unreleased/dt-printing-to-api.yml new file mode 100644 index 00000000000..5253b57f21a --- /dev/null +++ b/changelogs/unreleased/dt-printing-to-api.yml @@ -0,0 +1,4 @@ +--- +title: Added printing_merge_requst_link_enabled to the API +merge_request: +author: David Turner <dturner@twosigma.com> diff --git a/changelogs/unreleased/fix-missing-function-dropzone-input.yml b/changelogs/unreleased/fix-missing-function-dropzone-input.yml deleted file mode 100644 index d9dfc76faaf..00000000000 --- a/changelogs/unreleased/fix-missing-function-dropzone-input.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix for cut & pasted images not working -merge_request: -author: diff --git a/changelogs/unreleased/fixed-confidential-issue-bar.yml b/changelogs/unreleased/fixed-confidential-issue-bar.yml deleted file mode 100644 index 6a41590d0af..00000000000 --- a/changelogs/unreleased/fixed-confidential-issue-bar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make confidential issues more obviously confidential -merge_request: -author: diff --git a/changelogs/unreleased/refactor-projects-finder-init-collection.yml b/changelogs/unreleased/refactor-projects-finder-init-collection.yml new file mode 100644 index 00000000000..c8113419f21 --- /dev/null +++ b/changelogs/unreleased/refactor-projects-finder-init-collection.yml @@ -0,0 +1,5 @@ +--- +title: Refactor ProjectsFinder#init_collection to produce more efficient queries for + retrieving projects +merge_request: +author: diff --git a/config/boot.rb b/config/boot.rb index db5ab918021..16de55d7a86 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -6,7 +6,9 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) # set default directory for multiproces metrics gathering -ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' +if ENV['RAILS_ENV'] == 'development' || ENV['RAILS_ENV'] == 'test' + ENV['prometheus_multiproc_dir'] ||= 'tmp/prometheus_multiproc_dir' +end # Default Bootsnap configuration from https://github.com/Shopify/bootsnap#usage require 'bootsnap' diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 0b33783869b..43a8c0078ca 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -454,6 +454,10 @@ production: &base # introduced in 9.0). Eventually Gitaly use will become mandatory and # this option will disappear. enabled: true + # 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. + token: # # 4. Advanced settings @@ -469,6 +473,7 @@ production: &base default: path: /home/git/repositories/ gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket # TCP connections are supported too (e.g. tcp://host:port) + # gitaly_token: 'special token' # Optional: override global gitaly.token for this storage. ## Backup settings backup: @@ -594,6 +599,7 @@ test: gitaly_address: unix:tmp/tests/gitaly/gitaly.socket gitaly: enabled: true + token: secret backup: path: tmp/tests/backups gitlab_shell: diff --git a/config/karma.config.js b/config/karma.config.js index 5911a9a7e10..2f571978e08 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -66,5 +66,14 @@ module.exports = function(config) { karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1'); } + if (process.env.DEBUG) { + karmaConfig.logLevel = config.LOG_DEBUG; + process.env.CHROME_LOG_FILE = process.env.CHROME_LOG_FILE || 'chrome_debug.log'; + } + + if (process.env.CHROME_LOG_FILE) { + karmaConfig.customLaunchers.ChromeHeadlessCustom.flags.push('--enable-logging', '--v=1'); + } + config.set(karmaConfig); }; diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md new file mode 100644 index 00000000000..07c05b5a6fb --- /dev/null +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -0,0 +1,47 @@ +# GitLab Prometheus metrics + +>**Note:** +Available since [Omnibus GitLab 9.3][29118]. Currently experimental. For installations from source +you'll have to configure it yourself. + +GitLab monitors its own internal service metrics, and makes them available at the `/-/metrics` endpoint. Unlike other [Prometheus] exporters, this endpoint requires authentication as it is available on the same URL and port as user traffic. + +To enable the GitLab Prometheus metrics: + +1. Log into GitLab as an administrator, and go to the Admin area. +1. Click on the gear, then click on Settings. +1. Find the `Metrics - Prometheus` section, and click `Enable Prometheus Metrics` +1. [Restart GitLab][restart] for the changes to take effect + +## Collecting the metrics + +Since the metrics endpoint is available on the same host and port as other traffic, it requires authentication. The token and URL to access is displayed on the [Health Check][health-check] page. + +Currently the embedded Prometheus server is not automatically configured to collect metrics from this endpoint. We recommend setting up another Prometheus server, because the embedded server configuration is overwritten one every reconfigure of GitLab. In the future this will not be required. + +## Metrics available + +In this experimental phase, only a few metrics are available: + +| Metric | Type | Description | +| ------ | ---- | ----------- | +| db_ping_timeout | Gauge | Whether or not the last database ping timed out | +| db_ping_success | Gauge | Whether or not the last database ping succeeded | +| db_ping_latency | Gauge | Round trip time of the database ping | +| redis_ping_timeout | Gauge | Whether or not the last redis ping timed out | +| redis_ping_success | Gauge | Whether or not the last redis ping succeeded | +| redis_ping_latency | Gauge | Round trip time of the redis ping | +| filesystem_access_latency | gauge | Latency in accessing a specific filesystem | +| filesystem_accessible | gauge | Whether or not a specific filesystem is accessible | +| filesystem_write_latency | gauge | Write latency of a specific filesystem | +| filesystem_writable | gauge | Whether or not the filesystem is writable | +| filesystem_read_latency | gauge | Read latency of a specific filesystem | +| filesystem_readable | gauge | Whether or not the filesystem is readable | +| user_sessions_logins | Counter | Counter of how many users have logged in | + +[← Back to the main Prometheus page](index.md) + +[29118]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29118 +[Prometheus]: https://prometheus.io +[restart]: ../../restart_gitlab.md#omnibus-gitlab-restart +[health-check]: ../../../user/admin_area/monitoring/health_check.md diff --git a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md index edb9c911aac..f68b03d1ade 100644 --- a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md +++ b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md @@ -4,7 +4,7 @@ Available since [Omnibus GitLab 8.17][1132]. For installations from source you'll have to install and configure it yourself. -The [GitLab monitor exporter] allows you to measure various GitLab metrics. +The [GitLab monitor exporter] allows you to measure various GitLab metrics, pulled from Redis and the database. To enable the GitLab monitor exporter: diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index b2445d1c0e5..695fdf09a87 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -110,6 +110,14 @@ To disable the monitoring of Kubernetes: 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to take effect +## GitLab Prometheus metrics + +> Introduced as an experimental feature in GitLab 9.3. + +GitLab monitors its own internal service metrics, and makes them available at the `/-/metrics` endpoint. Unlike other exporters, this endpoint requires authentication as it is available on the same URL and port as user traffic. + +[➔ Read more about the GitLab Metrics.](gitlab_metrics.md) + ## Prometheus exporters There are a number of libraries and servers which help in exporting existing @@ -143,7 +151,7 @@ The Postgres exporter allows you to measure various PostgreSQL metrics. ### GitLab monitor exporter -The GitLab monitor exporter allows you to measure various GitLab metrics. +The GitLab monitor exporter allows you to measure various GitLab metrics, pulled from Redis and the database. [➔ Read more about the GitLab monitor exporter.](gitlab_monitor_exporter.md) diff --git a/doc/api/README.md b/doc/api/README.md index 4f189c16673..b7f6ee69193 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -29,10 +29,10 @@ following locations: - [Labels](labels.md) - [Merge Requests](merge_requests.md) - [Milestones](milestones.md) -- [Open source license templates](templates/licenses.md) - [Namespaces](namespaces.md) - [Notes](notes.md) (comments) - [Notification settings](notification_settings.md) +- [Open source license templates](templates/licenses.md) - [Pipelines](pipelines.md) - [Pipeline Triggers](pipeline_triggers.md) - [Pipeline Schedules](pipeline_schedules.md) diff --git a/doc/api/projects.md b/doc/api/projects.md index 58f18105e21..cc1bb3911c8 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -261,6 +261,7 @@ Parameters: ], "only_allow_merge_if_pipeline_succeeds": false, "only_allow_merge_if_all_discussions_are_resolved": false, + "printing_merge_requests_link_enabled": true, "request_access_enabled": false, "statistics": { "commit_count": 37, @@ -344,6 +345,7 @@ Parameters: | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | +| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | ### Create project for user @@ -379,6 +381,7 @@ Parameters: | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | +| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | ### Edit project diff --git a/doc/api/users.md b/doc/api/users.md index b1ebd7b0c47..cf09b8f44aa 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -62,6 +62,7 @@ GET /users "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, "bio": null, "location": null, "skype": "", @@ -94,6 +95,7 @@ GET /users "avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg", "web_url": "http://localhost:3000/jack_smith", "created_at": "2012-05-23T08:01:01Z", + "is_admin": false, "bio": null, "location": null, "skype": "", @@ -197,6 +199,7 @@ Parameters: "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", "web_url": "http://localhost:3000/john_smith", "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, "bio": null, "location": null, "skype": "", diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 197a92905c8..e5aaccdeadf 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -86,56 +86,31 @@ if your available memory changes. Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. -## GitLab Runner - -We strongly advise against installing GitLab Runner on the same machine you plan -to install GitLab on. Depending on how you decide to configure GitLab Runner and -what tools you use to exercise your application in the CI environment, GitLab -Runner can consume significant amount of available memory. - -Memory consumption calculations, that are available above, will not be valid if -you decide to run GitLab Runner and the GitLab Rails application on the same -machine. - -It is also not safe to install everything on a single machine, because of the -[security reasons] - especially when you plan to use shell executor with GitLab -Runner. - -We recommend using a separate machine for each GitLab Runner, if you plan to -use the CI features. - -[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md - -## Unicorn Workers - -It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests. - -For most instances we recommend using: CPU cores + 1 = unicorn workers. -So for a machine with 2 cores, 3 unicorn workers is ideal. - -For all machines that have 2GB and up we recommend a minimum of three unicorn workers. -If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping. - -To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). - ## Database +The server running the database should have _at least_ 5-10 GB of storage +available, though the exact requirements depend on the size of the GitLab +installation (e.g. the number of users, projects, etc). + We currently support the following databases: - PostgreSQL - MySQL/MariaDB -We _highly_ recommend the use of PostgreSQL instead of MySQL/MariaDB as not all -features of GitLab may work with MySQL/MariaDB. For example, MySQL does not have -the right features to support nested groups in an efficient manner; see -<https://gitlab.com/gitlab-org/gitlab-ce/issues/30472> for more information -about this. GitLab Geo also does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication). +We **highly recommend** the use of PostgreSQL instead of MySQL/MariaDB as not all +features of GitLab may work with MySQL/MariaDB: + +1. MySQL support for subgroups was [dropped with GitLab 9.3][post]. + See [issue #30472][30472] for more information. +1. GitLab Geo does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication). +1. [Zero downtime migrations][zero] do not work with MySQL + Existing users using GitLab with MySQL/MariaDB are advised to -migrate to PostgreSQL instead. +[migrate to PostgreSQL](../update/mysql_to_postgresql.md) instead. -The server running the database should have _at least_ 5-10 GB of storage -available, though the exact requirements depend on the size of the GitLab -installation (e.g. the number of users, projects, etc). +[30472]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472 +[zero]: ../update/README.md#upgrading-without-downtime +[post]: https://about.gitlab.com/2017/06/22/gitlab-9-3-released/#dropping-support-for-subgroups-in-mysql ### PostgreSQL Requirements @@ -154,6 +129,18 @@ CREATE EXTENSION pg_trgm; On some systems you may need to install an additional package (e.g. `postgresql-contrib`) for this extension to become available. +## Unicorn Workers + +It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests. + +For most instances we recommend using: CPU cores + 1 = unicorn workers. +So for a machine with 2 cores, 3 unicorn workers is ideal. + +For all machines that have 2GB and up we recommend a minimum of three unicorn workers. +If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping. + +To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings). + ## Redis and Sidekiq Redis stores all user sessions and the background task queue. @@ -172,6 +159,26 @@ default settings. If you would like to disable Prometheus and it's exporters or read more information about it, check the [Prometheus documentation](../administration/monitoring/prometheus/index.md). +## GitLab Runner + +We strongly advise against installing GitLab Runner on the same machine you plan +to install GitLab on. Depending on how you decide to configure GitLab Runner and +what tools you use to exercise your application in the CI environment, GitLab +Runner can consume significant amount of available memory. + +Memory consumption calculations, that are available above, will not be valid if +you decide to run GitLab Runner and the GitLab Rails application on the same +machine. + +It is also not safe to install everything on a single machine, because of the +[security reasons] - especially when you plan to use shell executor with GitLab +Runner. + +We recommend using a separate machine for each GitLab Runner, if you plan to +use the CI features. + +[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md + ## Supported web browsers We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11). diff --git a/doc/update/README.md b/doc/update/README.md index d024a809f24..22dbc7c750f 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -11,22 +11,6 @@ There are currently 3 official ways to install GitLab: Based on your installation, choose a section below that fits your needs. ---- - -<!-- START doctoc generated TOC please keep comment here to allow auto update --> -<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Omnibus Packages](#omnibus-packages) -- [Installation from source](#installation-from-source) -- [Installation using Docker](#installation-using-docker) -- [Upgrading between editions](#upgrading-between-editions) - - [Community to Enterprise Edition](#community-to-enterprise-edition) - - [Enterprise to Community Edition](#enterprise-to-community-edition) -- [Miscellaneous](#miscellaneous) - -<!-- END doctoc generated TOC please keep comment here to allow auto update --> - ## Omnibus Packages - The [Omnibus update guide](http://docs.gitlab.com/omnibus/update/README.html) diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index c4921c74a17..5724dcfab48 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -1,6 +1,9 @@ # Subgroups -> [Introduced][ce-2772] in GitLab 9.0. +>**Notes:** +- [Introduced][ce-2772] in GitLab 9.0. +- Not available when using MySQL as external database (support removed in + GitLab 9.3 [due to performance reasons][issue]). With subgroups (aka nested groups or hierarchical groups) you can have up to 20 levels of nested groups, which among other things can help you to: @@ -173,3 +176,4 @@ Here's a list of what you can't do with subgroups: [ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772 [permissions]: ../../permissions.md#group [reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb +[issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600 diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md index 208be7d0ed5..1760b182114 100644 --- a/doc/user/project/issues/confidential_issues.md +++ b/doc/user/project/issues/confidential_issues.md @@ -43,8 +43,9 @@ next to the issues that are marked as confidential. --- -While inside the issue, you can see a persistent dark banner at the top of the -screen. +Likewise, while inside the issue, you can see the eye-slash icon right next to +the issue number, but there is also an indicator in the comment area that the +issue you are commenting on is confidential. ![Confidential issue page](img/confidential_issues_issue_page.png) diff --git a/doc/user/project/issues/img/confidential_issues_issue_page.png b/doc/user/project/issues/img/confidential_issues_issue_page.png Binary files differindex 91f7cc8d3ca..f04ec8ff32b 100755 --- a/doc/user/project/issues/img/confidential_issues_issue_page.png +++ b/doc/user/project/issues/img/confidential_issues_issue_page.png diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 412443a2405..675bc52a983 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -43,11 +43,14 @@ module API expose :external end - class UserWithPrivateDetails < UserPublic - expose :private_token + class UserWithAdmin < UserPublic expose :admin?, as: :is_admin end + class UserWithPrivateDetails < UserWithAdmin + expose :private_token + end + class Email < Grape::Entity expose :id, :email end @@ -115,6 +118,7 @@ module API expose :only_allow_merge_if_pipeline_succeeds expose :request_access_enabled expose :only_allow_merge_if_all_discussions_are_resolved + expose :printing_merge_request_link_enabled expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 9ec418edea4..479ee16a611 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -71,11 +71,16 @@ module API end # - # Discover user by ssh key + # Discover user by ssh key or user id # get "/discover" do - key = Key.find(params[:key_id]) - present key.user, with: Entities::UserSafe + if params[:key_id] + key = Key.find(params[:key_id]) + user = key.user + elsif params[:user_id] + user = User.find_by(id: params[:user_id]) + end + present user, with: Entities::UserSafe end get "/check" do diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 50d34e8a738..c5df45b7902 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -23,6 +23,7 @@ module API optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' optional :tag_list, type: Array[String], desc: 'The list of tags for a project' optional :avatar, type: File, desc: 'Avatar image for project' + optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' end params :optional_params do @@ -218,6 +219,7 @@ module API :only_allow_merge_if_all_discussions_are_resolved, :only_allow_merge_if_pipeline_succeeds, :path, + :printing_merge_request_link_enabled, :public_builds, :request_access_enabled, :shared_runners_enabled, diff --git a/lib/api/users.rb b/lib/api/users.rb index 7257ecb5b67..bfb69d6dc18 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -59,7 +59,7 @@ module API users = UsersFinder.new(current_user, params).execute - entity = current_user.admin? ? Entities::UserPublic : Entities::UserBasic + entity = current_user.admin? ? Entities::UserWithAdmin : Entities::UserBasic present paginate(users), with: entity end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 284e6ad55a5..818b3d9c46b 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -10,7 +10,7 @@ module Gitlab delegate :sidekiq_throttling_enabled?, to: :current_application_settings - def fake_application_settings(defaults = ApplicationSetting.defaults) + def fake_application_settings(defaults = ::ApplicationSetting.defaults) FakeApplicationSettings.new(defaults) end @@ -24,7 +24,7 @@ module Gitlab def cached_application_settings begin - ApplicationSetting.cached + ::ApplicationSetting.cached rescue ::Redis::BaseError, ::Errno::ENOENT # In case Redis isn't running or the Redis UNIX socket file is not available end @@ -35,7 +35,7 @@ module Gitlab # This loads from the database into the cache, so handle Redis errors begin - db_settings = ApplicationSetting.current + db_settings = ::ApplicationSetting.current rescue ::Redis::BaseError, ::Errno::ENOENT # In case Redis isn't running or the Redis UNIX socket file is not available end @@ -45,14 +45,14 @@ module Gitlab # and other callers from failing, use any loaded settings and return # defaults for missing columns. if ActiveRecord::Migrator.needs_migration? - defaults = ApplicationSetting.defaults + defaults = ::ApplicationSetting.defaults defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present? return fake_application_settings(defaults) end return db_settings if db_settings.present? - ApplicationSetting.create_from_defaults || in_memory_application_settings + ::ApplicationSetting.create_from_defaults || in_memory_application_settings end def in_memory_application_settings diff --git a/lib/gitlab/email/html_parser.rb b/lib/gitlab/email/html_parser.rb index a4ca62bfc41..50559a48973 100644 --- a/lib/gitlab/email/html_parser.rb +++ b/lib/gitlab/email/html_parser.rb @@ -17,6 +17,13 @@ module Gitlab def filter_replies! document.xpath('//blockquote').each(&:remove) document.xpath('//table').each(&:remove) + + # bogus links with no href are sometimes added by outlook, + # and can result in Html2Text adding extra square brackets + # to the text, so we unwrap them here. + document.xpath('//a[not(@href)]').each do |link| + link.replace(link.children) + end end def filtered_html diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index bb04731f08c..d5d149f1423 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -4,7 +4,7 @@ module Gitlab class Commit include Gitlab::EncodingHelper - attr_accessor :raw_commit, :head, :refs + attr_accessor :raw_commit, :head SERIALIZE_KEYS = [ :id, :message, :parent_ids, diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 2343446bf22..f605c06dfc3 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -1,3 +1,5 @@ +require 'base64' + require 'gitaly' module Gitlab @@ -48,6 +50,26 @@ module Gitlab address end + # All Gitaly RPC call sites should use GitalyClient.call. This method + # makes sure that per-request authentication headers are set. + def self.call(storage, service, rpc, request) + metadata = request_metadata(storage) + metadata = yield(metadata) if block_given? + stub(service, storage).send(rpc, request, metadata) + end + + def self.request_metadata(storage) + encoded_token = Base64.strict_encode64(token(storage).to_s) + { metadata: { 'authorization' => "Bearer #{encoded_token}" } } + end + + def self.token(storage) + params = Gitlab.config.repositories.storages[storage] + raise "storage not found: #{storage.inspect}" if params.nil? + + params['gitaly_token'].presence || Gitlab.config.gitaly['token'] + end + def self.enabled? Gitlab.config.gitaly.enabled end diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb index ba3da781dad..73c1848c95f 100644 --- a/lib/gitlab/gitaly_client/commit.rb +++ b/lib/gitlab/gitaly_client/commit.rb @@ -11,28 +11,26 @@ module Gitlab end def is_ancestor(ancestor_id, child_id) - stub = GitalyClient.stub(:commit, @repository.storage) request = Gitaly::CommitIsAncestorRequest.new( repository: @gitaly_repo, ancestor_id: ancestor_id, child_id: child_id ) - stub.commit_is_ancestor(request).value + GitalyClient.call(@repository.storage, :commit, :commit_is_ancestor, request).value end def diff_from_parent(commit, options = {}) request_params = commit_diff_request_params(commit, options) request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false) - - response = diff_service_stub.commit_diff(Gitaly::CommitDiffRequest.new(request_params)) + request = Gitaly::CommitDiffRequest.new(request_params) + response = GitalyClient.call(@repository.storage, :diff, :commit_diff, request) Gitlab::Git::DiffCollection.new(GitalyClient::DiffStitcher.new(response), options) end def commit_deltas(commit) - request_params = commit_diff_request_params(commit) - - response = diff_service_stub.commit_delta(Gitaly::CommitDeltaRequest.new(request_params)) + request = Gitaly::CommitDeltaRequest.new(commit_diff_request_params(commit)) + response = GitalyClient.call(@repository.storage, :diff, :commit_delta, request) response.flat_map do |msg| msg.deltas.map { |d| Gitlab::Git::Diff.new(d) } end @@ -50,10 +48,6 @@ module Gitlab paths: options.fetch(:paths, []) } end - - def diff_service_stub - GitalyClient.stub(:diff, @repository.storage) - end end end end diff --git a/lib/gitlab/gitaly_client/notifications.rb b/lib/gitlab/gitaly_client/notifications.rb index 719554eac52..78ed433e6b8 100644 --- a/lib/gitlab/gitaly_client/notifications.rb +++ b/lib/gitlab/gitaly_client/notifications.rb @@ -1,17 +1,19 @@ module Gitlab module GitalyClient class Notifications - attr_accessor :stub - # 'repository' is a Gitlab::Git::Repository def initialize(repository) @gitaly_repo = repository.gitaly_repository - @stub = GitalyClient.stub(:notifications, repository.storage) + @storage = repository.storage end def post_receive - request = Gitaly::PostReceiveRequest.new(repository: @gitaly_repo) - @stub.post_receive(request) + GitalyClient.call( + @storage, + :notifications, + :post_receive, + Gitaly::PostReceiveRequest.new(repository: @gitaly_repo) + ) end end end diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb index 227fe45642e..6d5f54dd959 100644 --- a/lib/gitlab/gitaly_client/ref.rb +++ b/lib/gitlab/gitaly_client/ref.rb @@ -1,29 +1,28 @@ module Gitlab module GitalyClient class Ref - attr_accessor :stub - # 'repository' is a Gitlab::Git::Repository def initialize(repository) @gitaly_repo = repository.gitaly_repository - @stub = GitalyClient.stub(:ref, repository.storage) + @storage = repository.storage end def default_branch_name request = Gitaly::FindDefaultBranchNameRequest.new(repository: @gitaly_repo) - branch_name = stub.find_default_branch_name(request).name - - Gitlab::Git.branch_name(branch_name) + response = GitalyClient.call(@storage, :ref, :find_default_branch_name, request) + Gitlab::Git.branch_name(response.name) end def branch_names request = Gitaly::FindAllBranchNamesRequest.new(repository: @gitaly_repo) - consume_refs_response(stub.find_all_branch_names(request), prefix: 'refs/heads/') + response = GitalyClient.call(@storage, :ref, :find_all_branch_names, request) + consume_refs_response(response, prefix: 'refs/heads/') end def tag_names request = Gitaly::FindAllTagNamesRequest.new(repository: @gitaly_repo) - consume_refs_response(stub.find_all_tag_names(request), prefix: 'refs/tags/') + response = GitalyClient.call(@storage, :ref, :find_all_tag_names, request) + consume_refs_response(response, prefix: 'refs/tags/') end def find_ref_name(commit_id, ref_prefix) @@ -32,8 +31,7 @@ module Gitlab commit_id: commit_id, prefix: ref_prefix ) - - stub.find_ref_name(request).name + GitalyClient.call(@storage, :ref, :find_ref_name, request).name end def count_tag_names @@ -47,7 +45,8 @@ module Gitlab def local_branches(sort_by: nil) request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo) request.sort_by = sort_by_param(sort_by) if sort_by - consume_branches_response(stub.find_local_branches(request)) + response = GitalyClient.call(@storage, :ref, :find_local_branches, request) + consume_branches_response(response) end private diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb index 60686509332..9d314a56e58 100644 --- a/lib/gitlab/metrics/prometheus.rb +++ b/lib/gitlab/metrics/prometheus.rb @@ -5,8 +5,16 @@ module Gitlab module Prometheus include Gitlab::CurrentSettings + def metrics_folder_present? + ENV.has_key?('prometheus_multiproc_dir') && + ::Dir.exist?(ENV['prometheus_multiproc_dir']) && + ::File.writable?(ENV['prometheus_multiproc_dir']) + end + def prometheus_metrics_enabled? - @prometheus_metrics_enabled ||= current_application_settings[:prometheus_metrics_enabled] || false + return @prometheus_metrics_enabled if defined?(@prometheus_metrics_enabled) + + @prometheus_metrics_enabled = prometheus_metrics_enabled_unmemoized end def registry @@ -36,6 +44,12 @@ module Gitlab NullMetric.new end end + + private + + def prometheus_metrics_enabled_unmemoized + metrics_folder_present? && current_application_settings[:prometheus_metrics_enabled] || false + end end end end diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 2b53798e70f..36e5b5041a6 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -13,18 +13,8 @@ module Gitlab scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) } scope :non_public_only, -> { where.not(visibility_level: PUBLIC) } - scope :public_to_user, -> (user) do - if user - if user.admin? - all - elsif !user.external? - public_and_internal_only - else - public_only - end - else - public_only - end + scope :public_to_user, -> (user = nil) do + where(visibility_level: VisibilityLevel.levels_for_user(user)) end end @@ -35,6 +25,18 @@ module Gitlab class << self delegate :values, to: :options + def levels_for_user(user = nil) + return [PUBLIC] unless user + + if user.admin? + [PRIVATE, INTERNAL, PUBLIC] + elsif user.external? + [PUBLIC] + else + [INTERNAL, PUBLIC] + end + end + def string_values string_options.keys end diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 7f27317775c..f96ee69096d 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -26,7 +26,10 @@ module Gitlab } if Gitlab.config.gitaly.enabled - address = Gitlab::GitalyClient.address(project.repository_storage) + server = { + address: Gitlab::GitalyClient.address(project.repository_storage), + token: Gitlab::GitalyClient.token(project.repository_storage) + } params[:Repository] = repository.gitaly_repository.to_h feature_enabled = case action.to_s @@ -39,8 +42,10 @@ module Gitlab else raise "Unsupported action: #{action}" end - - params[:GitalyAddress] = address if feature_enabled + if feature_enabled + params[:GitalyAddress] = server[:address] # This field will be deprecated + params[:GitalyServer] = server + end end params diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index e88111c3725..a8db5701d0b 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -58,8 +58,9 @@ namespace :gitlab do storages << { name: key, path: val['path'] } end - - TOML.dump(socket_path: address.sub(%r{\Aunix:}, ''), storage: storages) + config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages } + config[:auth] = { token: 'secret' } if Rails.env.test? + TOML.dump(config) end def create_gitaly_configuration diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po index 6946a256308..f661cbddf5f 100644 --- a/locale/es/gitlab.po +++ b/locale/es/gitlab.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2017-06-15 21:59-0500\n" +"PO-Revision-Date: 2017-06-19 15:22-0500\n" "Language-Team: Spanish\n" "Language: es\n" "MIME-Version: 1.0\n" @@ -61,6 +61,12 @@ msgstr[1] "Ramas" msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgstr "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}" +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "Buscar ramas" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "Cambiar rama" + msgid "Branches" msgstr "Ramas" @@ -945,6 +951,9 @@ msgstr "Subir nuevo archivo" msgid "Upload file" msgstr "Subir archivo" +msgid "UploadLink|click to upload" +msgstr "Hacer clic para subir" + msgid "Use your global notification setting" msgstr "Utiliza tu configuración de notificación global" diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2d8076f5567..a2e32b478d3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-06-15 21:59-0500\n" -"PO-Revision-Date: 2017-06-15 21:59-0500\n" +"POT-Creation-Date: 2017-06-19 15:13-0500\n" +"PO-Revision-Date: 2017-06-19 15:13-0500\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -62,6 +62,12 @@ msgstr[1] "" msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgstr "" +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "" + msgid "Branches" msgstr "" @@ -946,6 +952,9 @@ msgstr "" msgid "Upload file" msgstr "" +msgid "UploadLink|click to upload" +msgstr "" + msgid "Use your global notification setting" msgstr "" diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 2eb04df3cb3..a99c19cb787 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -16,6 +16,21 @@ feature 'Issues > Labels bulk assignment', feature: true do gitlab_sign_in user end + context 'sidebar' do + before do + enable_bulk_update + end + + it 'is present when bulk edit is enabled' do + expect(page).to have_css('.issuable-sidebar') + end + + it 'is not present when bulk edit is disabled' do + disable_bulk_update + expect(page).not_to have_css('.issuable-sidebar') + end + end + context 'can bulk assign' do before do enable_bulk_update @@ -398,4 +413,8 @@ feature 'Issues > Labels bulk assignment', feature: true do visit namespace_project_issues_path(project.namespace, project) click_button 'Edit Issues' end + + def disable_bulk_update + click_button 'Cancel' + end end diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb index a88990eada0..5bee4a31379 100644 --- a/spec/features/reportable_note/snippets_spec.rb +++ b/spec/features/reportable_note/snippets_spec.rb @@ -19,15 +19,4 @@ describe 'Reportable note on snippets', :feature, :js do it_behaves_like 'reportable note' end - - describe 'on personal snippet' do - let(:snippet) { create(:personal_snippet, :public, author: user) } - let!(:note) { create(:note_on_personal_snippet, noteable: snippet, author: user) } - - before do - visit snippet_path(snippet) - end - - it_behaves_like 'reportable note' - end end diff --git a/spec/fixtures/emails/html_empty_link.eml b/spec/fixtures/emails/html_empty_link.eml new file mode 100644 index 00000000000..1672b98b925 --- /dev/null +++ b/spec/fixtures/emails/html_empty_link.eml @@ -0,0 +1,26 @@ + +MIME-Version: 1.0 +Received: by 10.25.161.144 with HTTP; Tue, 7 Oct 2014 22:17:17 -0700 (PDT) +X-Originating-IP: [117.207.85.84] +In-Reply-To: <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail> +References: <topic/35@discourse.techapj.com> + <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail> +Date: Wed, 8 Oct 2014 10:47:17 +0530 +Delivered-To: arpit@techapj.com +Message-ID: <CAOJeqne=SJ_LwN4sb-0Y95ejc2OpreVhdmcPn0TnmwSvTCYzzQ@mail.gmail.com> +Subject: Re: [Discourse] [Meta] Welcome to techAPJ's Discourse! +From: Arpit Jalan <arpit@techapj.com> +To: Discourse <mail+e1c7f2a380e33840aeb654f075490bad@arpitjalan.com>Accept-Language: en-US +Content-Language: en-US +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +x-originating-ip: [134.68.31.227] +Content-Type: multipart/alternative; + boundary="_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_" +MIME-Version: 1.0 + +--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_ +Content-Type: text/html; charset="utf-8" + +<a name="_MailEndCompose">no brackets!</a> +--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_-- diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index cc7f889b927..c1966c273db 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -82,42 +82,71 @@ describe ApplicationHelper do end describe 'avatar_icon' do - it 'returns an url for the avatar' do - user = create(:user, avatar: File.open(uploaded_image_temp_path)) - - avatar_url = "/uploads/system/user/avatar/#{user.id}/banana_sample.gif" - - expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) - - allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) - avatar_url = "#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif" - - expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) - end - - it 'returns an url for the avatar with relative url' do - stub_config_setting(relative_url_root: '/gitlab') - # Must be stubbed after the stub above, and separately - stub_config_setting(url: Settings.send(:build_gitlab_url)) - - user = create(:user, avatar: File.open(uploaded_image_temp_path)) - - expect(helper.avatar_icon(user.email).to_s). - to match("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif") - end + let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) } + + context 'using an email' do + context 'when there is a matching user' do + it 'returns a relative URL for the avatar' do + expect(helper.avatar_icon(user.email).to_s). + to eq("/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + + context 'when an asset_host is set in the config' do + let(:asset_host) { 'http://assets' } + + before do + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + end + + it 'returns an absolute URL on that asset host' do + expect(helper.avatar_icon(user.email, only_path: false).to_s). + to eq("#{asset_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + end + + context 'when only_path is set to false' do + it 'returns an absolute URL for the avatar' do + expect(helper.avatar_icon(user.email, only_path: false).to_s). + to eq("#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + end + + context 'when the GitLab instance is at a relative URL' do + before do + stub_config_setting(relative_url_root: '/gitlab') + # Must be stubbed after the stub above, and separately + stub_config_setting(url: Settings.send(:build_gitlab_url)) + end + + it 'returns a relative URL with the correct prefix' do + expect(helper.avatar_icon(user.email).to_s). + to eq("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + end + end - it 'calls gravatar_icon when no User exists with the given email' do - expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2) + context 'when no user exists for the email' do + it 'calls gravatar_icon' do + expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2) - helper.avatar_icon('foo@example.com', 20, 2) + helper.avatar_icon('foo@example.com', 20, 2) + end + end end - describe 'using a User' do - it 'returns an URL for the avatar' do - user = create(:user, avatar: File.open(uploaded_image_temp_path)) + describe 'using a user' do + context 'when only_path is true' do + it 'returns a relative URL for the avatar' do + expect(helper.avatar_icon(user, only_path: true).to_s). + to eq("/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end + end - expect(helper.avatar_icon(user).to_s). - to match("/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + context 'when only_path is false' do + it 'returns an absolute URL for the avatar' do + expect(helper.avatar_icon(user, only_path: false).to_s). + to eq("#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif") + end end end end diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js index ebfd60198b2..694f94efcff 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js @@ -1,15 +1,15 @@ import Vue from 'vue'; -import PipelinesTable from '~/commit/pipelines/pipelines_table'; +import pipelinesTable from '~/commit/pipelines/pipelines_table.vue'; describe('Pipelines table in Commits and Merge requests', () => { const jsonFixtureName = 'pipelines/pipelines.json'; let pipeline; + let PipelinesTable; - preloadFixtures('static/pipelines_table.html.raw'); preloadFixtures(jsonFixtureName); beforeEach(() => { - loadFixtures('static/pipelines_table.html.raw'); + PipelinesTable = Vue.extend(pipelinesTable); const pipelines = getJSONFixture(jsonFixtureName).pipelines; pipeline = pipelines.find(p => p.id === 1); }); @@ -26,8 +26,11 @@ describe('Pipelines table in Commits and Merge requests', () => { Vue.http.interceptors.push(pipelinesEmptyResponse); this.component = new PipelinesTable({ - el: document.querySelector('#commit-pipeline-table-view'), - }); + propsData: { + endpoint: 'endpoint', + helpPagePath: 'foo', + }, + }).$mount(); }); afterEach(function () { @@ -58,8 +61,11 @@ describe('Pipelines table in Commits and Merge requests', () => { Vue.http.interceptors.push(pipelinesResponse); this.component = new PipelinesTable({ - el: document.querySelector('#commit-pipeline-table-view'), - }); + propsData: { + endpoint: 'endpoint', + helpPagePath: 'foo', + }, + }).$mount(); }); afterEach(() => { @@ -92,8 +98,11 @@ describe('Pipelines table in Commits and Merge requests', () => { Vue.http.interceptors.push(pipelinesErrorResponse); this.component = new PipelinesTable({ - el: document.querySelector('#commit-pipeline-table-view'), - }); + propsData: { + endpoint: 'endpoint', + helpPagePath: 'foo', + }, + }).$mount(); }); afterEach(function () { diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index e54ea11b08c..3391cade541 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -16,6 +16,10 @@ import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; const date = new Date(); date.setFullYear(date.getFullYear() + 1); + // Add a day to prevent a transient error. If date is even 1 second + // short of a full year, timeFor will return '11 months remaining' + date.setDate(date.getDate() + 1); + expect( gl.utils.timeFor(date), ).toBe('1 year remaining'); diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index a4b98f6140d..5b64cbb2dfc 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -14,6 +14,7 @@ describe('Deploy keys key', () => { propsData: { deployKey, store, + endpoint: 'https://test.host/dummy/endpoint', }, }).$mount(); }; diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js index a69b39c35c4..08357d2b547 100644 --- a/spec/javascripts/deploy_keys/components/keys_panel_spec.js +++ b/spec/javascripts/deploy_keys/components/keys_panel_spec.js @@ -17,6 +17,7 @@ describe('Deploy keys panel', () => { keys: data.enabled_keys, showHelpBox: true, store, + endpoint: 'https://test.host/dummy/endpoint', }, }).$mount(); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index c92a147b937..9e2076dc383 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -4,6 +4,10 @@ import '~/filtered_search/filtered_search_tokenizer'; import '~/filtered_search/filtered_search_dropdown_manager'; describe('Filtered Search Dropdown Manager', () => { + beforeEach(() => { + spyOn(jQuery, 'ajax'); + }); + describe('addWordToInput', () => { function getInputValue() { return document.querySelector('.filtered-search').value; diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 6d00d71f145..f67cd356ef1 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -1,6 +1,7 @@ import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; +import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; import '~/lib/utils/url_utility'; import '~/lib/utils/common_utils'; import '~/filtered_search/filtered_search_token_keys'; @@ -71,6 +72,7 @@ describe('Filtered Search Manager', () => { beforeEach(() => { spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable); spyOn(recentSearchesStoreSrc, 'default'); + spyOn(RecentSearchesRoot.prototype, 'render'); filteredSearchManager = new gl.FilteredSearchManager(); filteredSearchManager.setup(); diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index a746a776548..0715f4d5f6b 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -55,13 +55,20 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merge_request) end + it 'merge_requests/changes_tab_with_comments.json' do |example| + create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) + create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) + render_merge_request(example.description, merge_request, action: :diffs, format: :json) + end + private - def render_merge_request(fixture_file_name, merge_request) - get :show, + def render_merge_request(fixture_file_name, merge_request, action: :show, format: :html) + get action, namespace_id: project.namespace.to_param, project_id: project, - id: merge_request.to_param + id: merge_request.to_param, + format: format expect(response).to be_success store_frontend_fixture(response, fixture_file_name) diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml deleted file mode 100644 index ad1682704bb..00000000000 --- a/spec/javascripts/fixtures/pipelines_table.html.haml +++ /dev/null @@ -1 +0,0 @@ -#commit-pipeline-table-view{ data: { endpoint: "endpoint", "help-page-path": "foo" } } diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 2ccc4f16192..276e01fc82f 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -51,7 +51,6 @@ describe('Issuable output', () => { }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, issueShowInterceptor); }); it('should render a title/description/edited and update title/description/edited on update', (done) => { diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 886462c4b9a..f3fdbff01a6 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -95,6 +95,18 @@ describe('Description component', () => { done(); }); }); + + it('clears task status text when no tasks are present', (done) => { + vm.taskStatus = '0 of 0'; + + setTimeout(() => { + expect( + document.querySelector('.issuable-meta #task_status').textContent.trim(), + ).toBe(''); + + done(); + }); + }); }); it('applies syntax highlighting and math when description changed', (done) => { diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index e54acfa8e44..b6d0ce02c4f 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -7,54 +7,92 @@ import '~/render_gfm'; import '~/render_math'; import '~/notes'; +const upArrowKeyCode = 38; + describe('Merge request notes', () => { window.gon = window.gon || {}; window.gl = window.gl || {}; gl.utils = gl.utils || {}; - const fixture = 'merge_requests/diff_comment.html.raw'; - preloadFixtures(fixture); + const discussionTabFixture = 'merge_requests/diff_comment.html.raw'; + const changesTabJsonFixture = 'merge_requests/changes_tab_with_comments.json'; + preloadFixtures(discussionTabFixture, changesTabJsonFixture); - beforeEach(() => { - loadFixtures(fixture); - gl.utils.disableButtonIfEmptyField = _.noop; - window.project_uploads_path = 'http://test.host/uploads'; - $('body').data('page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('author-id'); + describe('Discussion tab with diff comments', () => { + beforeEach(() => { + loadFixtures(discussionTabFixture); + gl.utils.disableButtonIfEmptyField = _.noop; + window.project_uploads_path = 'http://test.host/uploads'; + $('body').data('page', 'projects:merge_requests:show'); + window.gon.current_user_id = $('.note:last').data('author-id'); - return new Notes('', []); - }); + return new Notes('', []); + }); + + describe('up arrow', () => { + it('edits last comment when triggered in main form', () => { + const upArrowEvent = $.Event('keydown'); + upArrowEvent.which = upArrowKeyCode; + + spyOnEvent('.note:last .js-note-edit', 'click'); + + $('.js-note-text').trigger(upArrowEvent); + + expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); + }); + + it('edits last comment in discussion when triggered in discussion form', (done) => { + const upArrowEvent = $.Event('keydown'); + upArrowEvent.which = upArrowKeyCode; + + spyOnEvent('.note-discussion .js-note-edit', 'click'); + + $('.js-discussion-reply-button').click(); - describe('up arrow', () => { - it('edits last comment when triggered in main form', () => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = 38; + setTimeout(() => { + expect( + $('.note-discussion .js-note-text'), + ).toExist(); - spyOnEvent('.note:last .js-note-edit', 'click'); + $('.note-discussion .js-note-text').trigger(upArrowEvent); - $('.js-note-text').trigger(upArrowEvent); + expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit'); - expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); + done(); + }); + }); }); + }); - it('edits last comment in discussion when triggered in discussion form', (done) => { - const upArrowEvent = $.Event('keydown'); - upArrowEvent.which = 38; + describe('Changes tab with diff comments', () => { + beforeEach(() => { + const diffsResponse = getJSONFixture(changesTabJsonFixture); + const noteFormHtml = `<form class="js-new-note-form"> + <textarea class="js-note-text"></textarea> + </form>`; + setFixtures(diffsResponse.html + noteFormHtml); + $('body').data('page', 'projects:merge_requests:show'); + window.gon.current_user_id = $('.note:last').data('author-id'); + + return new Notes('', []); + }); - spyOnEvent('.note-discussion .js-note-edit', 'click'); + describe('up arrow', () => { + it('edits last comment in discussion when triggered in discussion form', (done) => { + const upArrowEvent = $.Event('keydown'); + upArrowEvent.which = upArrowKeyCode; - $('.js-discussion-reply-button').click(); + spyOnEvent('.note:last .js-note-edit', 'click'); - setTimeout(() => { - expect( - $('.note-discussion .js-note-text'), - ).toExist(); + $('.js-discussion-reply-button').trigger('click'); - $('.note-discussion .js-note-text').trigger(upArrowEvent); + setTimeout(() => { + $('.js-note-text').trigger(upArrowEvent); - expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit'); + expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit'); - done(); + done(); + }); }); }); }); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 28c9c7ab282..48620898357 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -1,25 +1,20 @@ import Vue from 'vue'; import asyncButtonComp from '~/pipelines/components/async_button.vue'; +import eventHub from '~/pipelines/event_hub'; describe('Pipelines Async Button', () => { let component; - let spy; let AsyncButtonComponent; beforeEach(() => { AsyncButtonComponent = Vue.extend(asyncButtonComp); - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); - component = new AsyncButtonComponent({ propsData: { endpoint: '/foo', title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - service: { - postAction: spy, - }, }, }).$mount(); }); @@ -33,7 +28,7 @@ describe('Pipelines Async Button', () => { }); it('should render the provided title', () => { - expect(component.$el.getAttribute('title')).toContain('Foo'); + expect(component.$el.getAttribute('data-original-title')).toContain('Foo'); expect(component.$el.getAttribute('aria-label')).toContain('Foo'); }); @@ -41,37 +36,12 @@ describe('Pipelines Async Button', () => { expect(component.$el.getAttribute('class')).toContain('bar'); }); - it('should call the service when it is clicked with the provided endpoint', () => { - component.$el.click(); - expect(spy).toHaveBeenCalledWith('/foo'); - }); - - it('should hide loading if request fails', () => { - spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); - - component = new AsyncButtonComponent({ - propsData: { - endpoint: '/foo', - title: 'Foo', - icon: 'fa fa-foo', - cssClass: 'bar', - dataAttributes: { - 'data-foo': 'foo', - }, - service: { - postAction: spy, - }, - }, - }).$mount(); - - component.$el.click(); - expect(component.$el.querySelector('.fa-spinner')).toBe(null); - }); - describe('With confirm dialog', () => { it('should call the service when confimation is positive', () => { spyOn(window, 'confirm').and.returnValue(true); - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); + eventHub.$on('postAction', (endpoint) => { + expect(endpoint).toEqual('/foo'); + }); component = new AsyncButtonComponent({ propsData: { @@ -79,15 +49,11 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - service: { - postAction: spy, - }, confirmActionMessage: 'bar', }, }).$mount(); component.$el.click(); - expect(spy).toHaveBeenCalledWith('/foo'); }); }); }); diff --git a/spec/javascripts/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js index 8a58b77f1e3..72fb0a8f9ef 100644 --- a/spec/javascripts/pipelines/pipelines_actions_spec.js +++ b/spec/javascripts/pipelines/pipelines_actions_spec.js @@ -3,7 +3,6 @@ import pipelinesActionsComp from '~/pipelines/components/pipelines_actions.vue'; describe('Pipelines Actions dropdown', () => { let component; - let spy; let actions; let ActionsComponent; @@ -22,14 +21,9 @@ describe('Pipelines Actions dropdown', () => { }, ]; - spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve()); - component = new ActionsComponent({ propsData: { actions, - service: { - postAction: spy, - }, }, }).$mount(); }); @@ -40,31 +34,6 @@ describe('Pipelines Actions dropdown', () => { ).toEqual(actions.length); }); - it('should call the service when an action is clicked', () => { - component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); - component.$el.querySelector('.js-pipeline-action-link').click(); - - expect(spy).toHaveBeenCalledWith(actions[0].path); - }); - - it('should hide loading if request fails', () => { - spy = jasmine.createSpy('spy').and.returnValue(Promise.reject()); - - component = new ActionsComponent({ - propsData: { - actions, - service: { - postAction: spy, - }, - }, - }).$mount(); - - component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click(); - component.$el.querySelector('.js-pipeline-action-link').click(); - - expect(component.$el.querySelector('.fa-spinner')).toEqual(null); - }); - it('should render a disabled action when it\'s not playable', () => { expect( component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'), diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 9475ee28a03..7ce39dca112 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import tableRowComp from '~/vue_shared/components/pipelines_table_row.vue'; +import tableRowComp from '~/pipelines/components/pipelines_table_row.vue'; describe('Pipelines Table Row', () => { const jsonFixtureName = 'pipelines/pipelines.json'; diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js index 4c35d702004..3afe89c8db4 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import pipelinesTableComp from '~/vue_shared/components/pipelines_table.vue'; +import pipelinesTableComp from '~/pipelines/components/pipelines_table.vue'; import '~/lib/utils/datetime_utility'; describe('Pipelines Table', () => { @@ -22,7 +22,6 @@ describe('Pipelines Table', () => { component = new PipelinesTableComponent({ propsData: { pipelines: [], - service: {}, }, }).$mount(); }); @@ -48,7 +47,6 @@ describe('Pipelines Table', () => { const component = new PipelinesTableComponent({ propsData: { pipelines: [], - service: {}, }, }).$mount(); expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0); @@ -58,10 +56,8 @@ describe('Pipelines Table', () => { describe('with data', () => { it('should render rows', () => { const component = new PipelinesTableComponent({ - el: document.querySelector('.test-dom-element'), propsData: { pipelines: [pipeline], - service: {}, }, }).$mount(); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 729db02e06c..f0d51bd0902 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,8 +1,14 @@ +/* eslint-disable jasmine/no-global-setup */ import $ from 'jquery'; import _ from 'underscore'; import 'jasmine-jquery'; import '~/commons'; +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + // enable test fixtures jasmine.getFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; @@ -22,7 +28,25 @@ window.gon = window.gon || {}; // enough for the socket to continue to communicate. // The downside is that it creates a minor performance penalty in the time it takes // to run our unit tests. -beforeEach(done => done()); // eslint-disable-line jasmine/no-global-setup +beforeEach(done => done()); + +beforeAll(() => { + const origError = console.error; + spyOn(console, 'error').and.callFake((message) => { + if (/^\[Vue warn\]/.test(message)) { + fail(message); + } else { + origError(message); + } + }); +}); + +const builtinVueHttpInterceptors = Vue.http.interceptors.slice(); + +beforeEach(() => { + // restore interceptors so we have no remaining ones from previous tests + Vue.http.interceptors = builtinVueHttpInterceptors.slice(); +}); // render all of our tests const testsContext = require.context('.', true, /_spec$/); diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index 28698e89c33..71659d5e8b0 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -208,5 +208,9 @@ describe Gitlab::Email::ReplyParser, lib: true do it "properly renders html-only email from MS Outlook" do expect(test_parse_body(fixture_file("emails/outlook_html.eml"))).to eq("Microsoft Outlook 2010") end + + it "does not wrap links with no href in unnecessary brackets" do + expect(test_parse_body(fixture_file("emails/html_empty_link.eml"))).to eq("no brackets!") + end end end diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb index cf1bc74779e..dff5b25c712 100644 --- a/spec/lib/gitlab/gitaly_client/commit_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::GitalyClient::Commit do right_commit_id: commit.id ) - expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) described_class.new(repository).diff_from_parent(commit) end @@ -31,7 +31,7 @@ describe Gitlab::GitalyClient::Commit do right_commit_id: initial_commit.id ) - expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) described_class.new(repository).diff_from_parent(initial_commit) end @@ -61,7 +61,7 @@ describe Gitlab::GitalyClient::Commit do right_commit_id: commit.id ) - expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([]) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request, kind_of(Hash)).and_return([]) described_class.new(repository).commit_deltas(commit) end @@ -76,7 +76,7 @@ describe Gitlab::GitalyClient::Commit do right_commit_id: initial_commit.id ) - expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request).and_return([]) + expect_any_instance_of(Gitaly::Diff::Stub).to receive(:commit_delta).with(request, kind_of(Hash)).and_return([]) described_class.new(repository).commit_deltas(initial_commit) end diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb index e5c9e06a15e..c2b8ca9f501 100644 --- a/spec/lib/gitlab/gitaly_client/notifications_spec.rb +++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::GitalyClient::Notifications do it 'sends a post_receive message' do expect_any_instance_of(Gitaly::Notifications::Stub). - to receive(:post_receive).with(gitaly_request_with_path(storage_name, relative_path)) + to receive(:post_receive).with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) subject.post_receive end diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb index 2ea44ef74b0..3272333bb33 100644 --- a/spec/lib/gitlab/gitaly_client/ref_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::GitalyClient::Ref do it 'sends a find_all_branch_names message' do expect_any_instance_of(Gitaly::Ref::Stub). to receive(:find_all_branch_names). - with(gitaly_request_with_path(storage_name, relative_path)). + with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)). and_return([]) client.branch_names @@ -32,7 +32,7 @@ describe Gitlab::GitalyClient::Ref do it 'sends a find_all_tag_names message' do expect_any_instance_of(Gitaly::Ref::Stub). to receive(:find_all_tag_names). - with(gitaly_request_with_path(storage_name, relative_path)). + with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)). and_return([]) client.tag_names @@ -43,7 +43,7 @@ describe Gitlab::GitalyClient::Ref do it 'sends a find_default_branch_name message' do expect_any_instance_of(Gitaly::Ref::Stub). to receive(:find_default_branch_name). - with(gitaly_request_with_path(storage_name, relative_path)). + with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)). and_return(double(name: 'foo')) client.default_branch_name @@ -54,7 +54,7 @@ describe Gitlab::GitalyClient::Ref do it 'sends a find_local_branches message' do expect_any_instance_of(Gitaly::Ref::Stub). to receive(:find_local_branches). - with(gitaly_request_with_path(storage_name, relative_path)). + with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)). and_return([]) client.local_branches @@ -63,7 +63,7 @@ describe Gitlab::GitalyClient::Ref do it 'parses and sends the sort parameter' do expect_any_instance_of(Gitaly::Ref::Stub). to receive(:find_local_branches). - with(gitaly_request_with_params(sort_by: :UPDATED_DESC)). + with(gitaly_request_with_params(sort_by: :UPDATED_DESC), kind_of(Hash)). and_return([]) client.local_branches(sort_by: 'updated_desc') diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 5a87b906609..58a84cd3fe1 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -15,6 +15,36 @@ describe Gitlab::Metrics do end end + describe '.prometheus_metrics_enabled_unmemoized' do + subject { described_class.send(:prometheus_metrics_enabled_unmemoized) } + + context 'prometheus metrics enabled in config' do + before do + allow(described_class).to receive(:current_application_settings).and_return(prometheus_metrics_enabled: true) + end + + context 'when metrics folder is present' do + before do + allow(described_class).to receive(:metrics_folder_present?).and_return(true) + end + + it 'metrics are enabled' do + expect(subject).to eq(true) + end + end + + context 'when metrics folder is missing' do + before do + allow(described_class).to receive(:metrics_folder_present?).and_return(false) + end + + it 'metrics are disabled' do + expect(subject).to eq(false) + end + end + end + end + describe '.prometheus_metrics_enabled?' do it 'returns a boolean' do expect(described_class.prometheus_metrics_enabled?).to be_in([true, false]) diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 3255c6f1ef7..a8f21803ec7 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -18,4 +18,35 @@ describe Gitlab::VisibilityLevel, lib: true do expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE) end end + + describe '.levels_for_user' do + it 'returns all levels for an admin' do + user = double(:user, admin?: true) + + expect(described_class.levels_for_user(user)). + to eq([Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'returns INTERNAL and PUBLIC for internal users' do + user = double(:user, admin?: false, external?: false) + + expect(described_class.levels_for_user(user)). + to eq([Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'returns PUBLIC for external users' do + user = double(:user, admin?: false, external?: true) + + expect(described_class.levels_for_user(user)). + to eq([Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'returns PUBLIC when no user is given' do + expect(described_class.levels_for_user). + to eq([Gitlab::VisibilityLevel::PUBLIC]) + end + end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index ad19998dff4..a3e8166cb70 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -202,7 +202,11 @@ describe Gitlab::Workhorse, lib: true do context 'when Gitaly is enabled' do let(:gitaly_params) do { - GitalyAddress: Gitlab::GitalyClient.address('default') + GitalyAddress: Gitlab::GitalyClient.address('default'), + GitalyServer: { + address: Gitlab::GitalyClient.address('default'), + token: Gitlab::GitalyClient.token('default') + } } end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index 09a4448d387..580c83c12c0 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -4,6 +4,18 @@ describe ProjectFeature do let(:project) { create(:empty_project) } let(:user) { create(:user) } + describe '.quoted_access_level_column' do + it 'returns the table name and quoted column name for a feature' do + expected = if Gitlab::Database.postgresql? + '"project_features"."issues_access_level"' + else + '`project_features`.`issues_access_level`' + end + + expect(described_class.quoted_access_level_column(:issues)).to eq(expected) + end + end + describe '#feature_available?' do let(:features) { %w(issues wiki builds merge_requests snippets repository) } diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb index 7d2599dc703..43b02568cb9 100644 --- a/spec/models/project_services/chat_message/pipeline_message_spec.rb +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -62,7 +62,7 @@ describe ChatMessage::PipelineMessage do def build_message(status_text = status, name = user[:name]) "<http://example.gitlab.com|project_name>:" \ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \ - " of branch `<http://example.gitlab.com/commits/develop|develop>`" \ + " of branch <http://example.gitlab.com/commits/develop|develop>" \ " by #{name} #{status_text} in 02:00:10" end end @@ -81,7 +81,7 @@ describe ChatMessage::PipelineMessage do expect(subject.pretext).to be_empty expect(subject.attachments).to eq(message) expect(subject.activity).to eq({ - title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker passed', + title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker passed', subtitle: 'in [project_name](http://example.gitlab.com)', text: 'in 02:00:10', image: '' @@ -98,7 +98,7 @@ describe ChatMessage::PipelineMessage do expect(subject.pretext).to be_empty expect(subject.attachments).to eq(message) expect(subject.activity).to eq({ - title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker failed', + title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by hacker failed', subtitle: 'in [project_name](http://example.gitlab.com)', text: 'in 02:00:10', image: '' @@ -113,7 +113,7 @@ describe ChatMessage::PipelineMessage do expect(subject.pretext).to be_empty expect(subject.attachments).to eq(message) expect(subject.activity).to eq({ - title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by API failed', + title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch [develop](http://example.gitlab.com/commits/develop) by API failed', subtitle: 'in [project_name](http://example.gitlab.com)', text: 'in 02:00:10', image: '' @@ -125,7 +125,7 @@ describe ChatMessage::PipelineMessage do def build_markdown_message(status_text = status, name = user[:name]) "[project_name](http://example.gitlab.com):" \ " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \ - " of branch `[develop](http://example.gitlab.com/commits/develop)`" \ + " of branch [develop](http://example.gitlab.com/commits/develop)" \ " by #{name} #{status_text} in 02:00:10" end end diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb index e38117b75f6..c794f659c41 100644 --- a/spec/models/project_services/chat_message/push_message_spec.rb +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -28,7 +28,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch `<http://url.com/commits/master|master>` of '\ + 'test.user pushed to branch <http://url.com/commits/master|master> of '\ '<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)') expect(subject.attachments).to eq([{ text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\ @@ -45,7 +45,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed to branch `[master](http://url.com/commits/master)` of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))') + 'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))') expect(subject.attachments).to eq( "[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 - author2") expect(subject.activity).to eq({ @@ -74,7 +74,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding pushes' do expect(subject.pretext).to eq('test.user pushed new tag ' \ - '`<http://url.com/commits/new_tag|new_tag>` to ' \ + '<http://url.com/commits/new_tag|new_tag> to ' \ '<http://url.com|project_name>') expect(subject.attachments).to be_empty end @@ -87,7 +87,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding pushes' do expect(subject.pretext).to eq( - 'test.user pushed new tag `[new_tag](http://url.com/commits/new_tag)` to [project_name](http://url.com)') + 'test.user pushed new tag [new_tag](http://url.com/commits/new_tag) to [project_name](http://url.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'test.user created tag', @@ -107,7 +107,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch `<http://url.com/commits/master|master>` to '\ + 'test.user pushed new branch <http://url.com/commits/master|master> to '\ '<http://url.com|project_name>') expect(subject.attachments).to be_empty end @@ -120,7 +120,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a new branch' do expect(subject.pretext).to eq( - 'test.user pushed new branch `[master](http://url.com/commits/master)` to [project_name](http://url.com)') + 'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'test.user created branch', @@ -140,7 +140,7 @@ describe ChatMessage::PushMessage, models: true do context 'without markdown' do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch `master` from <http://url.com|project_name>') + 'test.user removed branch master from <http://url.com|project_name>') expect(subject.attachments).to be_empty end end @@ -152,7 +152,7 @@ describe ChatMessage::PushMessage, models: true do it 'returns a message regarding a removed branch' do expect(subject.pretext).to eq( - 'test.user removed branch `master` from [project_name](http://url.com)') + 'test.user removed branch master from [project_name](http://url.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'test.user removed branch', diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 63333b7af1f..c7ba3ae903d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2060,4 +2060,36 @@ describe Project, models: true do expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i) end end + + describe '.public_or_visible_to_user' do + let!(:user) { create(:user) } + + let!(:private_project) do + create(:empty_project, :private, creator: user, namespace: user.namespace) + end + + let!(:public_project) { create(:empty_project, :public) } + + context 'with a user' do + let(:projects) do + Project.all.public_or_visible_to_user(user) + end + + it 'includes projects the user has access to' do + expect(projects).to include(private_project) + end + + it 'includes projects the user can see' do + expect(projects).to include(public_project) + end + end + + context 'without a user' do + it 'only includes public projects' do + projects = Project.all.public_or_visible_to_user + + expect(projects).to eq([public_project]) + end + end + end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index bc869ea1108..750682bde52 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -11,7 +11,7 @@ describe API::Users do let(:not_existing_user_id) { (User.maximum('id') || 0 ) + 10 } let(:not_existing_pat_id) { (PersonalAccessToken.maximum('id') || 0 ) + 10 } - describe "GET /users" do + describe 'GET /users' do context "when unauthenticated" do it "returns authentication error" do get api("/users") @@ -76,6 +76,12 @@ describe API::Users do expect(response).to have_http_status(403) end + + it 'does not reveal the `is_admin` flag of the user' do + get api('/users', user) + + expect(json_response.first.keys).not_to include 'is_admin' + end end context "when admin" do @@ -92,6 +98,7 @@ describe API::Users do expect(json_response.first.keys).to include 'two_factor_enabled' expect(json_response.first.keys).to include 'last_sign_in_at' expect(json_response.first.keys).to include 'confirmed_at' + expect(json_response.first.keys).to include 'is_admin' end it "returns an array of external users" do diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index e9c57f7c6c3..6d7401f9764 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -7,6 +7,38 @@ describe API::V3::Users do let(:email) { create(:email, user: user) } let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + describe 'GET /users' do + context 'when authenticated' do + it 'returns an array of users' do + get v3_api('/users', user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + username = user.username + expect(json_response.detect do |user| + user['username'] == username + end['username']).to eq(username) + end + end + + context 'when authenticated as user' do + it 'does not reveal the `is_admin` flag of the user' do + get v3_api('/users', user) + + expect(json_response.first.keys).not_to include 'is_admin' + end + end + + context 'when authenticated as admin' do + it 'reveals the `is_admin` flag of the user' do + get v3_api('/users', admin) + + expect(json_response.first.keys).to include 'is_admin' + end + end + end + describe 'GET /user/:id/keys' do before { admin } diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 5d2f4cf17fb..63bd0353d50 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -19,6 +19,67 @@ describe Projects::TransferService, services: true do it { expect(project.namespace).to eq(group) } end + context 'when transfer succeeds' do + before do + group.add_owner(user) + end + + it 'sends notifications' do + expect_any_instance_of(NotificationService).to receive(:project_was_moved) + + transfer_project(project, user, group) + end + + it 'executes system hooks' do + expect_any_instance_of(Projects::TransferService).to receive(:execute_system_hooks) + + transfer_project(project, user, group) + end + end + + context 'when transfer fails' do + let!(:original_path) { project_path(project) } + + def attempt_project_transfer + expect do + transfer_project(project, user, group) + end.to raise_error(ActiveRecord::ActiveRecordError) + end + + before do + group.add_owner(user) + + expect_any_instance_of(Labels::TransferService).to receive(:execute).and_raise(ActiveRecord::StatementInvalid, "PG ERROR") + end + + def project_path(project) + File.join(project.repository_storage_path, "#{project.path_with_namespace}.git") + end + + def current_path + project_path(project) + end + + it 'rolls back repo location' do + attempt_project_transfer + + expect(Dir.exist?(original_path)).to be_truthy + expect(original_path).to eq current_path + end + + it "doesn't send move notifications" do + expect_any_instance_of(NotificationService).not_to receive(:project_was_moved) + + attempt_project_transfer + end + + it "doesn't run system hooks" do + expect_any_instance_of(Projects::TransferService).not_to receive(:execute_system_hooks) + + attempt_project_transfer + end + end + context 'namespace -> no namespace' do before do @result = transfer_project(project, user, nil) diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index 0d80c95e826..27e079c01dd 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -13,9 +13,7 @@ shared_examples 'reportable note' do it 'dropdown has Edit, Report and Delete links' do dropdown = comment.find(more_actions_selector) - - dropdown.click - dropdown.find('.dropdown-menu li', match: :first) + open_dropdown(dropdown) expect(dropdown).to have_button('Edit comment') expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) @@ -24,13 +22,16 @@ shared_examples 'reportable note' do it 'Report button links to a report page' do dropdown = comment.find(more_actions_selector) - - dropdown.click - dropdown.find('.dropdown-menu li', match: :first) + open_dropdown(dropdown) dropdown.click_link('Report as abuse') expect(find('#user_name')['value']).to match(note.author.username) expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note)) end + + def open_dropdown(dropdown) + dropdown.click + dropdown.find('.dropdown-menu li', match: :first) + end end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index cfa6c9ca8ce..c9a0f1cb144 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -89,6 +89,7 @@ describe 'gitlab:gitaly namespace rake task' do } } allow(Gitlab.config.repositories).to receive(:storages).and_return(config) + allow(Rails.env).to receive(:test?).and_return(false) expected_output = '' Timecop.freeze do diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb new file mode 100644 index 00000000000..e56c0f6be03 --- /dev/null +++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'projects/notes/_more_actions_dropdown', :view do + let(:author_user) { create(:user) } + let(:not_author_user) { create(:user) } + + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let!(:note) { create(:note_on_issue, author: author_user, noteable: issue, project: project) } + + before do + assign(:project, project) + end + + it 'shows Report as abuse button if not editable and not current users comment' do + render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: false, note: note + + expect(rendered).to have_link('Report as abuse') + end + + it 'does not show the More actions button if not editable and current users comment' do + render 'projects/notes/more_actions_dropdown', current_user: author_user, note_editable: false, note: note + + expect(rendered).not_to have_selector('.dropdown.more-actions') + end + + it 'shows Report as abuse, Edit and Delete buttons if editable and not current users comment' do + render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note + + expect(rendered).to have_link('Report as abuse') + expect(rendered).to have_button('Edit comment') + expect(rendered).to have_link('Delete comment') + end + + it 'shows Edit and Delete buttons if editable and current users comment' do + render 'projects/notes/more_actions_dropdown', current_user: author_user, note_editable: true, note: note + + expect(rendered).to have_button('Edit comment') + expect(rendered).to have_link('Delete comment') + end +end |